<?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>Outbox on Tarragon</title><link>https://tarrragon.github.io/blog/tags/outbox/</link><description>Recent content in Outbox on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Thu, 23 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/outbox/index.xml" rel="self" type="application/rss+xml"/><item><title>3.3 outbox pattern 與發佈一致性</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/outbox-pattern/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/outbox-pattern/</guid><description>&lt;p>這一章處理 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction&lt;/a> 與訊息發佈之間的一致性問題，後續可以再延伸到 polling、relay 與 failure recovery。&lt;/p>
&lt;p>外部發件箱模式（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox pattern&lt;/a>）的核心責任是讓資料提交與事件發布在失敗時保持可恢復一致。它把重複發布轉成可判讀、可去重、可補償的治理問題。&lt;/p>
&lt;h2 id="基本流程">基本流程&lt;/h2>
&lt;p>transaction outbox 的典型流程是：在同一資料庫交易內，同時寫入業務資料與 outbox 記錄；交易提交後，由 relay worker 讀取 outbox 並發布到 broker；發布成功後標記或刪除 outbox 記錄。&lt;/p>
&lt;p>這個流程把一致性問題從「跨系統兩段提交」改成「單系統交易 + 非同步重送」，讓失敗路徑更可控。&lt;/p>
&lt;h2 id="relay-worker">relay worker&lt;/h2>
&lt;p>relay worker 的責任是穩定發布與可恢復進度。worker 需要具備批次拉取、順序控制、重試策略與停損條件。進度管理要明確，避免重啟後漏發或重複失控。&lt;/p>
&lt;p>當流量上升時，relay 吞吐會成為關鍵瓶頸。穩定做法是分 shard 處理、限制批次大小、對重試與正常發布做通道分流。&lt;/p>
&lt;h2 id="發布失敗與補償">發布失敗與補償&lt;/h2>
&lt;p>發布失敗通常分為暫時性與系統性。暫時性故障走有限重試，系統性故障走隔離與告警。關鍵是保留 outbox 記錄與發布狀態，讓恢復時可重播。&lt;/p>
&lt;p>duplicate publish 在 outbox 模式下屬於預期現象。消費端需要配合 idempotency 機制，確保重複事件不會產生重複業務結果。&lt;/p>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>判讀重點&lt;/th>
 &lt;th>對應動作&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>outbox backlog 持續堆積&lt;/td>
 &lt;td>relay 吞吐不足或下游故障持續&lt;/td>
 &lt;td>擴充 worker、分流重試、啟動降級流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>業務資料已更新但下游狀態延遲明顯&lt;/td>
 &lt;td>發布延遲超出可接受窗口&lt;/td>
 &lt;td>提升 relay 優先級、補告警與可視化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>duplicate consume 比例上升&lt;/td>
 &lt;td>重試與重播增加，去重壓力上升&lt;/td>
 &lt;td>強化 consumer idempotency 與去重儲存&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>relay 重啟後出現漏發&lt;/td>
 &lt;td>進度標記與交易邊界設計不穩&lt;/td>
 &lt;td>收斂進度策略、補恢復測試&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同步交易延遲上升且 outbox 寫入增加&lt;/td>
 &lt;td>outbox 表設計與索引不足&lt;/td>
 &lt;td>調整索引與分表策略、拆分熱路徑&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見誤區">常見誤區&lt;/h2>
