<?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>Replay on Tarragon</title><link>https://tarrragon.github.io/blog/tags/replay/</link><description>Recent content in Replay 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/replay/index.xml" rel="self" type="application/rss+xml"/><item><title>3.7 Event Contract 與 Replay Boundary</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/event-contract-replay-boundary/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/event-contract-replay-boundary/</guid><description>&lt;p>Event contract 與 replay boundary 的核心責任是讓事件在版本演進、重試與重播時仍可被理解與驗證。進入具體 broker 前，讀者需要先知道事件 payload 是跨服務副作用的契約。&lt;/p>
&lt;h2 id="event-contract">Event Contract&lt;/h2>
&lt;p>Event contract 的責任是定義 producer 發出的事實、consumer 能依賴的欄位，以及版本演進時的相容窗口。最小 contract 包含 event id、schema version、occurred time、producer、entity id、dedup key 與資料保護範圍。&lt;/p>
&lt;p>event id 讓訊息可追蹤；schema version 讓版本演進可判斷；occurred time 讓 replay 可分時間窗；dedup key 讓 consumer 可去重；PII scope 讓事件能接到資料保護。&lt;/p>
&lt;p>event id 支撐 incident timeline 與重複投遞判讀。schema version 支撐新舊 consumer 共存。occurred time 支撐 replay window 與對帳查詢。dedup key 支撐 idempotency。PII scope 支撐 audit 與資料保護。這些欄位先成立，broker &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention&lt;/a> 或 partition 設計才有可依附的語意。&lt;/p>
&lt;h2 id="schema-compatibility">Schema Compatibility&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-schema-compatibility/" data-link-title="Event Schema Compatibility" data-link-desc="說明 event schema 演進時，新舊 producer 與 consumer 能否互通的相容性等級">Schema compatibility&lt;/a> 的責任是讓 producer 與 consumer 可以分批升級。新增欄位要保留 optional，移除欄位要有相容窗口，語意改變要用新 version 或新 event type。&lt;/p>
&lt;p>序列化能解析是相容性的第一層。若欄位仍存在但語意改變，consumer 仍可能產生錯誤副作用。這類變更需要在 release gate 中驗證。&lt;/p>
&lt;h2 id="replay-boundary">Replay Boundary&lt;/h2>
&lt;p>Replay boundary 的責任是限制重播範圍，避免修復動作擴大事故。Replay 要能指定 time range、tenant、partition、event type、schema version 與 downstream capacity。&lt;/p>
&lt;p>replay window 要和 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">time range&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link&lt;/a> 對齊，讓事後能回放當時重播的是哪一批事件。&lt;/p>
&lt;h2 id="compensation">Compensation&lt;/h2>
&lt;p>Compensation 的責任是處理副作用已經發生但結果不正確的情況。寄信、發票、付款通知與 webhook 都可能需要補償，重播是其中一種恢復方式。&lt;/p>
&lt;p>補償前要先判斷副作用是否可逆、是否會通知使用者、是否需要人工審核。不可逆副作用要比可重播副作用更早接到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log&lt;/a>。&lt;/p>
&lt;h2 id="跨-broker-業務語意對映">跨 broker 業務語意對映&lt;/h2>
&lt;p>跨 broker migration 的工程責任是維持業務語意對映、broker 吞吐是次要驗證項。同一份 event contract 在 Kafka、Pub/Sub、SQS、NATS 的對映概念不同、需要逐項校準。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spotify-kafka-to-pubsub-migration-gcp/" data-link-title="9.C9 Spotify：從自管 Kafka 遷移到 GCP Pub/Sub 的事件交付系統" data-link-desc="Spotify 把自管 Kafka 事件系統遷移到 Google Cloud Pub/Sub、避免自管 broker 的容量規劃成本">9.C9 Spotify Kafka → Pub/Sub Migration&lt;/a> — Spotify 7500 萬用戶事件交付系統遷移、case 明確點出 Kafka 的 partition / offset / consumer group 對映成 Pub/Sub 的 subscription / ordering key / message attribute、需要校準業務語意而非直接搬。&lt;/p>
&lt;p>&lt;strong>典型概念對映差異&lt;/strong>（依據 9.C9 case 列出的三組對映展開、Pub/Sub 實際 API 細節為文章補充）：&lt;/p></description><content:encoded><![CDATA[<p>Event contract 與 replay boundary 的核心責任是讓事件在版本演進、重試與重播時仍可被理解與驗證。進入具體 broker 前，讀者需要先知道事件 payload 是跨服務副作用的契約。</p>
<h2 id="event-contract">Event Contract</h2>
<p>Event contract 的責任是定義 producer 發出的事實、consumer 能依賴的欄位，以及版本演進時的相容窗口。最小 contract 包含 event id、schema version、occurred time、producer、entity id、dedup key 與資料保護範圍。</p>
<p>event id 讓訊息可追蹤；schema version 讓版本演進可判斷；occurred time 讓 replay 可分時間窗；dedup key 讓 consumer 可去重；PII scope 讓事件能接到資料保護。</p>
<p>event id 支撐 incident timeline 與重複投遞判讀。schema version 支撐新舊 consumer 共存。occurred time 支撐 replay window 與對帳查詢。dedup key 支撐 idempotency。PII scope 支撐 audit 與資料保護。這些欄位先成立，broker <a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a> 或 partition 設計才有可依附的語意。</p>
<h2 id="schema-compatibility">Schema Compatibility</h2>
<p><a href="/blog/backend/knowledge-cards/event-schema-compatibility/" data-link-title="Event Schema Compatibility" data-link-desc="說明 event schema 演進時，新舊 producer 與 consumer 能否互通的相容性等級">Schema compatibility</a> 的責任是讓 producer 與 consumer 可以分批升級。新增欄位要保留 optional，移除欄位要有相容窗口，語意改變要用新 version 或新 event type。</p>
<p>序列化能解析是相容性的第一層。若欄位仍存在但語意改變，consumer 仍可能產生錯誤副作用。這類變更需要在 release gate 中驗證。</p>
<h2 id="replay-boundary">Replay Boundary</h2>
<p>Replay boundary 的責任是限制重播範圍，避免修復動作擴大事故。Replay 要能指定 time range、tenant、partition、event type、schema version 與 downstream capacity。</p>
<p>replay window 要和 <a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">time range</a> 與 <a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link</a> 對齊，讓事後能回放當時重播的是哪一批事件。</p>
<h2 id="compensation">Compensation</h2>
<p>Compensation 的責任是處理副作用已經發生但結果不正確的情況。寄信、發票、付款通知與 webhook 都可能需要補償，重播是其中一種恢復方式。</p>
<p>補償前要先判斷副作用是否可逆、是否會通知使用者、是否需要人工審核。不可逆副作用要比可重播副作用更早接到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</p>
<h2 id="跨-broker-業務語意對映">跨 broker 業務語意對映</h2>
<p>跨 broker migration 的工程責任是維持業務語意對映、broker 吞吐是次要驗證項。同一份 event contract 在 Kafka、Pub/Sub、SQS、NATS 的對映概念不同、需要逐項校準。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/spotify-kafka-to-pubsub-migration-gcp/" data-link-title="9.C9 Spotify：從自管 Kafka 遷移到 GCP Pub/Sub 的事件交付系統" data-link-desc="Spotify 把自管 Kafka 事件系統遷移到 Google Cloud Pub/Sub、避免自管 broker 的容量規劃成本">9.C9 Spotify Kafka → Pub/Sub Migration</a> — Spotify 7500 萬用戶事件交付系統遷移、case 明確點出 Kafka 的 partition / offset / consumer group 對映成 Pub/Sub 的 subscription / ordering key / message attribute、需要校準業務語意而非直接搬。</p>
<p><strong>典型概念對映差異</strong>（依據 9.C9 case 列出的三組對映展開、Pub/Sub 實際 API 細節為文章補充）：</p>
<ul>
<li><strong>Partition (Kafka) 跟 Subscription (Pub/Sub)</strong>：Kafka partition 是物理分片 + 順序邊界；Pub/Sub subscription 是邏輯 <a href="/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out</a>、無物理分片概念。靠 Kafka partition 保證 per-key 順序的 consumer、遷到 Pub/Sub 改用 ordering key</li>
<li><strong>Offset (Kafka) 對映成 message attribute (Pub/Sub)</strong>：9.C9 case 原文對映方向；replay 模型差異上、Kafka offset 是位置指標、可任意回放到某個 offset；Pub/Sub 用 Snapshot + Seek API 達成類似 replay 能力、模型不同</li>
<li><strong>Consumer Group (Kafka) 跟 Subscription (Pub/Sub)</strong>：Kafka consumer group 內部 rebalance 自動分 partition；Pub/Sub subscription 自動分 message、語意接近但 rebalance 細節差異會影響 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a> message 處理順序</li>
</ul>
<p><strong>遷移評估要驗證的業務語意</strong>：</p>
<ul>
<li>順序保證：原系統靠 partition / consumer group 保證什麼順序、新系統能否複製</li>
<li>Replay 模型：原系統 replay 方式、新系統的 replay 工具能否達成同範圍。replay window 上限由 idempotency 保留期反推、見 <a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 Replay 跟 Idempotency 共設計</a></li>
<li>失敗模式：consumer 故障時、原系統的 rebalance / redelivery 行為、新系統會不會差異</li>
</ul>
<p>判讀重點：broker migration 屬語意對映工程、吞吐能力比較是次要驗證項。對應 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern 的「Broker 遷移階段流程」</a>、實作面用 dual-write + shadow consume + cutover、驗證面靠 event contract 跟 replay 邊界做對帳。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 03 內部：replay window 跟 idempotency 共設計回到 <a href="/blog/backend/03-message-queue/processing-recovery-semantics/" data-link-title="3.6 Processing Semantics 與 Recovery Semantics" data-link-desc="說明投遞成功、處理成功與恢復成功為何是三個不同判斷。">3.6 processing-recovery-semantics</a>；broker 遷移階段流程回到 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern</a></li>
<li>與 04 的交接：event contract 演進 + replay 邊界進 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
<li>與 06 的交接：event contract 跟 replay 驗證進 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 Idempotency 與 Replay 驗證</a> 跟 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff</a></li>
<li>與 07 的交接：event payload 的 PII / audit 邊界進 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">7.4 data protection and masking</a></li>
</ol>
<h2 id="選型前判準">選型前判準</h2>
<p>Broker 選型前要先回答：</p>
<ol>
<li>event contract 是否能支援版本相容。</li>
<li>consumer 是否能用 dedup key 判斷重複。</li>
<li>replay window 是否能用查詢與指標證明。</li>
<li>不可逆副作用是否有補償流程。</li>
<li>event payload 是否包含 PII 或 audit-sensitive 欄位。</li>
</ol>
<p>這些問題決定後續要比較 broker retention、schema registry、DLQ、partition 與 replay 工具，並把吞吐放回服務語意下判讀。</p>
<h2 id="實體服務討論承接點">實體服務討論承接點</h2>
<p>實體 broker 文章要承接本篇的 event contract 與 replay boundary。Kafka 的長期 retention、RabbitMQ 的 routing 與 DLQ、SQS 的 <a href="/blog/backend/knowledge-cards/visibility-timeout/" data-link-title="Visibility Timeout" data-link-desc="說明訊息被取走後對其他 consumer 暫時不可見的時間窗，timeout 後重新投遞">visibility timeout</a>、NATS JetStream 的 stream/consumer 模型，都要放回事件契約與重播邊界下判讀。</p>
<p>若事件需要長期 replay，後續文章要比較 retention、offset、partition 與 schema evolution。若事件主要是工作任務，後續文章要比較 visibility、ack、DLQ 與重試治理。若事件包含 PII 或高風險副作用，後續文章要比較 audit、encryption、access control 與補償流程。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>要處理 outbox 與事件發布一致性，接著讀 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern 與發佈一致性</a>。要處理 consumer 端去重與重播，接著讀 <a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計與去重</a>。</p>
]]></content:encoded></item><item><title>3.8 Queue Consumer Retry 與 Replay Handoff（實作示範）</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/queue-consumer-retry-replay-handoff/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/queue-consumer-retry-replay-handoff/</guid><description>&lt;p>Queue consumer retry 與 replay handoff 的核心責任是把 request 外副作用做成可重試、可去重、可隔離、可重播的服務流程。這篇以 &lt;code>order_created&lt;/code> consumer 為例，示範 delivery、processing、recovery 三層語意如何交接到 evidence package、release gate 與 incident decision log。&lt;/p>
&lt;h2 id="服務路徑與語意分層">服務路徑與語意分層&lt;/h2>
&lt;p>這條路徑是 &lt;code>order-service -&amp;gt; broker -&amp;gt; order-created-consumer -&amp;gt; invoice/email/search/webhook&lt;/code>。Producer 把事件交給 broker 後，真正的業務完成要看 consumer 是否正確提交副作用。&lt;/p>
&lt;p>這篇先固定三層語意：&lt;/p>
&lt;ol>
&lt;li>Delivery semantics：訊息是否投遞與確認。&lt;/li>
&lt;li>Processing semantics：副作用是否可承受重複與部分失敗。&lt;/li>
&lt;li>Recovery semantics：故障後是否可重播並恢復一致。&lt;/li>
&lt;/ol>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack/nack&lt;/a> 成功只代表 delivery 進度，不代表發票與通知已完成。&lt;/p>
&lt;h2 id="event-contract-與相容邊界">Event Contract 與相容邊界&lt;/h2>
&lt;p>Event contract 的責任是讓 producer 與 consumer 在版本演進時仍可互通，且可被觀測與回放。&lt;/p>
&lt;p>&lt;code>order_created&lt;/code> 最小欄位：&lt;/p>
&lt;ol>
&lt;li>&lt;code>event_id&lt;/code>：全域唯一識別。&lt;/li>
&lt;li>&lt;code>schema_version&lt;/code>：事件版本。&lt;/li>
&lt;li>&lt;code>occurred_at&lt;/code>：事件發生時間。&lt;/li>
&lt;li>&lt;code>order_id&lt;/code>、&lt;code>tenant_id&lt;/code>：業務定位。&lt;/li>
&lt;li>&lt;code>idempotency_key&lt;/code>：副作用去重鍵。&lt;/li>
&lt;li>&lt;code>pii_scope&lt;/code>：敏感欄位範圍。&lt;/li>
&lt;/ol>
&lt;p>版本演進採向後相容優先：新增欄位可選、舊欄位保留窗口。schema 演進前要先確認 consumer 端 fallback 解析邏輯存在，避免切版後整批進 DLQ。&lt;/p>
&lt;h2 id="retry--dlq--quarantine">Retry / DLQ / Quarantine&lt;/h2>
&lt;p>Retry 的責任是吸收暫時性故障，不把短暫抖動升級成事故。這條路徑使用有限重試 + backoff + jitter：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>階段&lt;/th>
 &lt;th>判讀重點&lt;/th>
 &lt;th>動作&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>即時重試&lt;/td>
 &lt;td>下游短暫 timeout 或限流&lt;/td>
 &lt;td>在主通道重試少量次數&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>延遲重試&lt;/td>
 &lt;td>故障持續但可恢復&lt;/td>
 &lt;td>延長 backoff，避免重試風暴&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DLQ 隔離&lt;/td>
 &lt;td>payload 或版本異常、長時故障&lt;/td>
 &lt;td>轉入 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Quarantine&lt;/td>
 &lt;td>同型 poison message 連續爆發&lt;/td>
 &lt;td>停主通道回放，先分群診斷&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>DLQ 的責任是隔離與診斷，不是永久儲存。重點是把異常訊息分群後對應修法，修完再定向回放。&lt;/p>
