<?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>Runbook on Tarragon</title><link>https://tarrragon.github.io/blog/tags/runbook/</link><description>Recent content in Runbook on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Thu, 11 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/runbook/index.xml" rel="self" type="application/rss+xml"/><item><title>10.2 服務拆分執行 Runbook（Strangler Fig / 雙寫期 / 切流 / 回退）</title><link>https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-execution-runbook/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-execution-runbook/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分與邊界判讀&lt;/a> 處理「該不該拆」、本章處理「決定拆之後實際怎麼動手」。拆服務是漸進演進的過程、一次性大爆炸（big bang）的成功率極低。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/strangler-fig/" data-link-title="Strangler Fig Pattern" data-link-desc="服務拆分 / 系統替換的漸進演進模式、用『新舊共存 &amp;#43; 逐步遷移 &amp;#43; 最終下架』取代 big bang 重寫">Strangler Fig pattern&lt;/a> 是這層的工程基底 — 用「新功能在新服務、舊功能慢慢搬」的方式、把整個 monolith 包圍、逐步替換。&lt;/p>
&lt;h2 id="strangler-fig-pattern-的工程含義">Strangler Fig Pattern 的工程含義&lt;/h2>
&lt;p>Strangler Fig（絞殺榕）是 Martin Fowler 對漸進拆分的命名比喻：榕樹依附在宿主樹上、慢慢長大、最終取代宿主。應用到服務拆分：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>舊系統繼續運作&lt;/strong>：拆分過程中、monolith 仍是 source of truth、新服務從旁長出&lt;/li>
&lt;li>&lt;strong>流量逐步遷移&lt;/strong>：用 routing layer（API gateway、proxy、feature flag）控制哪些 request 走新服務、哪些走舊&lt;/li>
&lt;li>&lt;strong>驗證 → 擴大&lt;/strong>：每個遷移的功能先小流量驗證、確認新舊一致後再加流量比例&lt;/li>
&lt;li>&lt;strong>舊系統最終下架&lt;/strong>：當所有功能都遷出後、monolith 才被退役&lt;/li>
&lt;/ul>
&lt;p>Strangler Fig 跟 big bang 拆分的本質差異是「失敗代價可控」— 大爆炸拆分失敗就整個服務掛、Strangler 拆分失敗只影響該功能、且可即時切回 monolith。&lt;/p>
&lt;h2 id="拆分執行階段">拆分執行階段&lt;/h2>
&lt;p>把 Strangler 細化成可操作的四階段：&lt;/p>
&lt;h3 id="階段-1邊界冷凍--adapter-抽出">階段 1：邊界冷凍 + Adapter 抽出&lt;/h3>
&lt;p>動手拆之前、先在 monolith 內部把「將要拆出去」的功能用 adapter / interface 封起來。所有外部呼叫該功能都走 adapter、不直接呼叫實作。&lt;/p>
&lt;p>這層動作的責任：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>強制 dependency 清楚&lt;/strong>：哪些功能依賴它、哪些功能被它依賴、必須變成顯式 interface 而非分散在 codebase&lt;/li>
&lt;li>&lt;strong>資料邊界明示&lt;/strong>：該功能用到哪些 table / column、用 repository / DAO 封裝、不讓其他功能直接 access&lt;/li>
&lt;li>&lt;strong>變更頻率冷凍&lt;/strong>：拆分期間原則上不接受該功能的新需求、避免「拆到一半新需求又進來」&lt;/li>
&lt;/ul>
&lt;p>階段 1 在 monolith 內完成、不動部署、不動資料。完成後、拆分的「邊界」已經在 codebase 顯現、是 prerequisite。&lt;/p>
&lt;h3 id="階段-2新服務--雙寫期">階段 2：新服務 + 雙寫期&lt;/h3>
&lt;p>新服務 spin up、實作 adapter 同樣的介面。寫入路徑進入「雙寫期」：所有寫入同時寫 monolith 跟新服務、讀取仍從 monolith 取。&lt;/p>
&lt;p>雙寫期的設計關鍵：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>寫入順序&lt;/strong>：先寫 monolith 還是先寫新服務？通常先寫 monolith（保持 source of truth 一致性）、新服務寫失敗時記 error 但不影響業務&lt;/li>
&lt;li>&lt;strong>跨服務一致性&lt;/strong>：兩邊寫入用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox pattern&lt;/a> 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/saga/" data-link-title="Saga" data-link-desc="處理跨服務分散事務的補償型 transaction 序列、用最終一致換 ACID atomic">saga&lt;/a> 保證最終一致、不能容忍長期不一致&lt;/li>
&lt;li>&lt;strong>資料對賬機制&lt;/strong>：每天 / 每小時跑對賬 job、找出兩邊不一致的 row、修正 + 統計差異率&lt;/li>
&lt;li>&lt;strong>雙寫期長度&lt;/strong>：通常 1-4 週、視差異率收斂速度決定。差異率穩定在 0.01% 以下、可進階段 3&lt;/li>
&lt;/ul>
&lt;p>雙寫期的失敗訊號：差異率持續高於 1%、代表資料模型對應有 gap、不該進切流階段。&lt;/p>
&lt;h3 id="階段-3切流讀路徑遷移">階段 3：切流（讀路徑遷移）&lt;/h3>
&lt;p>雙寫期穩定後、讀路徑開始從 monolith 切到新服務。切流策略選擇：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>按 user / tenant ID hash 分流&lt;/strong>：取 user_id mod 100、x% 走新服務、其餘走 monolith。漸進 ramp up（1% → 5% → 25% → 100%）&lt;/li>
&lt;li>&lt;strong>按 endpoint 分流&lt;/strong>：read endpoint A 全切、endpoint B 跟 C 還在 monolith。適合「不同 endpoint 風險不同」的場景&lt;/li>
&lt;li>&lt;strong>Dark launch&lt;/strong>：每個 request 同時打兩邊、用 monolith 結果回應、log 兩邊差異。是 shadow read、不是真實切流、但能在切流前找出 edge case&lt;/li>
&lt;/ul>
&lt;p>切流期間的觀測重點：&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分與邊界判讀</a> 處理「該不該拆」、本章處理「決定拆之後實際怎麼動手」。拆服務是漸進演進的過程、一次性大爆炸（big bang）的成功率極低。<a href="/blog/backend/knowledge-cards/strangler-fig/" data-link-title="Strangler Fig Pattern" data-link-desc="服務拆分 / 系統替換的漸進演進模式、用『新舊共存 &#43; 逐步遷移 &#43; 最終下架』取代 big bang 重寫">Strangler Fig pattern</a> 是這層的工程基底 — 用「新功能在新服務、舊功能慢慢搬」的方式、把整個 monolith 包圍、逐步替換。</p>
<h2 id="strangler-fig-pattern-的工程含義">Strangler Fig Pattern 的工程含義</h2>
<p>Strangler Fig（絞殺榕）是 Martin Fowler 對漸進拆分的命名比喻：榕樹依附在宿主樹上、慢慢長大、最終取代宿主。應用到服務拆分：</p>
<ul>
<li><strong>舊系統繼續運作</strong>：拆分過程中、monolith 仍是 source of truth、新服務從旁長出</li>
<li><strong>流量逐步遷移</strong>：用 routing layer（API gateway、proxy、feature flag）控制哪些 request 走新服務、哪些走舊</li>
<li><strong>驗證 → 擴大</strong>：每個遷移的功能先小流量驗證、確認新舊一致後再加流量比例</li>
<li><strong>舊系統最終下架</strong>：當所有功能都遷出後、monolith 才被退役</li>
</ul>
<p>Strangler Fig 跟 big bang 拆分的本質差異是「失敗代價可控」— 大爆炸拆分失敗就整個服務掛、Strangler 拆分失敗只影響該功能、且可即時切回 monolith。</p>
<h2 id="拆分執行階段">拆分執行階段</h2>
<p>把 Strangler 細化成可操作的四階段：</p>
<h3 id="階段-1邊界冷凍--adapter-抽出">階段 1：邊界冷凍 + Adapter 抽出</h3>
<p>動手拆之前、先在 monolith 內部把「將要拆出去」的功能用 adapter / interface 封起來。所有外部呼叫該功能都走 adapter、不直接呼叫實作。</p>
<p>這層動作的責任：</p>
<ul>
<li><strong>強制 dependency 清楚</strong>：哪些功能依賴它、哪些功能被它依賴、必須變成顯式 interface 而非分散在 codebase</li>
<li><strong>資料邊界明示</strong>：該功能用到哪些 table / column、用 repository / DAO 封裝、不讓其他功能直接 access</li>
<li><strong>變更頻率冷凍</strong>：拆分期間原則上不接受該功能的新需求、避免「拆到一半新需求又進來」</li>
</ul>
<p>階段 1 在 monolith 內完成、不動部署、不動資料。完成後、拆分的「邊界」已經在 codebase 顯現、是 prerequisite。</p>
<h3 id="階段-2新服務--雙寫期">階段 2：新服務 + 雙寫期</h3>
<p>新服務 spin up、實作 adapter 同樣的介面。寫入路徑進入「雙寫期」：所有寫入同時寫 monolith 跟新服務、讀取仍從 monolith 取。</p>
<p>雙寫期的設計關鍵：</p>
<ul>
<li><strong>寫入順序</strong>：先寫 monolith 還是先寫新服務？通常先寫 monolith（保持 source of truth 一致性）、新服務寫失敗時記 error 但不影響業務</li>
<li><strong>跨服務一致性</strong>：兩邊寫入用 <a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox pattern</a> 或 <a href="/blog/backend/knowledge-cards/saga/" data-link-title="Saga" data-link-desc="處理跨服務分散事務的補償型 transaction 序列、用最終一致換 ACID atomic">saga</a> 保證最終一致、不能容忍長期不一致</li>
<li><strong>資料對賬機制</strong>：每天 / 每小時跑對賬 job、找出兩邊不一致的 row、修正 + 統計差異率</li>
<li><strong>雙寫期長度</strong>：通常 1-4 週、視差異率收斂速度決定。差異率穩定在 0.01% 以下、可進階段 3</li>
</ul>
<p>雙寫期的失敗訊號：差異率持續高於 1%、代表資料模型對應有 gap、不該進切流階段。</p>
<h3 id="階段-3切流讀路徑遷移">階段 3：切流（讀路徑遷移）</h3>
<p>雙寫期穩定後、讀路徑開始從 monolith 切到新服務。切流策略選擇：</p>
<ul>
<li><strong>按 user / tenant ID hash 分流</strong>：取 user_id mod 100、x% 走新服務、其餘走 monolith。漸進 ramp up（1% → 5% → 25% → 100%）</li>
<li><strong>按 endpoint 分流</strong>：read endpoint A 全切、endpoint B 跟 C 還在 monolith。適合「不同 endpoint 風險不同」的場景</li>
<li><strong>Dark launch</strong>：每個 request 同時打兩邊、用 monolith 結果回應、log 兩邊差異。是 shadow read、不是真實切流、但能在切流前找出 edge case</li>
</ul>
<p>切流期間的觀測重點：</p>
<ul>
<li><strong>錯誤率對比</strong>：新服務 vs monolith 同 endpoint 的 5xx / 4xx 比例</li>
<li><strong>延遲分布對比</strong>：P50 / P95 / P99 latency</li>
<li><strong>業務指標對比</strong>：轉換率、跳出率、訂單成功率 — 確認沒有「技術指標看起來正常、業務指標掉」的隱形 regression</li>
</ul>
<p>任一指標惡化、切回 monolith、不繼續推進。</p>
<h3 id="階段-4寫路徑遷移--monolith-退役">階段 4：寫路徑遷移 + Monolith 退役</h3>
<p>讀路徑 100% 切完、且穩定觀察一段時間後（建議至少 2 週）、寫路徑才從「雙寫」變成「只寫新服務」。</p>
<p>寫路徑切換的步驟：</p>
<ol>
<li><strong>雙寫變成「新服務 + 異步 backfill 到 monolith」</strong>：以新服務為主、monolith 變成 standby</li>
<li><strong>觀察期 1-2 週</strong>：確認新服務寫入路徑穩定、無資料遺失或不一致</li>
<li><strong>停止 backfill</strong>：monolith 不再被寫入、變成 read-only</li>
<li><strong>Monolith 該功能下架</strong>：等確認所有 dependency 都已遷移後（通常還要再 1-4 週觀察）、刪掉 monolith 對應 code 跟 table</li>
</ol>
<p>階段 4 是 point of no return — 過了寫路徑切換、回 monolith 的成本變得很高（要把新服務累積的寫入 backfill 回去）。這個 checkpoint 必須有明確的 go/no-go 決策、不是「順勢推進」。</p>
<h2 id="回退路徑設計">回退路徑設計</h2>
<p>回退條件必須在拆分啟動前就定義、不是事故時臨時決策。常見回退路徑：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>失敗訊號</th>
          <th>回退動作</th>
          <th>成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>Adapter 抽出後 monolith 變慢 / 出錯</td>
          <td>revert PR、重新規劃 adapter 邊界</td>
          <td>低</td>
      </tr>
      <tr>
          <td>2</td>
          <td>雙寫期差異率 &gt; 1% 持續</td>
          <td>停雙寫、回 monolith 單寫、修資料模型對應</td>
          <td>中</td>
      </tr>
      <tr>
          <td>3</td>
          <td>切流期間錯誤率 / 延遲 / 業務指標惡化</td>
          <td>切流比例調回 0%、回 monolith 單讀、雙寫繼續</td>
          <td>中</td>
      </tr>
      <tr>
          <td>4</td>
          <td>寫路徑切換後 1 週內出資料遺失</td>
          <td>觸發 backfill from 新服務 → monolith、切回雙寫期</td>
          <td>高</td>
      </tr>
      <tr>
          <td>4+</td>
          <td>Monolith 已下架、新服務出事</td>
          <td>災難級別、需要從備份重建 + 大規模事件公告</td>
          <td>極高</td>
      </tr>
  </tbody>