&lt;p>把 outbox 當作「一次解決一致性」的銀彈，會忽略消費端冪等與補償責任。outbox 保證的是發布可恢復，不是端到端結果自動正確。&lt;/p>
&lt;p>把 outbox 表當一般業務表無上限累積，也會放大查詢與維護成本。需要定義保留與清理節奏，並確保稽核需求有對應方案。&lt;/p>
&lt;h2 id="self-managed-vs-managed-broker-的長期-tco">Self-managed vs Managed broker 的長期 TCO&lt;/h2>
&lt;p>Broker 選型本質是 long-term TCO 決策、需評估雲端費用 + 工程稅 + 治理負擔三層成本。Self-managed Kafka 的容量規劃 + broker 數量 + 副本因子 + disk + ZooKeeper / KRaft 治理是長期工程 tax、每次擴容是工程專案。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spotify-kafka-to-pubsub-migration-gcp/" data-link-title="9.C9 Spotify：從自管 Kafka 遷移到 GCP Pub/Sub 的事件交付系統" data-link-desc="Spotify 把自管 Kafka 事件系統遷移到 Google Cloud Pub/Sub、避免自管 broker 的容量規劃成本">9.C9 Spotify Kafka → Pub/Sub Migration&lt;/a> — Spotify 從自管 Kafka 遷到 Google Cloud Pub/Sub、動機是 &lt;em>容量規劃的工程成本&lt;/em> 在 sustained growth 下變得不划算、非 Kafka 效能不足。對 7500 萬用戶的事件交付系統、把 broker 容量規劃跟運維負擔卸給 vendor、釋放工程團隊 capacity。&lt;/p>
&lt;p>&lt;strong>TCO 評估的真實成本項&lt;/strong>（9.C9 case 列前 4 項 + 雲端費用、第 5 項屬跨案例綜合）：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Broker 雲端費用&lt;/strong>：明面成本、相對小&lt;/li>
&lt;li>&lt;strong>容量規劃工程&lt;/strong>：每季 partition planning、每年容量擴張專案&lt;/li>
&lt;li>&lt;strong>故障處理人力&lt;/strong>：broker 故障 oncall、ZooKeeper / KRaft 故障診斷&lt;/li>
&lt;li>&lt;strong>升級遷移成本&lt;/strong>：Kafka 每個 major version 升級是專案&lt;/li>
&lt;li>&lt;strong>跨團隊治理&lt;/strong>（從 3.C6 Uber 跨案例補充）：規模化後的 multi-tenant 隔離、quota 管理、observability 建設&lt;/li>
&lt;/ul>
&lt;p>判讀含義：Self-managed Kafka 在中小團隊可能比 Pub/Sub 便宜（雲端費用低）；但規模化後人力成本壓過雲端費用差、managed service 反而划算。對應 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/vmware-kafka-to-msk/" data-link-title="3.C2 VMware Tanzu CloudHealth：Kafka 轉 Amazon MSK" data-link-desc="自管 Kafka 遷移到託管平台時的治理重點。">3.C2 VMware Tanzu Kafka → MSK&lt;/a> 同樣是「自管 → managed」的決策。&lt;/p></description><content:encoded><![CDATA[<p>這一章處理 <a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction</a> 與訊息發佈之間的一致性問題，後續可以再延伸到 polling、relay 與 failure recovery。</p>
<p>外部發件箱模式（<a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox pattern</a>）的核心責任是讓資料提交與事件發布在失敗時保持可恢復一致。它把重複發布轉成可判讀、可去重、可補償的治理問題。</p>
<h2 id="基本流程">基本流程</h2>
<p>transaction outbox 的典型流程是：在同一資料庫交易內，同時寫入業務資料與 outbox 記錄；交易提交後，由 relay worker 讀取 outbox 並發布到 broker；發布成功後標記或刪除 outbox 記錄。</p>
<p>這個流程把一致性問題從「跨系統兩段提交」改成「單系統交易 + 非同步重送」，讓失敗路徑更可控。</p>
<h2 id="relay-worker">relay worker</h2>
<p>relay worker 的責任是穩定發布與可恢復進度。worker 需要具備批次拉取、順序控制、重試策略與停損條件。進度管理要明確，避免重啟後漏發或重複失控。</p>
<p>當流量上升時，relay 吞吐會成為關鍵瓶頸。穩定做法是分 shard 處理、限制批次大小、對重試與正常發布做通道分流。</p>
<h2 id="發布失敗與補償">發布失敗與補償</h2>
<p>發布失敗通常分為暫時性與系統性。暫時性故障走有限重試，系統性故障走隔離與告警。關鍵是保留 outbox 記錄與發布狀態，讓恢復時可重播。</p>
<p>duplicate publish 在 outbox 模式下屬於預期現象。消費端需要配合 idempotency 機制，確保重複事件不會產生重複業務結果。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>outbox backlog 持續堆積</td>
          <td>relay 吞吐不足或下游故障持續</td>
          <td>擴充 worker、分流重試、啟動降級流程</td>
      </tr>
      <tr>
          <td>業務資料已更新但下游狀態延遲明顯</td>
          <td>發布延遲超出可接受窗口</td>
          <td>提升 relay 優先級、補告警與可視化</td>
      </tr>
      <tr>
          <td>duplicate consume 比例上升</td>
          <td>重試與重播增加，去重壓力上升</td>
          <td>強化 consumer idempotency 與去重儲存</td>
      </tr>
      <tr>
          <td>relay 重啟後出現漏發</td>
          <td>進度標記與交易邊界設計不穩</td>
          <td>收斂進度策略、補恢復測試</td>
      </tr>
      <tr>
          <td>同步交易延遲上升且 outbox 寫入增加</td>
          <td>outbox 表設計與索引不足</td>
          <td>調整索引與分表策略、拆分熱路徑</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 outbox 當作「一次解決一致性」的銀彈，會忽略消費端冪等與補償責任。outbox 保證的是發布可恢復，不是端到端結果自動正確。</p>