&lt;h2 id="idempotency-與-ack-timing">Idempotency 與 Ack Timing&lt;/h2>
&lt;p>Idempotency 的責任是把 at-least-once 交付轉成可接受業務結果。副作用如發票、email、webhook 都要以 &lt;code>idempotency_key&lt;/code> 做去重。&lt;/p>
&lt;p>Ack timing 的原則是「核心副作用提交後再 ack」：&lt;/p>
&lt;ol>
&lt;li>先執行副作用或落地可追蹤結果。&lt;/li>
&lt;li>成功後寫去重紀錄或 checkpoint。&lt;/li>
&lt;li>最後 ack broker。&lt;/li>
&lt;/ol>
&lt;p>先 ack 再副作用會造成資料遺失；副作用成功但去重紀錄失敗，則要由 recovery 層補償。&lt;/p>
&lt;h2 id="replay-runbook">Replay Runbook&lt;/h2>
&lt;p>Replay 的責任是故障後在可控範圍內恢復，不把修復變成第二次事故。&lt;/p>
&lt;p>這條路徑的 replay runbook：&lt;/p>
&lt;ol>
&lt;li>選定 replay window：依 &lt;code>occurred_at&lt;/code> 與 &lt;code>schema_version&lt;/code> 分段。&lt;/li>
&lt;li>Dry run：先在影子通道跑去重與下游容量驗證。&lt;/li>
&lt;li>限速回放：按 tenant 或 partition 分批，監控下游錯誤率。&lt;/li>
&lt;li>Reconciliation：對帳發票、通知、索引結果。&lt;/li>
&lt;li>Stop condition：duplicate side-effect、downstream timeout、DLQ 再爆發即停。&lt;/li>
&lt;/ol>
&lt;p>replay window 要能被明確描述與回放，不可用「重播昨天全部」這種不可驗證句子。&lt;/p>
&lt;h2 id="job-queue-的拓樸分工">Job queue 的拓樸分工&lt;/h2>
&lt;p>當背景工作同時要 &lt;em>高吞吐&lt;/em> 跟 &lt;em>快速反應&lt;/em>、單一通道模型會變成瓶頸。job queue 的擴展通常是 &lt;em>拓樸重整&lt;/em>、把不同工作類型切到不同傳遞路徑、而非單點替換。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/slack-job-queue-kafka-redis/" data-link-title="3.C5 Slack：Job Queue 演進到 Kafka &amp;#43; Redis" data-link-desc="背景工作通道在成長期如何從單一路徑演進成組合式架構。">3.C5 Slack Job Queue 演進到 Kafka + Redis&lt;/a> — Slack 在 job queue 擴展時把工作切到不同傳遞路徑、Kafka 跟 Redis 分別承擔持久性跟即時性目標、分開治理 lag、重試跟失敗重播。&lt;/p></description><content:encoded><![CDATA[<p>Queue consumer retry 與 replay handoff 的核心責任是把 request 外副作用做成可重試、可去重、可隔離、可重播的服務流程。這篇以 <code>order_created</code> consumer 為例，示範 delivery、processing、recovery 三層語意如何交接到 evidence package、release gate 與 incident decision log。</p>
<h2 id="服務路徑與語意分層">服務路徑與語意分層</h2>
<p>這條路徑是 <code>order-service -&gt; broker -&gt; order-created-consumer -&gt; invoice/email/search/webhook</code>。Producer 把事件交給 broker 後，真正的業務完成要看 consumer 是否正確提交副作用。</p>
<p>這篇先固定三層語意：</p>
<ol>
<li>Delivery semantics：訊息是否投遞與確認。</li>
<li>Processing semantics：副作用是否可承受重複與部分失敗。</li>
<li>Recovery semantics：故障後是否可重播並恢復一致。</li>
</ol>
<p><a href="/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack/nack</a> 成功只代表 delivery 進度，不代表發票與通知已完成。</p>
<h2 id="event-contract-與相容邊界">Event Contract 與相容邊界</h2>
<p>Event contract 的責任是讓 producer 與 consumer 在版本演進時仍可互通，且可被觀測與回放。</p>
<p><code>order_created</code> 最小欄位：</p>
<ol>
<li><code>event_id</code>：全域唯一識別。</li>
<li><code>schema_version</code>：事件版本。</li>
<li><code>occurred_at</code>：事件發生時間。</li>
<li><code>order_id</code>、<code>tenant_id</code>：業務定位。</li>
<li><code>idempotency_key</code>：副作用去重鍵。</li>
<li><code>pii_scope</code>：敏感欄位範圍。</li>
</ol>
<p>版本演進採向後相容優先：新增欄位可選、舊欄位保留窗口。schema 演進前要先確認 consumer 端 fallback 解析邏輯存在，避免切版後整批進 DLQ。</p>
<h2 id="retry--dlq--quarantine">Retry / DLQ / Quarantine</h2>
<p>Retry 的責任是吸收暫時性故障，不把短暫抖動升級成事故。這條路徑使用有限重試 + backoff + jitter：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>判讀重點</th>
          <th>動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>即時重試</td>
          <td>下游短暫 timeout 或限流</td>
          <td>在主通道重試少量次數</td>
      </tr>
      <tr>
          <td>延遲重試</td>
          <td>故障持續但可恢復</td>
          <td>延長 backoff，避免重試風暴</td>
      </tr>
      <tr>
          <td>DLQ 隔離</td>
          <td>payload 或版本異常、長時故障</td>
          <td>轉入 <a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">dead-letter queue</a></td>
      </tr>
      <tr>
          <td>Quarantine</td>
          <td>同型 poison message 連續爆發</td>
          <td>停主通道回放，先分群診斷</td>
      </tr>
  </tbody>
