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





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





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">incident_decision</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">timestamp</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-05-11T15:06:00Z</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">decision</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;freeze rollout at 25% and rollback one region&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">context</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;new version timeout to payment provider increased in ap-northeast&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">evidence</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span>- <span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l">checkout_error_rate_by_version_region</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span>- <span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l">payment_timeout_ratio_by_region</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">owner</span><span class="p">:</span><span class="w"> </span><span class="l">release-incident-commander</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">expected_effect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;contain customer impact and restore baseline success rate&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">rollback_condition</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;timeout ratio does not recover after rollback batch completes&#34;</span></span></span></code></pre></div><h2 id="case-write-back-與邊界">Case Write-back 與邊界</h2>
<p>這篇回寫對齊 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a>、<a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift</a> 與 <a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera</a>：前者看切換失序，後兩者看遷移路徑與回退策略。preflight / canary / drain 各階段的生命週期定義回到 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a>。</p>
<p>這篇不處理 schema migration 本身、cache stampede 或 queue replay。若核心風險在資料正式狀態、快取回源或事件恢復，路由到 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a>、<a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9 Cache Migration 與 Stampede Rollback</a> 或 <a href="/blog/backend/03-message-queue/queue-consumer-retry-replay-handoff/" data-link-title="3.8 Queue Consumer Retry 與 Replay Handoff（實作示範）" data-link-desc="以 order_created consumer 示範 queue 路徑如何交付 idempotency evidence、DLQ handling、replay runbook 與 incident decision log。">3.8 Queue Consumer Retry 與 Replay Handoff</a>。</p>
]]></content:encoded></item><item><title>7.24 資安事故如何回寫產品與架構</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/security-incident-write-back-to-product-and-architecture/</link><pubDate>Thu, 30 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/security-incident-write-back-to-product-and-architecture/</guid><description>&lt;p>本篇的責任是建立事故回寫路由。讀者讀完後，能把 incident 結果回寫到產品、架構、控制模式與章節知識網。&lt;/p>
&lt;h2 id="核心論點">核心論點&lt;/h2>
&lt;p>事故回寫的核心概念是把一次事件轉成長期能力。回寫完成後，下一次同類事件會在更早階段被辨識與收斂。&lt;/p>
&lt;h2 id="回寫層級">回寫層級&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層級&lt;/th>
 &lt;th>回寫目標&lt;/th>
 &lt;th>產出&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Rule layer&lt;/td>
 &lt;td>偵測規則與調校策略&lt;/td>
 &lt;td>rule update&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Control layer&lt;/td>
 &lt;td>控制面與驗證條件&lt;/td>
 &lt;td>control update&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Workflow layer&lt;/td>
 &lt;td>triage、升級、通訊流程&lt;/td>
 &lt;td>workflow update&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Product layer&lt;/td>
 &lt;td>需求優先序與設計輸入&lt;/td>
 &lt;td>product backlog&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Knowledge layer&lt;/td>
 &lt;td>章節、案例、卡片&lt;/td>
 &lt;td>documentation update&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="回寫欄位">回寫欄位&lt;/h2>