</table>
<p>階段 4 之後的回退代價是指數成長的。設計時要把 monolith 下架時點延後到「確信不需要回退」、寧可多保留 monolith 1-2 個月。</p>
<h2 id="拆分執行的判讀訊號">拆分執行的判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Adapter 抽出時發現難以封裝（dependency 散落各處）</td>
          <td>邊界其實沒形成、拆分判斷錯了</td>
          <td>回 10.1 重新評估、考慮先重構 monolith 再拆</td>
      </tr>
      <tr>
          <td>雙寫期差異率不收斂</td>
          <td>資料模型對應有 gap、或業務邏輯有 monolith 隱式依賴</td>
          <td>暫停拆分、做 data audit、找出隱式依賴點</td>
      </tr>
      <tr>
          <td>切流比例增加後業務指標掉</td>
          <td>技術等價但業務行為不等價（例如 latency 微升影響轉換）</td>
          <td>切回 monolith、檢查 latency / 業務指標關聯</td>
      </tr>
      <tr>
          <td>階段 4 出現「monolith 還有人在用」</td>
          <td>dependency 沒清乾淨、有隱藏的呼叫者</td>
          <td>延後 monolith 下架、用 access log audit 找出殘留呼叫者</td>
      </tr>
      <tr>
          <td>拆分過程中 dev velocity 大幅下降</td>
          <td>拆分成本超過短期收益、可能拆錯時機</td>
          <td>評估暫停拆分、回到 modular monolith</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把拆分當成「直接把功能搬出去」、跳過階段 1 adapter 抽出。沒有 adapter 抽出、新服務跟 monolith 的 dependency 邊界不清楚、雙寫期會出現難以排查的隱式依賴問題。</p>