</table>
<p>DLQ 的責任是隔離與診斷，不是永久儲存。重點是把異常訊息分群後對應修法，修完再定向回放。</p>
<h2 id="idempotency-與-ack-timing">Idempotency 與 Ack Timing</h2>
<p>Idempotency 的責任是把 at-least-once 交付轉成可接受業務結果。副作用如發票、email、webhook 都要以 <code>idempotency_key</code> 做去重。</p>
<p>Ack timing 的原則是「核心副作用提交後再 ack」：</p>
<ol>
<li>先執行副作用或落地可追蹤結果。</li>
<li>成功後寫去重紀錄或 checkpoint。</li>
<li>最後 ack broker。</li>
</ol>
<p>先 ack 再副作用會造成資料遺失；副作用成功但去重紀錄失敗，則要由 recovery 層補償。</p>
<h2 id="replay-runbook">Replay Runbook</h2>
<p>Replay 的責任是故障後在可控範圍內恢復，不把修復變成第二次事故。</p>
<p>這條路徑的 replay runbook：</p>
<ol>
<li>選定 replay window：依 <code>occurred_at</code> 與 <code>schema_version</code> 分段。</li>
<li>Dry run：先在影子通道跑去重與下游容量驗證。</li>
<li>限速回放：按 tenant 或 partition 分批，監控下游錯誤率。</li>
<li>Reconciliation：對帳發票、通知、索引結果。</li>
<li>Stop condition：duplicate side-effect、downstream timeout、DLQ 再爆發即停。</li>
</ol>
<p>replay window 要能被明確描述與回放，不可用「重播昨天全部」這種不可驗證句子。</p>
<h2 id="job-queue-的拓樸分工">Job queue 的拓樸分工</h2>
<p>當背景工作同時要 <em>高吞吐</em> 跟 <em>快速反應</em>、單一通道模型會變成瓶頸。job queue 的擴展通常是 <em>拓樸重整</em>、把不同工作類型切到不同傳遞路徑、而非單點替換。</p>
<p>對應 <a href="/blog/backend/03-message-queue/cases/slack-job-queue-kafka-redis/" data-link-title="3.C5 Slack：Job Queue 演進到 Kafka &#43; Redis" data-link-desc="背景工作通道在成長期如何從單一路徑演進成組合式架構。">3.C5 Slack Job Queue 演進到 Kafka + Redis</a> — Slack 在 job queue 擴展時把工作切到不同傳遞路徑、Kafka 跟 Redis 分別承擔持久性跟即時性目標、分開治理 lag、重試跟失敗重播。</p>
<p><strong>拓樸分工的判讀</strong>（基於 Slack case 揭露的雙通道分工方向）：</p>
<ul>
<li><strong>持久性主導的 job</strong>（發票、付款通知、合規記錄）→ Kafka / 持久 queue、保證 at-least-once</li>
<li><strong>即時性主導的 job</strong>（線上提醒、playback control、UI 更新）→ Redis / 輕量 queue、low latency</li>
</ul>
<p>設計含義：同一 consumer 應專注單一目標（高吞吐 / 即時 / 持久擇一）、其他目標拆到對應路徑。對應 <a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer-design 三個工程議題鐵三角</a> — idempotency / 重播流程 / 下游承載能力是 consumer 內部設計、拓樸分工是 <em>跨 consumer</em> 的責任拆分、兩者互補。</p>
<h2 id="job-queue-規模差異的治理重點">Job queue 規模差異的治理重點</h2>
<p>不同規模服務的 job queue 治理問題差異大、SSoT 在本章。對應 <a href="/blog/backend/03-message-queue/cases/contrast-queue-model-by-scale/" data-link-title="3.C10 對照：規模差異下的佇列模型" data-link-desc="同一 queue 模型在不同規模下的治理與失敗邊界差異。">3.C10 對照：規模差異下的佇列模型</a>：</p>
<ul>
<li><strong>小型服務</strong>：優先用 managed queue（SQS / Pub/Sub）、運維成本最低。最容易忽略的是語意邊界（重試次數、死信規則、重播責任）、規模一上來會出現資料重複與漏處理。<strong>升級訊號</strong>：team 數超 3-5 個、各自寫 consumer 開始出現 idempotency 不一致、進中型階段</li>
<li><strong>中型服務</strong>：常見問題是 lag 與 DLQ 長期累積。原因是 consumer idempotency + 重播流程 + 下游承載能力沒一起設計。對應前段 Job queue 拓樸分工。<strong>升級訊號</strong>：DLQ 累積速度高於排空速度連續 7 天、單一 tenant 流量尖峰拖垮其他 tenant、進大型階段</li>
<li><strong>大型服務</strong>：需要處理跨租戶跟跨區壓力。單叢集思維會讓任何一類流量尖峰拖垮整體。對應 <a href="/blog/backend/03-message-queue/cases/linkedin-kafka-tiered-clusters/" data-link-title="3.C4 LinkedIn：Kafka 分層叢集治理" data-link-desc="Kafka 從單叢集走向 tiered clusters 的轉換案例。">3.C4 LinkedIn Tiered Clusters</a> 跟 <a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker-basics 分層治理平台</a>、重點從「怎麼送訊息」轉成「怎麼隔離失敗」</li>
</ul>
<p>判讀重點：當前服務規模決定要處理的 <em>主要</em> 問題。規模尚小的服務硬上 multi-tenant 隔離治理屬過度設計、規模化服務應同時考慮 broker 容量是否充足跟隔離邊界是否完整。判斷自己在哪個階段、看 <em>升級訊號</em> 對應的指標。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>Queue evidence 的責任是證明「投遞可達」與「處理可恢復」兩者同時成立。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>broker metric、consumer metric、DLQ log、reconciliation query</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">Time range</a></td>
          <td>retry/replay 批次窗口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">Query link</a></td>
          <td>lag、retry count、DLQ count、duplicate side-effect、throughput</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>queue owner、consumer owner、downstream owner</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">Data quality</a></td>
          <td>指標延遲、抽樣缺口、對帳覆蓋率</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">Confidence</a></td>
          <td>confirmed / suspected / needs follow-up</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">Known gap</a></td>
          <td>尚未驗證之下游 webhook 供應商、低流量 tenant replay</td>
      </tr>
  </tbody>