<p>把 outbox 表當一般業務表無上限累積，也會放大查詢與維護成本。需要定義保留與清理節奏，並確保稽核需求有對應方案。</p>
<h2 id="self-managed-vs-managed-broker-的長期-tco">Self-managed vs Managed broker 的長期 TCO</h2>
<p>Broker 選型本質是 long-term TCO 決策、需評估雲端費用 + 工程稅 + 治理負擔三層成本。Self-managed Kafka 的容量規劃 + broker 數量 + 副本因子 + disk + ZooKeeper / KRaft 治理是長期工程 tax、每次擴容是工程專案。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/spotify-kafka-to-pubsub-migration-gcp/" data-link-title="9.C9 Spotify：從自管 Kafka 遷移到 GCP Pub/Sub 的事件交付系統" data-link-desc="Spotify 把自管 Kafka 事件系統遷移到 Google Cloud Pub/Sub、避免自管 broker 的容量規劃成本">9.C9 Spotify Kafka → Pub/Sub Migration</a> — Spotify 從自管 Kafka 遷到 Google Cloud Pub/Sub、動機是 <em>容量規劃的工程成本</em> 在 sustained growth 下變得不划算、非 Kafka 效能不足。對 7500 萬用戶的事件交付系統、把 broker 容量規劃跟運維負擔卸給 vendor、釋放工程團隊 capacity。</p>
<p><strong>TCO 評估的真實成本項</strong>（9.C9 case 列前 4 項 + 雲端費用、第 5 項屬跨案例綜合）：</p>
<ul>
<li><strong>Broker 雲端費用</strong>：明面成本、相對小</li>
<li><strong>容量規劃工程</strong>：每季 partition planning、每年容量擴張專案</li>
<li><strong>故障處理人力</strong>：broker 故障 oncall、ZooKeeper / KRaft 故障診斷</li>
<li><strong>升級遷移成本</strong>：Kafka 每個 major version 升級是專案</li>
<li><strong>跨團隊治理</strong>（從 3.C6 Uber 跨案例補充）：規模化後的 multi-tenant 隔離、quota 管理、observability 建設</li>
</ul>
<p>判讀含義：Self-managed Kafka 在中小團隊可能比 Pub/Sub 便宜（雲端費用低）；但規模化後人力成本壓過雲端費用差、managed service 反而划算。對應 <a href="/blog/backend/03-message-queue/cases/vmware-kafka-to-msk/" data-link-title="3.C2 VMware Tanzu CloudHealth：Kafka 轉 Amazon MSK" data-link-desc="自管 Kafka 遷移到託管平台時的治理重點。">3.C2 VMware Tanzu Kafka → MSK</a> 同樣是「自管 → managed」的決策。</p>
<p><strong>Managed service 的取捨</strong>：</p>
<ul>
<li>Pub/Sub 自動 scaling、伴隨 vendor lock-in、cost-per-message 累積、message ordering / latency 特性跟 Kafka 差異</li>
<li>業務語意對映（Kafka partition / offset / consumer group 在 Pub/Sub 對映成 subscription / ordering key / message attribute）需重新校準、見 <a href="/blog/backend/03-message-queue/event-contract-replay-boundary/" data-link-title="3.7 Event Contract 與 Replay Boundary" data-link-desc="說明 event schema、idempotency key、replay window 與補償如何先於 broker 選型。">3.7 跨 broker 業務語意對映</a></li>
<li>遷移本身需驗證業務語意 — 對應 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 schema migration rollout evidence</a> 的同類流程</li>
</ul>
<h2 id="broker-遷移的階段流程">Broker 遷移的階段流程</h2>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/spotify-kafka-to-pubsub-migration-gcp/" data-link-title="9.C9 Spotify：從自管 Kafka 遷移到 GCP Pub/Sub 的事件交付系統" data-link-desc="Spotify 把自管 Kafka 事件系統遷移到 Google Cloud Pub/Sub、避免自管 broker 的容量規劃成本">9.C9 Spotify</a> — broker 遷移屬高併發容量工程、需維持 producer 連續寫入、保證 message 不丟。Spotify case 列三階段（dual write → shadow → cutover）、本章補第四階段（Decommission）作為清理收尾。replay 模型差異見 <a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 Replay 跟 Idempotency 共設計</a>。</p>
<ol>
<li><strong>Dual-write</strong>：producer 同時寫兩個 broker、確保 cutover 前新 broker 有完整資料</li>
<li><strong>Shadow consume</strong>：新 broker 有獨立 consumer group 消費、驗證業務結果跟舊 broker 一致</li>
<li><strong>Cutover</strong>：流量逐步切到新 broker、保留舊 broker 為 fallback</li>
<li><strong>Decommission</strong>（本章補充、case 未明文）：確認新 broker 穩定後關掉舊 broker、清理舊架構</li>
</ol>
<p>遷移期容量規劃含義：</p>
<ul>
<li>Dual-write 期間 broker 雙倍流量（writer side）</li>
<li>Shadow consume 期間 consumer 雙倍負載（reader side）</li>
<li>業務驗證（mismatch tracking）期間有額外的對帳工作量</li>
</ul>
<p>跟 <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移</a> 是同類流程、流程細節跟 evidence chain 可互相參考。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>outbox 一致性可用 <a href="/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">GitHub 2018 Oct21 MySQL Topology Incident</a> 的恢復段落回寫。先看資料寫入與下游狀態同步是否脫節，再回到本章檢查 outbox backlog、relay 進度與重播策略。
這個案例主要支撐的是「提交後發布一致性」判讀，不直接支撐 broker 的底層投遞參數；若問題是 ack/partition 策略，應回到 3.1/3.2。</p>
<p>當資料已提交但事件遲到，或重播後副作用重複時，先調整 relay 節流與 consumer 冪等，再把驗證證據對齊 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 1.3 的交接：交易邊界語意回到 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction 與一致性邊界</a>。</li>
<li>與 3.2 的交接：發布後重試與隔離回到 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">durable queue 與重試策略</a>。</li>
<li>與 3.4 的交接：消費冪等與重播回到 <a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">consumer 設計與去重</a>。</li>
<li>與 6.12 的交接：一致性驗證與重播演練回到 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">Idempotency 與 Replay 驗證</a>。</li>
<li>與 8.19 的交接：發布故障決策回到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要從 outbox 延伸到消費恢復，接著讀 <a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計與去重</a>。要看 queue 切換失敗時的一致性風險，接著讀 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例</a>。</p>
]]></content:encoded></item></channel></rss>