<p>把雙寫期當成「過渡而已、隨便寫」。雙寫期是拆分的 source of truth verification 階段、差異率沒收斂前不能進切流。隨便寫的結果是切流後出資料一致性事故。</p>
<p>把「monolith 下架」當成拆分成功訊號。Monolith 下架太早是常見事故來源 — 即使流量 100% 切完、可能仍有 batch job / report / 內部 tool 在用 monolith。下架前先用 access log audit 確認真實流量為 0。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>本章專注「Strangler Fig 漸進拆分的執行流程」。當問題進入「該不該拆」的判讀、回 <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分與邊界判讀</a>；進入跨服務通訊設計（同步 vs 異步、event-driven）、進 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 message queue</a>；進入部署層的切流機制（feature flag、canary、blue/green）、進 <a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.8 deployment rollout</a>；進入資料庫遷移層的具體技術（dual write、shadow read、cutover），進 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作</a>。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>09 / 05 案例庫中、Strangler 拆分案例不算多（多數案例是已拆完的狀態描述、而非拆分過程紀錄）。可用以下案例反向追問：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix Aurora consolidation</a> — Netflix 的故事是「拆完合回去」、隱含 strangler 反向。對照本章可問：合併過程是否也走了類似四階段、只是方向相反（雙寫期把多 DB 合到 Aurora、再切讀路徑、最後下架原 DB）？</li>
<li><a href="/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2 Condé Nast：EKS 平台整併</a> — 平台層整併。本章在「服務層」、整併在「平台層」、邏輯類似但 surface 不同。</li>
</ul>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分判讀</a> 的交接：10.1 給「該拆」的判讀、本章給「怎麼拆」的執行。</li>
<li>與 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 message queue + outbox</a> 的交接：雙寫期跟拆分後跨服務通訊都依賴 outbox / saga 保證一致性。</li>
<li>與 <a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.8 deployment rollout</a> 的交接：階段 3 切流的技術機制（feature flag、canary）跟部署層的 rollout 同源。</li>
<li>與 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作</a> 的交接：階段 2 雙寫期跟資料庫遷移的雙寫期是同一套機制、只是 surface 不同。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看拆分判讀（該不該拆）、回 <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分與邊界判讀</a>。要看拆分後跨服務通訊設計、進 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 模組訊息佇列</a>。要看部署層的切流技術細節、進 <a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.8 Deployment Rollout</a>。</p>
]]></content:encoded></item><item><title>10.3 託管形態遷出：資產線盤點與並行期執行</title><link>https://tarrragon.github.io/blog/backend/10-system-evolution/managed-platform-exit/</link><pubDate>Thu, 11 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/10-system-evolution/managed-platform-exit/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型&lt;/a> 的升級自建 tripwire 回答「何時該重新評估」、評估成立後、本章接手回答「按下遷出鍵之後的工程」。讀者情境：產品跑在 Wix / Shopify / Firebase / WordPress 這類託管形態上、tripwire 已命中、目標是自建或半託管。遷出的核心原則是把「搬家」拆成多條資產線各自的受控 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration&lt;/a>：資料、身分、流量、整合的可攜性差異極大、斷點位置不同、可以分開 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cutover-window/" data-link-title="Cutover Window" data-link-desc="說明正式切換發生的觀察窗口、停止條件與回退判讀範圍">cutover&lt;/a> — 把它們綁成同一天的大爆炸切換（big bang cutover）、等於把可攜性最差的那條線的風險強加給其他所有線。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">Vendor lock-in&lt;/a> 在遷出日的具體形狀就是這幾條資產線的斷點。0.21 的可遷出保險清單（自有網域、資料定期匯出、客戶聯絡管道自有、金流可攜性、密碼不可攜的預案、業務邏輯文件化）是進場時買的保險、本章是理賠流程 — 保險有買、每條線的斷點都有現成出口；保險沒買、本章每一節都會多一段「先補保險再動手」的前置工。&lt;/p>
&lt;h2 id="資產線盤點">資產線盤點&lt;/h2>
&lt;p>動手前先盤點：這個產品在平台上累積了哪些資產、每項資產走哪條線、可攜性如何。盤點的產出是一張「資產 → 線 → 出口 → 斷點」對照、它決定後面所有階段的順序與凍結窗口長度。&lt;/p>
&lt;h3 id="資料線">資料線&lt;/h3>
&lt;p>資料線問兩個問題：拿得出來嗎、拿出來之後能直接用嗎。多數平台對第一個問題的答案是肯定的 — Shopify 的商品與訂單歷史有官方 CSV / API 匯出、WordPress 的文章與媒體是最成熟的匯出路徑、Firebase 的 Firestore 有官方 export。真正的工程量在第二個問題：匯出格式是平台資料模型的快照、不是自建系統的 schema。&lt;/p>
&lt;p>兩個典型情境。第一、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/baas/" data-link-title="BaaS（Backend as a Service）" data-link-desc="說明把認證、資料庫、檔案儲存、推播打包成現成模組、由前端 SDK 直連的後端交付形態">BaaS&lt;/a> 的反正規化結構：Firestore 的文件沿查詢需求生長、同一份事實散在多個 collection、而目標端的關聯式 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database&lt;/a> 要求單一事實單一位置 — 執行順序是先設計目標 schema、再寫轉換管線、而不是把 export 原樣灌進去。第二、半託管 CMS 的外掛私有表：WordPress 官方匯出涵蓋文章與媒體、外掛各自的私有表（會員等級、預約規則、客製欄位）在匯出範圍之外 — 每個外掛要單獨確認資料位置與匯出手段。盤點階段把這兩類「拿得出來但不能直接用」的資產標出來、它們是資料線時程的主要變數。&lt;/p>
&lt;p>歷史資料搬完之後、增量是另一個問題：平台在並行期仍持續產生新訂單、新會員、新內容、需要一條增量同步管道（webhook、API 輪詢、排程匯出）把變更餵進新系統 — 角色等同自建世界的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">change data capture&lt;/a>、只是來源是平台 API 而不是資料庫 log。&lt;/p>
&lt;p>資料線還有一類「可放棄、但要快照」的資產：平台內建報表與分析歷史。這類數據多數沒有匯出路徑、平台降級或關站後即消失 — 歷史明細可以放棄、但 cutover 後的健康判讀需要遷移前的基線（自然流量、轉換率、客單價）。盤點階段把基線指標匯出存檔、觀察期的「下滑超過預估」才有對照對象。&lt;/p>
&lt;h3 id="身分線">身分線&lt;/h3>
&lt;p>身分線的可攜性在所有資產線中分布最極端。會員的 email 與基本資料幾乎都可匯出；密碼雜湊多數平台拒絕交出 — Firebase Auth 是少數友善案例、官方工具可匯出密碼雜湊、演算法參數從主控台另行取得、自建認證系統照參數驗證即可無感銜接。多數平台（電商會員、網站會員系統）把雜湊留在自己手上、這條線的執行形態於是變成全體重設密碼。&lt;/p>
&lt;p>重設密碼遷移要當成產品功能設計、而不是遷移日的告示：分批寄送重設邀請、首次登入時引導重設、保留舊 email 驗證鏈路、把重設高峰排開行銷活動。0.21 可遷出保險裡「密碼不可攜的預案」指的就是這套體驗、執行階段它從預案變成排程上的工作項。&lt;/p>
&lt;p>Session 綁定在平台端、cutover 當天全體使用者重新登入是預設行為、要納入切換日的客服與監控預期。第三方登入（Google / Apple 登入）的識別碼可攜性介於兩者之間：識別碼存在 provider 端、但可能綁定在 OAuth client 或開發者帳號的範圍上 — Apple 的 user identifier 以開發者團隊為界、換團隊後同一使用者拿到不同識別碼。遷移前先用測試帳號驗證新舊系統拿到的識別碼一致、再決定第三方登入使用者要走無感銜接還是重新綁定。&lt;/p>
&lt;p>身分線的盤點對象除了終端使用者、還有操作者與機器：員工帳號、角色權限、API key 與第三方服務的 OAuth 授權都要在新系統重新佈建、並納入 cutover 演練 — 切換日客服登不進新後台、是這條線最常見的自傷事故。&lt;/p>
&lt;h3 id="流量線">流量線&lt;/h3>
&lt;p>流量線的前提是自有網域 — 0.21 可遷出保險清單裡的保險項。網域在自己名下、DNS 自己控制、流量切換就是一次 DNS 變更加一套轉址規則；流量活在平台贈送的子網域上、遷出等於換址、SEO 與既有連結歸零、這條線要先補保險（買網域、在平台上綁定、讓搜尋引擎與外部連結先收斂到自有網域）再談切換。&lt;/p>
&lt;p>執行面的關鍵是斷裂面管理。平台的 URL 結構（&lt;code>/products/handle&lt;/code>、&lt;code>/blogs/news/slug&lt;/code>）跟自建系統的路由幾乎必然不同、而離開平台後、舊 URL 的轉址規則沒有地方住 — 平台停用後它連 404 都不會回、是 DNS 直接指向新系統。所以轉址表（舊 URL 樣式 → 新 URL）要建在新系統自己身上：cutover 後由新系統對舊樣式回 301、搜尋引擎與外部連結沿轉址收斂。配套動作：cutover 前把 DNS TTL 調低、cutover 後重交 sitemap、用搜尋主控台觀察索引替換進度。SEO 累積是按月計的資產、轉址表缺漏的代價以自然流量下滑直接體現。&lt;/p>
&lt;h3 id="整合線">整合線&lt;/h3>
&lt;p>整合線收所有由平台出面跟外部世界握手的合約、其中金流是最硬的斷點 — 它在本章盤點順序排最後、執行確認要排最早、答案會改變整場遷移的形狀。一次性收款的遷移成本低 — 換金流串接、新訂單走新管道。訂閱制是另一回事：扣款授權 token 存在金流商的 vault 裡、且常綁定在平台名下的金流帳戶上。遷出時先問金流商「授權能否轉移到商家自有的金流帳戶」— 部分金流商支援處理商之間的卡號資料轉移、談得下來就是一次後台作業；談不下來、全體訂閱者重新授權、流失率直接換算成訂閱營收缺口。執行手段跟重設密碼同構：分批通知、寬限期、必要時用優惠補償授權摩擦。&lt;/p>
&lt;p>金流之外、整合線還包括：平台外掛 / app 生態承擔的業務邏輯（Shopify app 做的折扣規則、WordPress 外掛做的預約流程）要逐個盤點、決定重寫進自建系統、換成獨立 SaaS、或趁機放棄；對外 webhook（ERP、出貨、會計系統）的端點切換要跟對方排時程；行銷 email 的寄送信譽綁在平台的寄件網域上、換到自有寄件網域要重建 SPF / DKIM 並逐步暖機、避免遷移週的通知信全進垃圾箱。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a> 的升級自建 tripwire 回答「何時該重新評估」、評估成立後、本章接手回答「按下遷出鍵之後的工程」。讀者情境：產品跑在 Wix / Shopify / Firebase / WordPress 這類託管形態上、tripwire 已命中、目標是自建或半託管。遷出的核心原則是把「搬家」拆成多條資產線各自的受控 <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration</a>：資料、身分、流量、整合的可攜性差異極大、斷點位置不同、可以分開 <a href="/blog/backend/knowledge-cards/cutover-window/" data-link-title="Cutover Window" data-link-desc="說明正式切換發生的觀察窗口、停止條件與回退判讀範圍">cutover</a> — 把它們綁成同一天的大爆炸切換（big bang cutover）、等於把可攜性最差的那條線的風險強加給其他所有線。</p>
<p><a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">Vendor lock-in</a> 在遷出日的具體形狀就是這幾條資產線的斷點。0.21 的可遷出保險清單（自有網域、資料定期匯出、客戶聯絡管道自有、金流可攜性、密碼不可攜的預案、業務邏輯文件化）是進場時買的保險、本章是理賠流程 — 保險有買、每條線的斷點都有現成出口；保險沒買、本章每一節都會多一段「先補保險再動手」的前置工。</p>
<h2 id="資產線盤點">資產線盤點</h2>
<p>動手前先盤點：這個產品在平台上累積了哪些資產、每項資產走哪條線、可攜性如何。盤點的產出是一張「資產 → 線 → 出口 → 斷點」對照、它決定後面所有階段的順序與凍結窗口長度。</p>
<h3 id="資料線">資料線</h3>
<p>資料線問兩個問題：拿得出來嗎、拿出來之後能直接用嗎。多數平台對第一個問題的答案是肯定的 — Shopify 的商品與訂單歷史有官方 CSV / API 匯出、WordPress 的文章與媒體是最成熟的匯出路徑、Firebase 的 Firestore 有官方 export。真正的工程量在第二個問題：匯出格式是平台資料模型的快照、不是自建系統的 schema。</p>
<p>兩個典型情境。第一、<a href="/blog/backend/knowledge-cards/baas/" data-link-title="BaaS（Backend as a Service）" data-link-desc="說明把認證、資料庫、檔案儲存、推播打包成現成模組、由前端 SDK 直連的後端交付形態">BaaS</a> 的反正規化結構：Firestore 的文件沿查詢需求生長、同一份事實散在多個 collection、而目標端的關聯式 <a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">database</a> 要求單一事實單一位置 — 執行順序是先設計目標 schema、再寫轉換管線、而不是把 export 原樣灌進去。第二、半託管 CMS 的外掛私有表：WordPress 官方匯出涵蓋文章與媒體、外掛各自的私有表（會員等級、預約規則、客製欄位）在匯出範圍之外 — 每個外掛要單獨確認資料位置與匯出手段。盤點階段把這兩類「拿得出來但不能直接用」的資產標出來、它們是資料線時程的主要變數。</p>
<p>歷史資料搬完之後、增量是另一個問題：平台在並行期仍持續產生新訂單、新會員、新內容、需要一條增量同步管道（webhook、API 輪詢、排程匯出）把變更餵進新系統 — 角色等同自建世界的 <a href="/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">change data capture</a>、只是來源是平台 API 而不是資料庫 log。</p>
<p>資料線還有一類「可放棄、但要快照」的資產：平台內建報表與分析歷史。這類數據多數沒有匯出路徑、平台降級或關站後即消失 — 歷史明細可以放棄、但 cutover 後的健康判讀需要遷移前的基線（自然流量、轉換率、客單價）。盤點階段把基線指標匯出存檔、觀察期的「下滑超過預估」才有對照對象。</p>
<h3 id="身分線">身分線</h3>
<p>身分線的可攜性在所有資產線中分布最極端。會員的 email 與基本資料幾乎都可匯出；密碼雜湊多數平台拒絕交出 — Firebase Auth 是少數友善案例、官方工具可匯出密碼雜湊、演算法參數從主控台另行取得、自建認證系統照參數驗證即可無感銜接。多數平台（電商會員、網站會員系統）把雜湊留在自己手上、這條線的執行形態於是變成全體重設密碼。</p>
<p>重設密碼遷移要當成產品功能設計、而不是遷移日的告示：分批寄送重設邀請、首次登入時引導重設、保留舊 email 驗證鏈路、把重設高峰排開行銷活動。0.21 可遷出保險裡「密碼不可攜的預案」指的就是這套體驗、執行階段它從預案變成排程上的工作項。</p>
<p>Session 綁定在平台端、cutover 當天全體使用者重新登入是預設行為、要納入切換日的客服與監控預期。第三方登入（Google / Apple 登入）的識別碼可攜性介於兩者之間：識別碼存在 provider 端、但可能綁定在 OAuth client 或開發者帳號的範圍上 — Apple 的 user identifier 以開發者團隊為界、換團隊後同一使用者拿到不同識別碼。遷移前先用測試帳號驗證新舊系統拿到的識別碼一致、再決定第三方登入使用者要走無感銜接還是重新綁定。</p>
<p>身分線的盤點對象除了終端使用者、還有操作者與機器：員工帳號、角色權限、API key 與第三方服務的 OAuth 授權都要在新系統重新佈建、並納入 cutover 演練 — 切換日客服登不進新後台、是這條線最常見的自傷事故。</p>
<h3 id="流量線">流量線</h3>
<p>流量線的前提是自有網域 — 0.21 可遷出保險清單裡的保險項。網域在自己名下、DNS 自己控制、流量切換就是一次 DNS 變更加一套轉址規則；流量活在平台贈送的子網域上、遷出等於換址、SEO 與既有連結歸零、這條線要先補保險（買網域、在平台上綁定、讓搜尋引擎與外部連結先收斂到自有網域）再談切換。</p>
<p>執行面的關鍵是斷裂面管理。平台的 URL 結構（<code>/products/handle</code>、<code>/blogs/news/slug</code>）跟自建系統的路由幾乎必然不同、而離開平台後、舊 URL 的轉址規則沒有地方住 — 平台停用後它連 404 都不會回、是 DNS 直接指向新系統。所以轉址表（舊 URL 樣式 → 新 URL）要建在新系統自己身上：cutover 後由新系統對舊樣式回 301、搜尋引擎與外部連結沿轉址收斂。配套動作：cutover 前把 DNS TTL 調低、cutover 後重交 sitemap、用搜尋主控台觀察索引替換進度。SEO 累積是按月計的資產、轉址表缺漏的代價以自然流量下滑直接體現。</p>
<h3 id="整合線">整合線</h3>
<p>整合線收所有由平台出面跟外部世界握手的合約、其中金流是最硬的斷點 — 它在本章盤點順序排最後、執行確認要排最早、答案會改變整場遷移的形狀。一次性收款的遷移成本低 — 換金流串接、新訂單走新管道。訂閱制是另一回事：扣款授權 token 存在金流商的 vault 裡、且常綁定在平台名下的金流帳戶上。遷出時先問金流商「授權能否轉移到商家自有的金流帳戶」— 部分金流商支援處理商之間的卡號資料轉移、談得下來就是一次後台作業；談不下來、全體訂閱者重新授權、流失率直接換算成訂閱營收缺口。執行手段跟重設密碼同構：分批通知、寬限期、必要時用優惠補償授權摩擦。</p>
<p>金流之外、整合線還包括：平台外掛 / app 生態承擔的業務邏輯（Shopify app 做的折扣規則、WordPress 外掛做的預約流程）要逐個盤點、決定重寫進自建系統、換成獨立 SaaS、或趁機放棄；對外 webhook（ERP、出貨、會計系統）的端點切換要跟對方排時程；行銷 email 的寄送信譽綁在平台的寄件網域上、換到自有寄件網域要重建 SPF / DKIM 並逐步暖機、避免遷移週的通知信全進垃圾箱。</p>
<h2 id="並行期設計">並行期設計</h2>
<p><a href="/blog/backend/knowledge-cards/parallel-run/" data-link-title="並行期（Parallel Run）" data-link-desc="說明舊系統維持 source of truth、新系統以單向同步加唯讀驗證運轉的遷移共存階段、與雙寫的寫入路徑控制權差異">並行期</a>是舊平台與新系統共存、用真實資料驗證新系統的階段 — 前提是目標系統已依 <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">模組零的選型順序</a> 建置完成、本章不重複選型推導。它跟 <a href="/blog/backend/10-system-evolution/service-decomposition-execution-runbook/" data-link-title="10.2 服務拆分執行 Runbook（Strangler Fig / 雙寫期 / 切流 / 回退）" data-link-desc="10.1 決定該拆之後、實際怎麼動手拆 — Strangler Fig pattern、雙寫期管理、切流策略、回退條件設計">10.2 服務拆分執行 Runbook</a> 的雙寫期同源但形狀不同：服務拆分時、寫入路徑在自己的程式碼裡、可以實作 <a href="/blog/backend/knowledge-cards/dual-write/" data-link-title="Dual Write" data-link-desc="說明同一變更同時寫入兩個系統時的一致性風險">dual write</a>；託管平台的寫入發生在平台內部 — 顧客在 Shopify 結帳、會員在平台註冊 — 自建程式碼插不進那條寫入路徑。所以並行期的形態是「平台維持 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>、單向同步、新系統唯讀驗證」：</p>
<ol>
<li>增量同步管道（webhook / API 輪詢 / 排程匯出）持續把平台變更餵進新系統</li>
<li>新系統以唯讀 replica 的角色運轉、對帳 job 定期比對兩邊的訂單數、會員數、金額總和</li>
<li>內部使用者先在新系統上工作（報表、後台查詢）、用真實業務流量驗證資料轉換的正確性</li>
<li>差異率收斂並穩定後、才排 cutover 日</li>
</ol>
<p>Cutover 本身是一段 <a href="/blog/backend/knowledge-cards/cutover-window/" data-link-title="Cutover Window" data-link-desc="說明正式切換發生的觀察窗口、停止條件與回退判讀範圍">cutover window</a>、不是一個按鈕：選低流量時段、短暫凍結平台側變更（電商常用「暫停結帳維護頁」幾十分鐘）、跑最後一輪增量同步、切 DNS、然後密集觀察訂單成功率、登入成功率、金流授權成功率 — 觀察清單來自資產線盤點、每條線各有自己的健康訊號。</p>
<p>回切窗口的設計決定這場遷移的失敗代價。cutover 後保留舊平台訂閱與設定、回切動作是 DNS 切回；代價是新系統在窗口內產生的交易要補回平台 — 平台側通常沒有批次匯入訂單的好路徑、補回多半是手動作業、所以回切窗口內要刻意壓低不可逆變更的累積速度（例如窗口前 48 小時內暫停大型行銷活動）。這跟 10.2 寫路徑切換的 point of no return 是同一個判讀：回退成本隨時間墊高、go/no-go 要當成有明確時點的決策執行、判定條件在進入窗口前排定。</p>
<p>關舊站走降級、而不是直接刪除。觀察期過後、平台帳號先降到最低方案、店面關閉但後台保留 — 退款處理、客服查歷史訂單、會計與稅務稽核都還會用到平台側資料。刪除帳號前的檢查條件：所有歷史資料已完整落地自有儲存並驗證可讀、法規要求的交易紀錄保存年限已由自有系統接手、最後一筆平台側退款 / 爭議單已結案。</p>
<h2 id="部分遷出是常見的中繼形態">部分遷出是常見的中繼形態</h2>
<p>資產線可以獨立 cutover 的另一面、是遷出可以分期：先撤其中幾條線、其餘留在平台。部分遷出是把遷移風險拆期攤還的標準形態、結構上同 <a href="/blog/backend/knowledge-cards/strangler-fig/" data-link-title="Strangler Fig Pattern" data-link-desc="服務拆分 / 系統替換的漸進演進模式、用『新舊共存 &#43; 逐步遷移 &#43; 最終下架』取代 big bang 重寫">Strangler Fig</a>：新系統從旁長出、逐線取代、平台最後才退役。</p>
<p>常見的中繼形態有四種。資料層先撤：增量同步管道建好之後、自有資料庫先成為報表與分析的 source、前台與結帳留在平台 — 0.21 BaaS 段描述的跨集合報表困境、在這個形態下已經解掉、而最高風險的金流與流量線還沒動。前台先撤（headless）：自建前端體驗層、平台降級為後端引擎（結帳 API、內容 API）— 流量線與 SEO 控制權先回手、金流與資料留在平台的成熟路徑上。身分後撤：認證是使用者感知最強的線、Firebase Auth 這類可攜性好的元件常被留到最後 — 資料與流量都搬完、產品穩定後、再做密碼雜湊匯入或重設遷移。金流後撤（或長期留平台）：訂閱授權轉移談不下來時、資料、前台與流量都遷出、訂閱扣款續走平台帳戶 — 它跟前三種不同、可能由中繼轉成長期形態、去留判讀回整合線的金流斷點確認。</p>
<p>中繼形態的判讀標準是「每個階段結束時、撤出的那條線已經完整脫離平台、由新系統持有唯一事實」。模糊狀態（一半訂單在平台、一半在自建、靠人腦記得哪邊查）是部分遷出最常見的事故源 — 每條線在任一時刻都要有唯一的 source of truth。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>盤點時發現業務邏輯只存在平台 UI 設定裡</td>
          <td>0.21 可遷出保險「業務邏輯文件化」缺項</td>
          <td>先文件化再動手、規則重建期計入時程</td>
      </tr>
      <tr>
          <td>並行期對帳差異率不收斂</td>
          <td>資料轉換有 gap、或增量同步管道漏事件</td>
          <td>暫停 cutover 排程、audit 轉換管線與 webhook</td>
      </tr>
      <tr>
          <td>金流商拒絕授權轉移</td>
          <td>訂閱線變成全體重新授權、流失進入營收預估</td>
          <td>重算遷移 ROI、評估訂閱線單獨延後</td>
      </tr>
      <tr>
          <td>Cutover 後自然流量持續下滑超過觀察期預估</td>
          <td>轉址表缺漏、或索引替換異常</td>
          <td>比對搜尋主控台的 404 清單、補轉址規則</td>
      </tr>
      <tr>
          <td>回切窗口內手動補單量超出客服消化能力</td>
          <td>不可逆變更累積速度超過回切設計</td>
          <td>縮短決策週期、提前 go/no-go 判定</td>
      </tr>
      <tr>
          <td>並行期超過原定窗口仍未排 cutover</td>
          <td>並行不是穩態、雙系統維運與平台月費在吃遷移 ROI</td>
          <td>重訂 cutover 條件、或承認部分遷出為長期形態</td>
      </tr>
      <tr>
          <td>新舊系統各管一部分同類資料超過一個階段</td>
          <td>部分遷出停在模糊狀態、source of truth 分裂</td>
          <td>強制收斂該資產線、明確指定唯一 source of truth</td>
      </tr>
  </tbody>