&lt;p>回寫欄位的責任是讓教訓可重用。每次回寫至少記錄事件訊號、決策原因、成本影響、改進方案、驗收條件與下一次檢查點。&lt;/p>
&lt;h2 id="與產品決策連結">與產品決策連結&lt;/h2>
&lt;p>與產品決策連結的責任是讓安全改進進入 roadmap。高影響教訓可轉成設計約束、放行條件與資源分配調整。&lt;/p>
&lt;h2 id="與架構決策連結">與架構決策連結&lt;/h2>
&lt;p>與架構決策連結的責任是讓技術改進可追溯。回寫到架構時需標示控制責任、邊界改動與相依影響。&lt;/p>
&lt;h2 id="與知識網連結">與知識網連結&lt;/h2>
&lt;p>與知識網連結的責任是讓教訓可查詢。回寫結果可同步更新 7.x 章節、藍隊素材庫與知識卡片連結。&lt;/p>
&lt;h2 id="素材回寫入口">素材回寫入口&lt;/h2>
&lt;p>素材回寫入口的責任是把 field case、scenario 與 control pattern 轉成文章更新路由。案例提供壓力，情境提供演練，控制模式提供可搬運欄位。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>素材&lt;/th>
 &lt;th>回寫責任&lt;/th>
 &lt;th>文章路由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/field-cases/" data-link-title="7.BM2 藍隊現場案例素材" data-link-desc="定義藍隊現場案例的收錄規則，支援後續防守推演與控制面補強">Field cases&lt;/a>&lt;/td>
 &lt;td>把真實事件壓力整理成 defender pressure&lt;/td>
 &lt;td>&lt;code>7.B12&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/scenarios/" data-link-title="7.BM3 藍隊推演情境素材" data-link-desc="定義藍隊推演情境模板，協助把來源與案例轉成 tabletop 與 Game Day">Scenarios&lt;/a>&lt;/td>
 &lt;td>把案例壓力轉成 tabletop 與 Game Day&lt;/td>
 &lt;td>&lt;code>7.B9&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/" data-link-title="7.BM4 藍隊控制模式素材" data-link-desc="定義藍隊控制模式分類，支援 release gate、偵測驗證與事故交接">Control patterns&lt;/a>&lt;/td>
 &lt;td>把重複做法抽成 owner、evidence、lifecycle 與 write-back 欄位&lt;/td>
 &lt;td>&lt;code>7.B1&lt;/code> + &lt;code>7.B3&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/exercise-write-back-pattern/" data-link-title="Exercise Write-back Pattern" data-link-desc="定義 tabletop 與 game day 如何把 finding 回寫成控制更新、runbook 更新與 [tripwire](/backend/knowledge-cards/tripwire/)">Exercise write-back pattern&lt;/a>&lt;/td>
 &lt;td>把演練 finding 轉成控制、runbook、owner 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tripwire/" data-link-title="Tripwire" data-link-desc="說明風險決策在條件變化時如何自動回到評估流程">tripwire&lt;/a> 任務&lt;/td>
 &lt;td>&lt;code>7.24&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern&lt;/a>&lt;/td>
 &lt;td>把 MFA、rotation、reset workflow 與 exposure monitoring 寫進產品基線&lt;/td>
 &lt;td>&lt;code>7.2&lt;/code> + &lt;code>7.B12&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/recovery-readiness-pattern/" data-link-title="Recovery Readiness Pattern" data-link-desc="定義長時間 outage 復原、備援存取與外部依賴溝通的共同欄位">Recovery readiness pattern&lt;/a>&lt;/td>
 &lt;td>把復原目標、備援存取、依賴地圖與通報節奏寫進架構決策&lt;/td>
 &lt;td>&lt;code>7.24&lt;/code> + &lt;code>08&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="判讀訊號與路由">判讀訊號與路由&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>判讀訊號&lt;/th>
 &lt;th>代表需求&lt;/th>
 &lt;th>下一步路由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>事故後只有修補任務&lt;/td>
 &lt;td>需要補產品與架構回寫&lt;/td>
 &lt;td>7.24 → 7.21&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回寫內容找不到驗收條件&lt;/td>
 &lt;td>需要補回寫欄位&lt;/td>
 &lt;td>7.24 → 7.B3&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同類事件重複出現&lt;/td>
 &lt;td>需要補 workflow 與規則更新&lt;/td>
 &lt;td>7.24 → 7.B5 / 7.B6&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>教訓留在單次會議紀錄&lt;/td>
 &lt;td>需要補知識網連結&lt;/td>
 &lt;td>7.24 → 7.26&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="必連章節">必連章節&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">7.B5 Detection Engineering Lifecycle&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/incident-triage-loop/" data-link-title="7.B6 Incident Triage Loop" data-link-desc="把資安訊號轉成 triage、severity、owner、containment 與 evidence 的回應循環">7.B6 Incident Triage Loop&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/blue-team-scenario-library/" data-link-title="7.B9 Blue Team Scenario Library" data-link-desc="把高風險服務情境轉成可重用推演素材，支援 tabletop 與 game day 設計">7.B9 Blue Team Scenario Library&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/defender-pressure-from-real-incidents/" data-link-title="7.B12 Defender Pressure From Real Incidents" data-link-desc="從真實事故抽出防守壓力模型，補強藍隊判讀、演練與交接設計">7.B12 Defender Pressure From Real Incidents&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/security-as-service-design-input/" data-link-title="7.21 資安如何成為服務設計輸入" data-link-desc="把資安需求前移到服務設計階段，建立可交接的設計輸入欄位與判讀路由">7.21 資安如何成為服務設計輸入&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/security-material-library-for-engineering-simulation/" data-link-title="7.26 資安素材庫如何支援工程推演" data-link-desc="說明專業來源、案例、情境與控制模式如何組合成工程推演與章節回寫流程">7.26 資安素材庫如何支援工程推演&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="完稿判準">完稿判準&lt;/h2>
