<?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>Evidence Package on Tarragon</title><link>https://tarrragon.github.io/blog/tags/evidence-package/</link><description>Recent content in Evidence Package on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 11 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/evidence-package/index.xml" rel="self" type="application/rss+xml"/><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.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>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>Evidence Package</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/</guid><description>&lt;p>Evidence package 的核心概念是「把查詢、時間窗、資料品質限制與 owner 打包成可交接證據」。它連接 &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/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a>，讓事故與驗證能回放同一組事實。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Evidence package 位在 &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/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/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 之間。Dashboard 提供操作視角，SLO 提供判讀門檻，evidence package 保存支撐判斷的來源、時間窗、查詢入口與限制。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>系統需要 evidence package 的訊號是同一段事故證據在交班、release gate 或復盤時反覆被重新查證。常見例子是只保存截圖，下一班 on-call 看得到圖表形狀，卻缺少 query、time range、sampling ratio、ingest delay 與 owner，導致決策背景需要重新建立。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Evidence package 要包含 source、time range、query link、owner、data quality、confidence、known gap 與 retention。它的責任是讓證據可查、可解釋、可重跑，並能交給 &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/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/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action item closure&lt;/a> 使用。&lt;/p></description><content:encoded><![CDATA[<p>Evidence package 的核心概念是「把查詢、時間窗、資料品質限制與 owner 打包成可交接證據」。它連接 <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/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a>，讓事故與驗證能回放同一組事實。</p>
<h2 id="概念位置">概念位置</h2>
<p>Evidence package 位在 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</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/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 之間。Dashboard 提供操作視角，SLO 提供判讀門檻，evidence package 保存支撐判斷的來源、時間窗、查詢入口與限制。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>系統需要 evidence package 的訊號是同一段事故證據在交班、release gate 或復盤時反覆被重新查證。常見例子是只保存截圖，下一班 on-call 看得到圖表形狀，卻缺少 query、time range、sampling ratio、ingest delay 與 owner，導致決策背景需要重新建立。</p>
<h2 id="設計責任">設計責任</h2>
<p>Evidence package 要包含 source、time range、query link、owner、data quality、confidence、known gap 與 retention。它的責任是讓證據可查、可解釋、可重跑，並能交給 <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/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</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></channel></rss>