</table>
<p>業務邏輯那一列值得展開：平台設定裡長出來的折扣邏輯、會員等級、運費規則、是盤點階段最容易漏的資產 — 它們沒有檔案形態、不會出現在任何匯出工具裡。0.21 可遷出保險清單把「業務邏輯文件化」列為進場保險、沒買這項保險的遷移、第一個階段是考古：對著平台後台逐頁截圖、把規則寫成文件、再評估哪些重寫、哪些放棄。</p>
<p>金流那一列是整場遷移裡少數「工程努力無法繞過」的斷點 — 授權轉移的決定權在金流商與平台的合約上、不在工程團隊手上。所以它在盤點階段就要最先確認：答案直接改變遷移的營收影響模型、甚至可能讓「訂閱線留在平台、其餘遷出」成為長期形態。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>本章專注「託管形態 → 自建 / 半託管」的遷出執行。當問題回到「該不該遷、何時該重新評估」、回 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a> 的升級自建 tripwire 表；遷移目標的自建選型（資料庫、部署、金流接法）走 <a href="/blog/backend/00-service-selection/" data-link-title="模組零：後端服務選型" data-link-desc="從需求類型判斷資料庫、快取、訊息佇列、觀測與部署平台的選型方向">模組零的選型順序</a>；自建系統之間的資料庫搬遷技術細節（雙寫、shadow read、切換）進 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作</a>；服務層的漸進替換紀律進 <a href="/blog/backend/10-system-evolution/service-decomposition-execution-runbook/" data-link-title="10.2 服務拆分執行 Runbook（Strangler Fig / 雙寫期 / 切流 / 回退）" data-link-desc="10.1 決定該拆之後、實際怎麼動手拆 — Strangler Fig pattern、雙寫期管理、切流策略、回退條件設計">10.2 服務拆分執行 Runbook</a>。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>要回頭確認遷移時機與保險、見 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a>。要看並行期同源的雙寫與切流紀律、見 <a href="/blog/backend/10-system-evolution/service-decomposition-execution-runbook/" data-link-title="10.2 服務拆分執行 Runbook（Strangler Fig / 雙寫期 / 切流 / 回退）" data-link-desc="10.1 決定該拆之後、實際怎麼動手拆 — Strangler Fig pattern、雙寫期管理、切流策略、回退條件設計">10.2 服務拆分執行 Runbook</a>。遷入自建後的第一站、從 <a href="/blog/backend/00-service-selection/backend-demand-taxonomy/" data-link-title="0.0 後端需求分類地圖" data-link-desc="先從需求形狀辨識狀態、讀取、非同步、即時、診斷、交付與可靠性問題">0.0 後端需求分類地圖</a> 開始走選型順序。</p>
]]></content:encoded></item><item><title>SQLite Observability and Runbook</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/observability-runbook/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/observability-runbook/</guid><description>&lt;p>SQLite observability and runbook 的核心責任是把低操作成本服務補成可交接的 production evidence。SQLite 的元件少，但正式服務仍需要觀測 busy errors、WAL growth、backup freshness、restore drill、disk usage、migration result、file permission 與 application-level query health。&lt;/p>
&lt;p>本文的判讀錨點是：SQLite 的 observability 要貼近 file、process、filesystem 與 application。它通常沒有 server DB 那種長駐監控平面，因此 runbook 要把 signal 從 app metrics、log、scheduled job、file metadata 與 restore evidence 裡組出來。&lt;/p>
&lt;h2 id="signal-inventory">Signal Inventory&lt;/h2>
&lt;p>Signal inventory 的核心責任是列出 SQLite production 化後最能預告事故的訊號。這些訊號要放進 dashboard、log search 或 scheduled report，讓事故前後都能直接查。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Signal&lt;/th>
 &lt;th>來源&lt;/th>
 &lt;th>代表風險&lt;/th>
 &lt;th>建議反應&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>SQLITE_BUSY&lt;/code> count&lt;/td>
 &lt;td>app log / metric&lt;/td>
 &lt;td>writer contention、long reader&lt;/td>
 &lt;td>查 transaction duration、busy timeout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>WAL file size&lt;/td>
 &lt;td>filesystem metric&lt;/td>
 &lt;td>checkpoint lag、long reader&lt;/td>
 &lt;td>查 checkpoint result、reader age&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backup age&lt;/td>
 &lt;td>scheduled job metric&lt;/td>
 &lt;td>RPO 擴大&lt;/td>
 &lt;td>重跑 backup、檢查 storage&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Restore drill age&lt;/td>
 &lt;td>release evidence&lt;/td>
 &lt;td>RTO 信心下降&lt;/td>
 &lt;td>排程 restore drill&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Disk free&lt;/td>
 &lt;td>host / platform metric&lt;/td>
 &lt;td>write failure、checkpoint failure&lt;/td>
 &lt;td>清理、擴容、降級寫入&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Migration version&lt;/td>
 &lt;td>app startup / metadata&lt;/td>
 &lt;td>schema drift&lt;/td>
 &lt;td>block release、跑 validation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Integrity check result&lt;/td>
 &lt;td>maintenance job&lt;/td>
 &lt;td>corruption / storage issue&lt;/td>
 &lt;td>進入 restore decision&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>SQLITE_BUSY&lt;/code> 是 writer boundary 的最直接訊號。它可能代表長交易、read cursor 未關、parallel test 共用 DB、checkpoint 壓力或 write burst；runbook 要先查 query duration 與 transaction boundary，再調 busy timeout。&lt;/p>