&lt;p>完稿時要讓讀者能把事故教訓寫成回寫任務。輸出至少包含回寫層級、回寫欄位、產品路由、架構路由與知識路由。&lt;/p></description><content:encoded><![CDATA[<p>本篇的責任是建立事故回寫路由。讀者讀完後，能把 incident 結果回寫到產品、架構、控制模式與章節知識網。</p>
<h2 id="核心論點">核心論點</h2>
<p>事故回寫的核心概念是把一次事件轉成長期能力。回寫完成後，下一次同類事件會在更早階段被辨識與收斂。</p>
<h2 id="回寫層級">回寫層級</h2>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>回寫目標</th>
          <th>產出</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Rule layer</td>
          <td>偵測規則與調校策略</td>
          <td>rule update</td>
      </tr>
      <tr>
          <td>Control layer</td>
          <td>控制面與驗證條件</td>
          <td>control update</td>
      </tr>
      <tr>
          <td>Workflow layer</td>
          <td>triage、升級、通訊流程</td>
          <td>workflow update</td>
      </tr>
      <tr>
          <td>Product layer</td>
          <td>需求優先序與設計輸入</td>
          <td>product backlog</td>
      </tr>
      <tr>
          <td>Knowledge layer</td>
          <td>章節、案例、卡片</td>
          <td>documentation update</td>
      </tr>
  </tbody>
</table>
<h2 id="回寫欄位">回寫欄位</h2>
<p>回寫欄位的責任是讓教訓可重用。每次回寫至少記錄事件訊號、決策原因、成本影響、改進方案、驗收條件與下一次檢查點。</p>
<h2 id="與產品決策連結">與產品決策連結</h2>
<p>與產品決策連結的責任是讓安全改進進入 roadmap。高影響教訓可轉成設計約束、放行條件與資源分配調整。</p>
<h2 id="與架構決策連結">與架構決策連結</h2>
<p>與架構決策連結的責任是讓技術改進可追溯。回寫到架構時需標示控制責任、邊界改動與相依影響。</p>
<h2 id="與知識網連結">與知識網連結</h2>
<p>與知識網連結的責任是讓教訓可查詢。回寫結果可同步更新 7.x 章節、藍隊素材庫與知識卡片連結。</p>
<h2 id="素材回寫入口">素材回寫入口</h2>
<p>素材回寫入口的責任是把 field case、scenario 與 control pattern 轉成文章更新路由。案例提供壓力，情境提供演練，控制模式提供可搬運欄位。</p>
<table>
  <thead>
      <tr>
          <th>素材</th>
          <th>回寫責任</th>
          <th>文章路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/materials/field-cases/" data-link-title="7.BM2 藍隊現場案例素材" data-link-desc="定義藍隊現場案例的收錄規則，支援後續防守推演與控制面補強">Field cases</a></td>
          <td>把真實事件壓力整理成 defender pressure</td>
          <td><code>7.B12</code></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/materials/scenarios/" data-link-title="7.BM3 藍隊推演情境素材" data-link-desc="定義藍隊推演情境模板，協助把來源與案例轉成 tabletop 與 Game Day">Scenarios</a></td>
          <td>把案例壓力轉成 tabletop 與 Game Day</td>
          <td><code>7.B9</code></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/" data-link-title="7.BM4 藍隊控制模式素材" data-link-desc="定義藍隊控制模式分類，支援 release gate、偵測驗證與事故交接">Control patterns</a></td>
          <td>把重複做法抽成 owner、evidence、lifecycle 與 write-back 欄位</td>
          <td><code>7.B1</code> + <code>7.B3</code></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/exercise-write-back-pattern/" data-link-title="Exercise Write-back Pattern" data-link-desc="定義 tabletop 與 game day 如何把 finding 回寫成控制更新、runbook 更新與 [tripwire](/backend/knowledge-cards/tripwire/)">Exercise write-back pattern</a></td>
          <td>把演練 finding 轉成控制、runbook、owner 與 <a href="/blog/backend/knowledge-cards/tripwire/" data-link-title="Tripwire" data-link-desc="說明風險決策在條件變化時如何自動回到評估流程">tripwire</a> 任務</td>
          <td><code>7.24</code></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/credential-hygiene-pattern/" data-link-title="Credential Hygiene Pattern" data-link-desc="定義 credential、MFA、輪替、infostealer 監控與 network boundary 的共同基線">Credential hygiene pattern</a></td>
          <td>把 MFA、rotation、reset workflow 與 exposure monitoring 寫進產品基線</td>
          <td><code>7.2</code> + <code>7.B12</code></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/07-security-data-protection/blue-team/materials/control-patterns/recovery-readiness-pattern/" data-link-title="Recovery Readiness Pattern" data-link-desc="定義長時間 outage 復原、備援存取與外部依賴溝通的共同欄位">Recovery readiness pattern</a></td>
          <td>把復原目標、備援存取、依賴地圖與通報節奏寫進架構決策</td>
          <td><code>7.24</code> + <code>08</code></td>
      </tr>
  </tbody>
</table>
<h2 id="判讀訊號與路由">判讀訊號與路由</h2>
<table>
  <thead>
      <tr>
          <th>判讀訊號</th>
          <th>代表需求</th>
          <th>下一步路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>事故後只有修補任務</td>
          <td>需要補產品與架構回寫</td>
          <td>7.24 → 7.21</td>
      </tr>
      <tr>
          <td>回寫內容找不到驗收條件</td>
          <td>需要補回寫欄位</td>
          <td>7.24 → 7.B3</td>
      </tr>
      <tr>
          <td>同類事件重複出現</td>
          <td>需要補 workflow 與規則更新</td>
          <td>7.24 → 7.B5 / 7.B6</td>
      </tr>
      <tr>
          <td>教訓留在單次會議紀錄</td>
          <td>需要補知識網連結</td>
          <td>7.24 → 7.26</td>
      </tr>
  </tbody>
</table>
<h2 id="必連章節">必連章節</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/" data-link-title="7.B5 Detection Engineering Lifecycle" data-link-desc="把偵測規則視為可維護資產，建立從來源、測試、調校到退場的完整生命週期">7.B5 Detection Engineering Lifecycle</a></li>
<li><a href="/blog/backend/07-security-data-protection/blue-team/incident-triage-loop/" data-link-title="7.B6 Incident Triage Loop" data-link-desc="把資安訊號轉成 triage、severity、owner、containment 與 evidence 的回應循環">7.B6 Incident Triage Loop</a></li>
<li><a href="/blog/backend/07-security-data-protection/blue-team/blue-team-scenario-library/" data-link-title="7.B9 Blue Team Scenario Library" data-link-desc="把高風險服務情境轉成可重用推演素材，支援 tabletop 與 game day 設計">7.B9 Blue Team Scenario Library</a></li>
<li><a href="/blog/backend/07-security-data-protection/blue-team/defender-pressure-from-real-incidents/" data-link-title="7.B12 Defender Pressure From Real Incidents" data-link-desc="從真實事故抽出防守壓力模型，補強藍隊判讀、演練與交接設計">7.B12 Defender Pressure From Real Incidents</a></li>
<li><a href="/blog/backend/07-security-data-protection/security-as-service-design-input/" data-link-title="7.21 資安如何成為服務設計輸入" data-link-desc="把資安需求前移到服務設計階段，建立可交接的設計輸入欄位與判讀路由">7.21 資安如何成為服務設計輸入</a></li>
<li><a href="/blog/backend/07-security-data-protection/security-material-library-for-engineering-simulation/" data-link-title="7.26 資安素材庫如何支援工程推演" data-link-desc="說明專業來源、案例、情境與控制模式如何組合成工程推演與章節回寫流程">7.26 資安素材庫如何支援工程推演</a></li>
</ul>
<h2 id="完稿判準">完稿判準</h2>
<p>完稿時要讓讀者能把事故教訓寫成回寫任務。輸出至少包含回寫層級、回寫欄位、產品路由、架構路由與知識路由。</p>
]]></content:encoded></item></channel></rss>