</table>
<p>這份 evidence 要對齊 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a> 與 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff</a>。</p>
<h2 id="release-gate">Release Gate</h2>
<p>Queue release gate 的責任是決定是否擴大回放或恢復主通道，而不是只看單一 lag 指標。</p>
<table>
  <thead>
      <tr>
          <th>Gate 欄位</th>
          <th>最小內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/gate-decision/" data-link-title="Gate Decision" data-link-desc="說明 release gate 如何把證據轉成放行、暫停、回退或補證據的決策">Gate decision</a></td>
          <td>放行下一批 replay、維持觀察、暫停 consumer</td>
      </tr>
      <tr>
          <td>Checks</td>
          <td>idempotency proof、DLQ drain 結果、下游容量、duplicate 比例</td>
      </tr>
      <tr>
          <td>Stop condition</td>
          <td>retry storm、DLQ 再爆發、下游錯誤率超門檻</td>
      </tr>
      <tr>
          <td>Rollback window</td>
          <td>replay 可中止窗口、主通道可回切時間</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>queue on-call、business owner</td>
      </tr>
  </tbody>
</table>
<p>這組欄位對齊 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 Idempotency 與 Replay 驗證</a> 與 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a>。</p>
<h2 id="incident-decision-log">Incident Decision Log</h2>
<p>pause consumer、drain DLQ、啟動 replay、停止 replay、執行補償都屬事故決策，需寫入 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">incident_decision</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">timestamp</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-05-11T13:18:00Z</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">decision</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;pause invoice consumer and start scoped replay for tenant A&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">context</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;duplicate invoices increased after consumer version rollout&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">evidence</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span>- <span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l">duplicate_invoice_ratio_tenant_a</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span>- <span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l">dlq_events_by_schema_version</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">owner</span><span class="p">:</span><span class="w"> </span><span class="l">queue-incident-commander</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">expected_effect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;stop duplicate side effects and restore invoice consistency&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">rollback_condition</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;duplicate ratio does not decrease within two replay batches&#34;</span></span></span></code></pre></div><h2 id="case-write-back-與邊界">Case Write-back 與邊界</h2>
<p>這篇回寫對齊 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例</a>，重點是切換時語意分層混淆導致 delivery 成功但業務結果失真。</p>
<p>這篇不處理同步 API latency、cache TTL 或 deployment drain。若風險在同步交易壓力、快取失效或流量切換，路由到 <a href="/blog/backend/04-observability/checkout-api-evidence-package/" data-link-title="4.22 Checkout API Evidence Package 實作示範" data-link-desc="用 checkout 路徑示範 evidence package 如何交接給 release gate 與 incident decision。">4.22 Checkout API Evidence Package</a>、<a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9 Cache Migration 與 Stampede Rollback</a> 或 <a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.8 Deployment Rollout with Drain and Rollback</a>。</p>
]]></content:encoded></item></channel></rss>