&lt;p>WAL size 是 checkpoint 與 reader 壓力的綜合訊號。WAL 持續成長時，先確認是否有長 reader、backup process、未完成 transaction 或 checkpoint 失敗；接著才考慮手動 checkpoint。&lt;/p>
&lt;p>Backup age 是 RPO 的可觀測版本。若目標 RPO 是 5 分鐘，dashboard 就要顯示 last successful backup / replica time 與警戒線。&lt;/p>
&lt;h2 id="backup-evidence">Backup Evidence&lt;/h2>
&lt;p>Backup evidence 的核心責任是證明資料可被拿回來。SQLite backup 的完成標準包含成功建立備份、保存 sidecar 語意、恢復到新路徑、通過 integrity check、跑 application smoke test。&lt;/p></description><content:encoded><![CDATA[<p>SQLite observability and runbook 的核心責任是把低操作成本服務補成可交接的 production evidence。SQLite 的元件少，但正式服務仍需要觀測 busy errors、WAL growth、backup freshness、restore drill、disk usage、migration result、file permission 與 application-level query health。</p>
<p>本文的判讀錨點是：SQLite 的 observability 要貼近 file、process、filesystem 與 application。它通常沒有 server DB 那種長駐監控平面，因此 runbook 要把 signal 從 app metrics、log、scheduled job、file metadata 與 restore evidence 裡組出來。</p>
<h2 id="signal-inventory">Signal Inventory</h2>
<p>Signal inventory 的核心責任是列出 SQLite production 化後最能預告事故的訊號。這些訊號要放進 dashboard、log search 或 scheduled report，讓事故前後都能直接查。</p>
<table>
  <thead>
      <tr>
          <th>Signal</th>
          <th>來源</th>
          <th>代表風險</th>
          <th>建議反應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>SQLITE_BUSY</code> count</td>
          <td>app log / metric</td>
          <td>writer contention、long reader</td>
          <td>查 transaction duration、busy timeout</td>
      </tr>
      <tr>
          <td>WAL file size</td>
          <td>filesystem metric</td>
          <td>checkpoint lag、long reader</td>
          <td>查 checkpoint result、reader age</td>
      </tr>
      <tr>
          <td>Backup age</td>
          <td>scheduled job metric</td>
          <td>RPO 擴大</td>
          <td>重跑 backup、檢查 storage</td>
      </tr>
      <tr>
          <td>Restore drill age</td>
          <td>release evidence</td>
          <td>RTO 信心下降</td>
          <td>排程 restore drill</td>
      </tr>
      <tr>
          <td>Disk free</td>
          <td>host / platform metric</td>
          <td>write failure、checkpoint failure</td>
          <td>清理、擴容、降級寫入</td>
      </tr>
      <tr>
          <td>Migration version</td>
          <td>app startup / metadata</td>
          <td>schema drift</td>
          <td>block release、跑 validation</td>
      </tr>
      <tr>
          <td>Integrity check result</td>
          <td>maintenance job</td>
          <td>corruption / storage issue</td>
          <td>進入 restore decision</td>
      </tr>
  </tbody>
</table>
<p><code>SQLITE_BUSY</code> 是 writer boundary 的最直接訊號。它可能代表長交易、read cursor 未關、parallel test 共用 DB、checkpoint 壓力或 write burst；runbook 要先查 query duration 與 transaction boundary，再調 busy timeout。</p>
<p>WAL size 是 checkpoint 與 reader 壓力的綜合訊號。WAL 持續成長時，先確認是否有長 reader、backup process、未完成 transaction 或 checkpoint 失敗；接著才考慮手動 checkpoint。</p>
<p>Backup age 是 RPO 的可觀測版本。若目標 RPO 是 5 分鐘，dashboard 就要顯示 last successful backup / replica time 與警戒線。</p>
<h2 id="backup-evidence">Backup Evidence</h2>
<p>Backup evidence 的核心責任是證明資料可被拿回來。SQLite backup 的完成標準包含成功建立備份、保存 sidecar 語意、恢復到新路徑、通過 integrity check、跑 application smoke test。</p>
<table>
  <thead>
      <tr>
          <th>Evidence</th>
          <th>最小內容</th>
          <th>失敗時路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Backup job result</td>
          <td>timestamp、duration、file size、target</td>
          <td>重跑 job、檢查 credential / disk</td>
      </tr>
      <tr>
          <td>Restore artifact</td>
          <td>restored path、checksum、row count</td>
          <td>回前一份 backup、檢查 WAL / snapshot</td>
      </tr>
      <tr>
          <td>Integrity result</td>
          <td><code>PRAGMA integrity_check;</code></td>
          <td>停止寫入、進入 corruption triage</td>
      </tr>
      <tr>
          <td>Application smoke test</td>
          <td>啟動、讀核心頁、寫測試資料</td>
          <td>rollback、保留 evidence</td>
      </tr>
      <tr>
          <td>Retention note</td>
          <td>保存天數、刪除策略、legal hold</td>
          <td>更新 data protection policy</td>
      </tr>
  </tbody>
</table>
<p>SQLite 官方 <a href="https://www.sqlite.org/backup.html">backup API</a> 與 CLI <code>.backup</code> 是備份設計的基礎路由。WAL mode 下，直接複製單一 <code>.db</code> 檔容易漏掉 sidecar file 的時序；runbook 應使用 SQLite-aware backup 或經過 checkpoint / stop-the-world 的 snapshot。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sqlite3 app.db <span class="s2">&#34;.backup &#39;backup/app-2026-05-21.db&#39;&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sqlite3 backup/app-2026-05-21.db <span class="s2">&#34;PRAGMA integrity_check;&#34;</span></span></span></code></pre></div><p>這段命令提供最小 restore evidence 的起點。正式演練要把備份檔複製到隔離路徑，使用相同 application version 啟動，跑核心 read/write smoke test，再記錄耗時與失敗條件。</p>
<h2 id="migration-evidence">Migration Evidence</h2>
<p>Migration evidence 的核心責任是讓 SQLite schema change 可回退、可審查、可交接。單檔 DB 在使用者裝置或服務節點上升級時，migration 失敗會直接影響啟動、資料讀取與同步。</p>
<table>
  <thead>
      <tr>
          <th>Evidence</th>
          <th>內容</th>
          <th>Release gate</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema version</td>
          <td><code>PRAGMA user_version</code> 或 migration table</td>
          <td>app startup 比對 expected version</td>
      </tr>
      <tr>
          <td>Pre-migration snapshot</td>
          <td>backup path、size、checksum</td>
          <td>migration 前完成</td>
      </tr>
      <tr>
          <td>Validation query</td>
          <td>row count、FK check、domain invariant</td>
          <td>migration 後立即執行</td>
      </tr>
      <tr>
          <td>Smoke test</td>
          <td>核心 read/write workflow</td>
          <td>app release gate</td>
      </tr>
      <tr>
          <td>Rollback route</td>
          <td>restore snapshot 或 block startup</td>
          <td>migration 失敗時啟動</td>
      </tr>
  </tbody>
</table>
<p>Migration log 要包含版本、耗時、row count、錯誤、validation result 與 rollback decision。若 SQLite file 位於 end-user device，log 還要能被使用者支援流程收集，避免事故只停在「app 開不起來」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">user_version</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">foreign_key_check</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span></span></span></code></pre></div><p>這些 query 是 migration 後的最小 evidence。正式服務要再補 domain-specific invariant，例如「所有 active subscription 都有 owner」、「所有 pending mutation 都有 idempotency key」。</p>
<h2 id="incident-runbook">Incident Runbook</h2>
<p>Incident runbook 的核心責任是把 SQLite 事故分流到正確處置。SQLite 常見事故包含 disk full、busy storm、WAL growth、bad migration、corruption suspicion、backup failure 與 permission error。</p>
<table>
  <thead>
      <tr>
          <th>Incident</th>
          <th>第一個判讀問題</th>
          <th>立即處置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Busy storm</td>
          <td>有長 transaction 或 write burst 嗎</td>
          <td>暫停非必要寫入、查 transaction duration</td>
      </tr>
      <tr>
          <td>Disk full</td>
          <td>DB / WAL / backup 哪個吃掉空間</td>
          <td>停止寫入、清理 backup、擴容</td>
      </tr>
      <tr>
          <td>WAL growth</td>
          <td>checkpoint 被誰阻擋</td>
          <td>查 reader、跑 checkpoint evidence</td>
      </tr>
      <tr>
          <td>Bad migration</td>
          <td>schema version 與 app version 是否一致</td>
          <td>停止 rollout、restore snapshot、保留 failed DB</td>
      </tr>
      <tr>
          <td>Corruption signal</td>
          <td>integrity check 是否失敗</td>
          <td>進入 read-only、restore last good backup</td>
      </tr>
      <tr>
          <td>Backup failure</td>
          <td>credential、network、destination 是否可用</td>
          <td>切換 destination、補跑 restore drill</td>
      </tr>
  </tbody>
</table>
<p>Busy storm 要先保護使用者操作。可以降低 write endpoint、停用背景 job、延長 retry backoff，然後用 log 查最長 transaction 與最多重試的 query。</p>
<p>Disk full 要先停止寫入。SQLite 在 disk full 時可能讓 write / checkpoint / backup 同時失敗；runbook 要保留剩餘空間、DB file、WAL file、backup directory 與 tmp directory 的大小。</p>
<p>Bad migration 要保留 failed artifact。先複製 failed DB 到 evidence path，記錄 schema version、app version、migration id、validation error，再執行 rollback。</p>
<h2 id="dashboard-and-alert-route">Dashboard and Alert Route</h2>
<p>Dashboard and alert route 的核心責任是讓 SQLite 被納入正式服務的可觀測系統。SQLite signal 常來自 application，因此 metric 命名要接近操作問題。</p>
<table>
  <thead>
      <tr>
          <th>Metric name example</th>
          <th>類型</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>sqlite_busy_total</code></td>
          <td>counter</td>
          <td>writer contention</td>
      </tr>
      <tr>
          <td><code>sqlite_query_duration_ms</code></td>
          <td>histogram</td>
          <td>slow query / long transaction</td>
      </tr>
      <tr>
          <td><code>sqlite_wal_size_bytes</code></td>
          <td>gauge</td>
          <td>checkpoint pressure</td>
      </tr>
      <tr>
          <td><code>sqlite_backup_age_seconds</code></td>
          <td>gauge</td>
          <td>RPO evidence</td>
      </tr>
      <tr>
          <td><code>sqlite_restore_drill_age_days</code></td>
          <td>gauge</td>
          <td>RTO confidence</td>
      </tr>
      <tr>
          <td><code>sqlite_disk_free_bytes</code></td>
          <td>gauge</td>
          <td>disk full prevention</td>
      </tr>
      <tr>
          <td><code>sqlite_migration_version</code></td>
          <td>gauge</td>
          <td>schema drift</td>
      </tr>
  </tbody>
</table>
<p>Alert 要連到 runbook，並提供可執行的第一步。每個 alert 至少要有 owner、severity、first query、rollback condition 與 escalation route。</p>
<p>Log schema 要保留 query category，而非只記原始 SQL。正式服務通常應避免把完整 SQL 與 PII 直接寫入 log；可以記 operation name、duration、row count、error code、busy retry count 與 correlation id。</p>
<h2 id="handoff">Handoff</h2>
<p>Handoff 的核心責任是讓下一個維護者知道 SQLite service 的邊界。交接文件要把「誰負責檔案」、「誰負責備份」、「誰能執行 restore」、「何時升級資料庫」寫清楚。</p>
<p>最小 handoff 包含：</p>
<ol>
<li>Database file path、sidecar file policy、journal mode 與 PRAGMA baseline。</li>
<li>Backup command、destination、retention、last restore drill。</li>
<li>Migration command、schema version、rollback route。</li>
<li>Alert list、dashboard link、incident owner。</li>
<li>Known limits：writer concurrency、file size、edge / sync boundary。</li>
<li>Next route：PostgreSQL、D1 / Turso、Litestream / LiteFS 的評估條件。</li>
</ol>
<p>Handoff 的重點是把低操作成本保留下來。SQLite 的好處來自少元件；可交接文件讓少元件不等於少 evidence。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Observability / runbook 完成後，下一步要接到具體演練。Backup 與 restore 讀 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/backup-restore-drill/" data-link-title="SQLite Backup Restore Drill" data-link-desc="SQLite .backup、VACUUM INTO、restore validation、sidecar file handling 與 RPO / RTO note 的操作說明">SQLite backup restore drill</a>；WAL 與 busy 讀 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/wal-busy-reproduction/" data-link-title="SQLite WAL Busy Reproduction" data-link-desc="SQLite long transaction、SQLITE_BUSY、busy_timeout、checkpoint growth 與 writer queue 的操作說明">WAL busy reproduction</a>；正式服務的 evidence 可對齊 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a> 與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</p>
]]></content:encoded></item></channel></rss>