<?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>Consumer on Tarragon</title><link>https://tarrragon.github.io/blog/tags/consumer/</link><description>Recent content in Consumer on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 16 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/consumer/index.xml" rel="self" type="application/rss+xml"/><item><title>3.4 consumer 設計與去重</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/</guid><description>&lt;p>消費者設計（consumer design）的核心責任是把訊息投遞結果轉成可恢復的業務結果。queue 層提供 delivery 保證，consumer 層提供 processing 與 recovery 保證；三者對齊後，非同步流程才具備可預期性。&lt;/p>
&lt;h2 id="三層語意">三層語意&lt;/h2>
&lt;p>consumer 端需要同時處理三層語意：&lt;/p>
&lt;ol>
&lt;li>delivery semantics：訊息是否被成功投遞與確認，包含 ack/nack、retry、DLQ。&lt;/li>
&lt;li>processing semantics：業務副作用是否可承受重複、亂序與部分失敗。&lt;/li>
&lt;li>recovery semantics：故障後是否能重播、補償與回復到一致狀態。&lt;/li>
&lt;/ol>
&lt;p>這三層拆開後，才能看清問題落在哪一層。訊息送達不代表副作用完成；副作用完成不代表系統可恢復。&lt;/p>
&lt;h2 id="consumer-grouppartition-與順序責任">consumer group、partition 與順序責任&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition&lt;/a> 定義了並行與順序邊界。順序要求高的流程要把同一鍵值固定在同一 partition；吞吐優先的流程可提高 partition 數並分散處理。&lt;/p>
&lt;p>分區策略會直接影響恢復成本。分區鍵混亂時，重播與補償很難限定範圍，事故期間容易擴大影響面。&lt;/p>
&lt;h2 id="checkpointoffset-與-idempotency">checkpoint、offset 與 idempotency&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間處理流程如何記錄可恢復進度">checkpoint&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset&lt;/a> 的責任是標記「處理到哪裡」，不是「業務一定完成」。寫 checkpoint 的時機要晚於副作用提交，避免進度前移導致資料遺漏。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency&lt;/a> key 的責任是讓重試與重播可重入。付款、發票、通知、庫存變更都需要明確冪等鍵與去重儲存策略，讓「至少一次投遞」不會變成「多次業務結果」。&lt;/p>
&lt;h2 id="replay-safety">replay safety&lt;/h2>
&lt;p>replay safety 的核心是先定義可重播範圍，再定義副作用控制。常見做法包含：&lt;/p>
&lt;ol>
&lt;li>限定 replay window，避免一次重播跨越多個版本邊界。&lt;/li>
&lt;li>將副作用拆成可比對與可補償動作，保留對帳路徑。&lt;/li>
&lt;li>對 replay 期間的下游壓力設置節流與停損條件。&lt;/li>
&lt;/ol>
&lt;p>poison message 要獨立隔離。持續重試同一壞訊息會壓垮整體吞吐，穩定做法是送入 &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;/p>
&lt;h2 id="queue-語意誤配是-broker-遷移最常見的失敗模式">Queue 語意誤配是 broker 遷移最常見的失敗模式&lt;/h2>
&lt;p>Broker 遷移失敗的根因通常是 &lt;em>consumer 對舊 broker 行為的隱式依賴&lt;/em>、不是 broker 本身效能。表面上訊息仍被送達、但業務資料開始出現重複扣款、重複寄信、狀態漏更新。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/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 反例：Queue Semantics Mismatch Cutover&lt;/a> — case 揭露切換後語意誤配三個方向：consumer 依賴特定 offset 行為、依賴特定重試節奏、依賴特定 idempotency 行為。失敗重播時、新系統即使提供相近 delivery semantics、結果可能不同。語意誤配會沿著下游資料寫入擴散、難以靠 queue depth 判斷。&lt;/p>
&lt;p>&lt;strong>典型誤配場景&lt;/strong>（基於通用 broker 行為知識展開、非 3.C9 case 原文具體列舉）：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>At-least-once 假設變成 exactly-once 依賴&lt;/strong>：consumer 假設 broker 僅送一次、靠記憶單次處理；新 broker 重送同一 message、consumer 處理兩次&lt;/li>
&lt;li>&lt;strong>Offset 跳號處理差異&lt;/strong>：舊系統重啟後 offset 從特定位置開始、新系統可能從 latest / earliest 不同位置開始&lt;/li>
&lt;li>&lt;strong>Consumer group rebalance 行為差異&lt;/strong>：rebalance 期間舊系統會 pause 處理、新系統可能繼續處理、產生並發寫入衝突&lt;/li>
&lt;li>&lt;strong>DLQ retry 節奏差異&lt;/strong>：舊系統 DLQ message 預設不重試、新系統可能自動重試、製造重複副作用&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>回退判讀&lt;/strong>：回退前要先確認哪一段資料已經被新語意處理過。直接切回舊 broker 可能讓同一批事件再次被處理。穩定做法是先凍結新 consumer、保留 offset 對照與 replay 範圍、再決定補償或重播。&lt;/p>
&lt;p>詳細處理 / 恢復語意分層見 &lt;a href="https://tarrragon.github.io/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&lt;/a>。規模差異判讀（小 / 中 / 大型服務的 job queue 治理重點）見 &lt;a href="https://tarrragon.github.io/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&lt;/a> — 中型服務常見問題是 lag/DLQ 長期累積、需具備定向 replay 能力、否則退回全 topic 重播會放大下游壓力。&lt;/p></description><content:encoded><![CDATA[<p>消費者設計（consumer design）的核心責任是把訊息投遞結果轉成可恢復的業務結果。queue 層提供 delivery 保證，consumer 層提供 processing 與 recovery 保證；三者對齊後，非同步流程才具備可預期性。</p>
<h2 id="三層語意">三層語意</h2>
<p>consumer 端需要同時處理三層語意：</p>
<ol>
<li>delivery semantics：訊息是否被成功投遞與確認，包含 ack/nack、retry、DLQ。</li>
<li>processing semantics：業務副作用是否可承受重複、亂序與部分失敗。</li>
<li>recovery semantics：故障後是否能重播、補償與回復到一致狀態。</li>
</ol>
<p>這三層拆開後，才能看清問題落在哪一層。訊息送達不代表副作用完成；副作用完成不代表系統可恢復。</p>
<h2 id="consumer-grouppartition-與順序責任">consumer group、partition 與順序責任</h2>
<p><a href="/blog/backend/knowledge-cards/consumer-group/" data-link-title="Consumer Group" data-link-desc="說明一組 consumer 如何共同分攤 stream 或 topic 的處理責任">consumer group</a> 與 <a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition</a> 定義了並行與順序邊界。順序要求高的流程要把同一鍵值固定在同一 partition；吞吐優先的流程可提高 partition 數並分散處理。</p>
<p>分區策略會直接影響恢復成本。分區鍵混亂時，重播與補償很難限定範圍，事故期間容易擴大影響面。</p>
<h2 id="checkpointoffset-與-idempotency">checkpoint、offset 與 idempotency</h2>
<p><a href="/blog/backend/knowledge-cards/checkpoint/" data-link-title="Checkpoint" data-link-desc="說明長時間處理流程如何記錄可恢復進度">checkpoint</a> 與 <a href="/blog/backend/knowledge-cards/offset/" data-link-title="Offset" data-link-desc="說明 consumer 在事件流中的讀取位置與重放基準">offset</a> 的責任是標記「處理到哪裡」，不是「業務一定完成」。寫 checkpoint 的時機要晚於副作用提交，避免進度前移導致資料遺漏。</p>
<p><a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> key 的責任是讓重試與重播可重入。付款、發票、通知、庫存變更都需要明確冪等鍵與去重儲存策略，讓「至少一次投遞」不會變成「多次業務結果」。</p>
<h2 id="replay-safety">replay safety</h2>
<p>replay safety 的核心是先定義可重播範圍，再定義副作用控制。常見做法包含：</p>
<ol>
<li>限定 replay window，避免一次重播跨越多個版本邊界。</li>
<li>將副作用拆成可比對與可補償動作，保留對帳路徑。</li>
<li>對 replay 期間的下游壓力設置節流與停損條件。</li>
</ol>
<p>poison message 要獨立隔離。持續重試同一壞訊息會壓垮整體吞吐，穩定做法是送入 <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>，再走診斷與修復流程。</p>
<h2 id="queue-語意誤配是-broker-遷移最常見的失敗模式">Queue 語意誤配是 broker 遷移最常見的失敗模式</h2>
<p>Broker 遷移失敗的根因通常是 <em>consumer 對舊 broker 行為的隱式依賴</em>、不是 broker 本身效能。表面上訊息仍被送達、但業務資料開始出現重複扣款、重複寄信、狀態漏更新。</p>
<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 反例：Queue Semantics Mismatch Cutover</a> — case 揭露切換後語意誤配三個方向：consumer 依賴特定 offset 行為、依賴特定重試節奏、依賴特定 idempotency 行為。失敗重播時、新系統即使提供相近 delivery semantics、結果可能不同。語意誤配會沿著下游資料寫入擴散、難以靠 queue depth 判斷。</p>
<p><strong>典型誤配場景</strong>（基於通用 broker 行為知識展開、非 3.C9 case 原文具體列舉）：</p>
<ul>
<li><strong>At-least-once 假設變成 exactly-once 依賴</strong>：consumer 假設 broker 僅送一次、靠記憶單次處理；新 broker 重送同一 message、consumer 處理兩次</li>
<li><strong>Offset 跳號處理差異</strong>：舊系統重啟後 offset 從特定位置開始、新系統可能從 latest / earliest 不同位置開始</li>
<li><strong>Consumer group rebalance 行為差異</strong>：rebalance 期間舊系統會 pause 處理、新系統可能繼續處理、產生並發寫入衝突</li>
<li><strong>DLQ retry 節奏差異</strong>：舊系統 DLQ message 預設不重試、新系統可能自動重試、製造重複副作用</li>
</ul>
<p><strong>回退判讀</strong>：回退前要先確認哪一段資料已經被新語意處理過。直接切回舊 broker 可能讓同一批事件再次被處理。穩定做法是先凍結新 consumer、保留 offset 對照與 replay 範圍、再決定補償或重播。</p>
<p>詳細處理 / 恢復語意分層見 <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>。規模差異判讀（小 / 中 / 大型服務的 job queue 治理重點）見 <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> — 中型服務常見問題是 lag/DLQ 長期累積、需具備定向 replay 能力、否則退回全 topic 重播會放大下游壓力。</p>
<h2 id="三個工程議題要一起設計">三個工程議題要一起設計</h2>
<p><code>Consumer idempotency</code> + <code>重播流程</code> + <code>下游承載能力</code> 三件事是 consumer design 的鐵三角、需同步落地。缺一個會在規模化時暴露成事故：</p>
<ul>
<li><strong>Consumer idempotency 不完整</strong>：DLQ replay 後產生重複副作用、即使 broker 切換成功、業務帳本仍然錯亂</li>
<li><strong>重播流程不完整</strong>：事故當下需具備定向 replay 能力、否則退回全 topic 重播會放大下游壓力</li>
<li><strong>下游承載能力不足</strong>：consumer 跟 broker 都健康、但下游 DB / API 撐不住 replay 速率、形成新事故</li>
</ul>
<p>Job queue 的拓樸分工是另一個獨立議題、跟鐵三角互補但不重疊 — 詳見 <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 Job queue 拓樸分工</a>、主寫 Slack Kafka + Redis 案例。consumer 內部三件事要做好之外、不同類工作（高吞吐 / 即時 / 持久）也應專注單一目標、其他目標拆到對應路徑。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a> 持續上升</td>
          <td>consumer 吞吐低於輸入速率</td>
          <td>提升併發、拆分 partition、檢查下游瓶頸</td>
      </tr>
      <tr>
          <td>retry count 上升且成功率下降</td>
          <td>錯誤已從暫時性轉為系統性</td>
          <td>啟動降級、切換路由、保留重播窗口</td>
      </tr>
      <tr>
          <td>duplicate side effect 增加</td>
          <td>冪等鍵或去重流程失效</td>
          <td>修正 idempotency store、暫停高風險副作用</td>
      </tr>
      <tr>
          <td>DLQ 量快速增加</td>
          <td>payload 或版本相容性問題集中爆發</td>
          <td>分批隔離、加 schema 檢查、修復後定向重播</td>
      </tr>
      <tr>
          <td>replay 期間下游 timeout 同步上升</td>
          <td>重播速率超出依賴容量</td>
          <td>節流 replay、分段回放、加 backpressure 控制</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 consumer 設計等同於「把 handler 寫完」，會漏掉恢復責任。consumer 的工程價值在於故障後仍可追蹤、可補償、可重播。</p>
<p>把 DLQ 當成終點，會讓問題在下次事件再出現。DLQ 的責任是隔離與診斷入口，最終要回到 schema、邏輯或依賴治理。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>consumer 恢復語意可用 <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> 與 <a href="/blog/backend/03-message-queue/cases/linkedin-topicgc-kafka-governance/" data-link-title="3.C3 LinkedIn：TopicGC 與 Kafka 治理轉換" data-link-desc="Kafka topic 從手動治理轉自動治理對叢集的影響。">3.C3 LinkedIn：TopicGC</a> 對照回寫。先判讀問題是 idempotency 失效、checkpoint 前移，還是 replay 邊界失控，再對應本章的 processing/recovery 段落。
這組案例主要支撐的是「處理恢復語意」判讀，不直接支撐 deployment drain 或 cache eviction；若根因在切流順序或快取容量，應轉到 5.3 或 2.3。</p>
<p>若重播成功但業務狀態仍不一致，先補副作用補償與對帳路徑，並把決策證據同步到 <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>
<h2 id="跨模組路由">跨模組路由</h2>
<p>consumer 設計是 01/03/04/06/08 的交界點。</p>
<ol>
<li>與 03 內部的交接：processing/recovery 語意完整定義在 <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>；event contract 跟 replay boundary 在 <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</a>；規模差異判讀跟 job queue 拓樸分工在 <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</a>。</li>
<li>與 01 的交接：交易與發布一致性回到 <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> 與 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction boundary</a>。</li>
<li>與 04 的交接：lag、retry、DLQ、duplicate 指標進入 <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>。</li>
<li>與 06 的交接：重試與重播驗證進入 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 Idempotency 與 Replay 驗證</a>。</li>
<li>與 08 的交接：pause consumer、replay 決策與補償判斷記錄到 <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>要看 processing / recovery 三層語意完整定義、接著讀 <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/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker 基礎與投遞模型</a> 與 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a>。要看錯誤切換案例，接著讀 <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><item><title>NATS core 到 JetStream：fire-and-forget 在哪裡不夠、跨過去要付什麼</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/jetstream-durability-consumer/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/jetstream-durability-consumer/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS&lt;/a> overview 的 implementation-layer deep article、定位在「要不要從 core NATS 跨進 JetStream」的決策入口。選型層（NATS vs Kafka / RabbitMQ）見 overview；本文只處理 core 與 JetStream 的邊界與基本 consumer 設定。決定採用 JetStream 後的完整實作（stream / consumer 每個旋鈕、跨區拓樸、多租戶）見 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/nats/jetstream-supercluster-design/" data-link-title="NATS JetStream 設計與 supercluster / leaf node：stream、consumer、跨區拓樸與多租戶" data-link-desc="NATS JetStream 的 implementation-layer deep article：stream 設計（storage / retention / discard / 容量上限）、consumer 設計（pull/push、explicit ack、AckWait、MaxDeliver、replay）、Cluster Raft / Supercluster gateway / Leaf node edge 三層拓樸、subject-based ACL 多租戶；含 4 個 production 故障演練（AckWait 太短重投、discard policy 選錯丟訊息、leaf node 斷線重連、stream replica 失去 quorum）。">JetStream 設計與 supercluster / leaf node&lt;/a>。JetStream 實機驗證於 nats:latest（-js）、最後檢查日 2026-06-16；機制以 &lt;a href="https://docs.nats.io/nats-concepts/jetstream">NATS JetStream 官方文件&lt;/a> 為準。&lt;/p>&lt;/blockquote>
&lt;h2 id="fire-and-forget-在-rolling-deploy-那一刻掉訊息">fire-and-forget 在 rolling deploy 那一刻掉訊息&lt;/h2>
&lt;p>Core NATS 的低延遲來自它什麼都不記——一則訊息發布出去，當下有訂閱者就送達、沒有就丟棄。沒有儲存、沒有 ack、沒有重送。這適合「即時但可丟」的場景（metrics、presence、即時通知）：訂閱者暫時離線錯過幾則無所謂，下一則馬上來。&lt;/p>
&lt;p>但這個設計有一條清楚的邊界。&lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/nats-clarifai-async-task-queue/" data-link-title="3.C38 Clarifai：NATS Streaming ML 平台非同步任務" data-link-desc="Clarifai custom model 訓練、rolling deploy 掉訊息、改 NATS Streaming queue group、3 週遷移 1 服務、5 月 5 服務、每日 100k&amp;#43; 訊息 100% uptime。">Clarifai 用 NATS 跑 ML 模型訓練的非同步任務&lt;/a>，任務從幾秒到幾分鐘，原本同步呼叫——結果每次 rolling deployment（pod 輪流重啟）就掉訊息：訊息發布的瞬間目標 worker 正在重啟，core NATS 找不到訂閱者就丟了。他們的解法是改用 NATS（當時是 NATS Streaming、JetStream 的前身）的 &lt;strong>at-least-once delivery + redelivery + queue group&lt;/strong>，每日 100k+ 訊息、達成 100% uptime。這個案例揭露的邊界是——&lt;strong>ML 長尾任務不能容忍 rolling deploy 掉訊息，core NATS 的 fire-and-forget 到此為止，要跨進 JetStream。&lt;/strong>&lt;/p>
&lt;p>JetStream 在 core NATS 之上加了一層持久化的 stream + 可重送的 consumer。本文處理這條邊界：什麼時候 core 夠用、什麼時候要 JetStream、跨過去的 consumer 模型怎麼設才不會丟訊息或重投風暴。&lt;/p>
&lt;h2 id="核心概念stream-與-consumer-的求值模型">核心概念：stream 與 consumer 的求值模型&lt;/h2>
&lt;p>JetStream 把「訊息儲存」跟「消費進度」拆成兩個獨立物件——stream（存什麼、留多久）跟 consumer（誰讀、怎麼 ack）。理解 JetStream 就是理解這兩者。&lt;/p>
&lt;p>&lt;strong>stream 決定訊息怎麼被儲存與保留&lt;/strong>。一個 stream 綁定一組 subject、把符合的訊息持久化。三個關鍵維度：storage（&lt;code>file&lt;/code> 持久 / &lt;code>memory&lt;/code> 重啟即失）、retention（&lt;code>limits&lt;/code> 依大小/時間/數量保留、&lt;code>workqueue&lt;/code> 消費後即刪、&lt;code>interest&lt;/code> 有訂閱者才留）、limits（max-msgs / max-bytes / max-age）。retention 選錯是常見陷阱——&lt;code>workqueue&lt;/code> 是「每則訊息只被一個 consumer 消費一次就刪」，&lt;code>limits&lt;/code> 是「保留著、多個 consumer 各自讀」。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a> overview 的 implementation-layer deep article、定位在「要不要從 core NATS 跨進 JetStream」的決策入口。選型層（NATS vs Kafka / RabbitMQ）見 overview；本文只處理 core 與 JetStream 的邊界與基本 consumer 設定。決定採用 JetStream 後的完整實作（stream / consumer 每個旋鈕、跨區拓樸、多租戶）見 <a href="/blog/backend/03-message-queue/vendors/nats/jetstream-supercluster-design/" data-link-title="NATS JetStream 設計與 supercluster / leaf node：stream、consumer、跨區拓樸與多租戶" data-link-desc="NATS JetStream 的 implementation-layer deep article：stream 設計（storage / retention / discard / 容量上限）、consumer 設計（pull/push、explicit ack、AckWait、MaxDeliver、replay）、Cluster Raft / Supercluster gateway / Leaf node edge 三層拓樸、subject-based ACL 多租戶；含 4 個 production 故障演練（AckWait 太短重投、discard policy 選錯丟訊息、leaf node 斷線重連、stream replica 失去 quorum）。">JetStream 設計與 supercluster / leaf node</a>。JetStream 實機驗證於 nats:latest（-js）、最後檢查日 2026-06-16；機制以 <a href="https://docs.nats.io/nats-concepts/jetstream">NATS JetStream 官方文件</a> 為準。</p></blockquote>
<h2 id="fire-and-forget-在-rolling-deploy-那一刻掉訊息">fire-and-forget 在 rolling deploy 那一刻掉訊息</h2>
<p>Core NATS 的低延遲來自它什麼都不記——一則訊息發布出去，當下有訂閱者就送達、沒有就丟棄。沒有儲存、沒有 ack、沒有重送。這適合「即時但可丟」的場景（metrics、presence、即時通知）：訂閱者暫時離線錯過幾則無所謂，下一則馬上來。</p>
<p>但這個設計有一條清楚的邊界。<a href="/blog/backend/03-message-queue/cases/nats-clarifai-async-task-queue/" data-link-title="3.C38 Clarifai：NATS Streaming ML 平台非同步任務" data-link-desc="Clarifai custom model 訓練、rolling deploy 掉訊息、改 NATS Streaming queue group、3 週遷移 1 服務、5 月 5 服務、每日 100k&#43; 訊息 100% uptime。">Clarifai 用 NATS 跑 ML 模型訓練的非同步任務</a>，任務從幾秒到幾分鐘，原本同步呼叫——結果每次 rolling deployment（pod 輪流重啟）就掉訊息：訊息發布的瞬間目標 worker 正在重啟，core NATS 找不到訂閱者就丟了。他們的解法是改用 NATS（當時是 NATS Streaming、JetStream 的前身）的 <strong>at-least-once delivery + redelivery + queue group</strong>，每日 100k+ 訊息、達成 100% uptime。這個案例揭露的邊界是——<strong>ML 長尾任務不能容忍 rolling deploy 掉訊息，core NATS 的 fire-and-forget 到此為止，要跨進 JetStream。</strong></p>
<p>JetStream 在 core NATS 之上加了一層持久化的 stream + 可重送的 consumer。本文處理這條邊界：什麼時候 core 夠用、什麼時候要 JetStream、跨過去的 consumer 模型怎麼設才不會丟訊息或重投風暴。</p>
<h2 id="核心概念stream-與-consumer-的求值模型">核心概念：stream 與 consumer 的求值模型</h2>
<p>JetStream 把「訊息儲存」跟「消費進度」拆成兩個獨立物件——stream（存什麼、留多久）跟 consumer（誰讀、怎麼 ack）。理解 JetStream 就是理解這兩者。</p>
<p><strong>stream 決定訊息怎麼被儲存與保留</strong>。一個 stream 綁定一組 subject、把符合的訊息持久化。三個關鍵維度：storage（<code>file</code> 持久 / <code>memory</code> 重啟即失）、retention（<code>limits</code> 依大小/時間/數量保留、<code>workqueue</code> 消費後即刪、<code>interest</code> 有訂閱者才留）、limits（max-msgs / max-bytes / max-age）。retention 選錯是常見陷阱——<code>workqueue</code> 是「每則訊息只被一個 consumer 消費一次就刪」，<code>limits</code> 是「保留著、多個 consumer 各自讀」。</p>
<p><strong>consumer 是 stream 上的一個可重播視圖</strong>。同一個 stream 可以有多個 consumer，各自維護自己的消費位置。consumer 的關鍵屬性：</p>
<ul>
<li>push vs pull：push 由 server 主動推給訂閱者；pull 由 client 主動拉（<code>consumer next</code>），pull 對流量控制與 worker pool 更可控</li>
<li>durable vs ephemeral：durable consumer 的進度持久（重啟後從上次位置續讀），ephemeral 在 client 斷線後消失（進度丟失）</li>
<li>ack policy：<code>explicit</code>（每則都要 ack、at-least-once 的基礎）/ <code>all</code>（ack 一則等於 ack 之前所有）/ <code>none</code>（不需 ack、近似 fire-and-forget）</li>
<li>max_deliver + ack_wait：沒 ack 的訊息在 <code>ack_wait</code> 後重送，最多 <code>max_deliver</code> 次</li>
</ul>
<p><strong>at-least-once 來自「explicit ack + redelivery」</strong>。consumer 取出訊息、處理、明確 ack；沒 ack（處理失敗或 crash）的訊息在 ack_wait 逾時後重送。這就是 Clarifai 要的「rolling deploy 不丟訊息」——worker 重啟時沒 ack 的任務會被重送給其他 worker。</p>
<h2 id="配置durable-pull-consumer實機驗證">配置：durable pull consumer（實機驗證）</h2>





<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"><span class="c1"># 啟動 JetStream（server 加 -js）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># docker run -d --name nats nats:latest -js</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 1. 建 stream：file storage、limits retention</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">nats stream add ORDERS --subjects <span class="s2">&#34;orders.&gt;&#34;</span> --storage file --defaults
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">#   Subjects: orders.&gt;   Storage: File   Retention: Limits   Replicas: 1</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># 2. publish</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">nats pub orders.new <span class="s2">&#34;order-1&#34;</span>   <span class="c1"># Published 7 bytes to &#34;orders.new&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 3. stream info 確認持久化</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">nats stream info ORDERS
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1">#   Storage: File   Messages: 3   Bytes: 141 B   ← 訊息已落盤、consumer 重啟不丟</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># 4. durable pull consumer（explicit ack、可重送）</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">nats consumer add ORDERS workers --pull --ack explicit --deliver all --defaults
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1">#   Pull Mode: true   Ack Policy: Explicit</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># 5. 拉取消費（worker pool 多個實例共用同一 durable consumer = queue group 語意）</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">nats consumer next ORDERS workers --count <span class="m">3</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1">#   order-1  order-2  order-3</span></span></span></code></pre></div><p>實機驗證於 nats:latest（最後檢查日 2026-06-16）：file storage 的 stream 把訊息落盤（Messages: 3）、durable pull consumer 用 explicit ack 消費。多個 worker 連到同一個 durable pull consumer 形成 worker pool（訊息分給其中一個），這正是 Clarifai 的 queue group 模式。</p>
<p>判讀：</p>
<ul>
<li>worker pool 用同一個 durable pull consumer（共享進度、訊息分流），不是每個 worker 一個 consumer</li>
<li><code>--ack explicit</code> 是 at-least-once 的前提；處理成功才 ack</li>
<li>pull 模式比 push 對 worker pool 更可控（worker 按自己能力拉、不會被 push 淹）</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1用-core-nats-跑該持久的任務rolling-deploy-掉訊息">Case 1：用 core NATS 跑該持久的任務、rolling deploy 掉訊息</h3>
<p><strong>徵兆</strong>：平時正常，但每次部署（pod 輪流重啟）就有一批任務消失、沒有錯誤。</p>
<p><strong>根因</strong>：用 core NATS（fire-and-forget）跑需要可靠處理的任務。發布瞬間目標訂閱者正在重啟，core NATS 找不到訂閱者就丟棄——這是 core 的設計，不是故障。正是 <a href="/blog/backend/03-message-queue/cases/nats-clarifai-async-task-queue/" data-link-title="3.C38 Clarifai：NATS Streaming ML 平台非同步任務" data-link-desc="Clarifai custom model 訓練、rolling deploy 掉訊息、改 NATS Streaming queue group、3 週遷移 1 服務、5 月 5 服務、每日 100k&#43; 訊息 100% uptime。">Clarifai 的原始問題</a>。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>需要不丟的任務用 JetStream（持久 stream + durable consumer + explicit ack）</li>
<li>訊息落盤後 consumer 重啟從上次位置續讀，rolling deploy 不丟</li>
<li>釐清邊界：可丟的即時資料（metrics / presence）留 core NATS、不可丟的跨 JetStream</li>
<li>不要用 core NATS 當任務隊列——它沒有持久化與重送</li>
</ol>
<h3 id="case-2ephemeral-consumer-斷線消費進度全丟">Case 2：ephemeral consumer 斷線、消費進度全丟</h3>
<p><strong>徵兆</strong>：consumer 重連後從頭重讀整個 stream、或漏掉斷線期間的訊息，進度不連續。</p>
<p><strong>根因</strong>：用了 ephemeral consumer——它的進度不持久，client 斷線後 consumer 本身消失。重連是建一個全新 consumer，從 <code>deliver</code> policy 的起點開始（all 從頭、new 只看新的），不接續之前的進度。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>需要跨重啟接續的用 durable consumer（具名、進度持久）</li>
<li>ephemeral 只適合臨時、一次性的讀取（debug、一次性掃描）</li>
<li>worker pool 一定用 durable（多 worker 共享持久進度）</li>
<li>確認 <code>deliver</code> policy（all / new / last）符合預期的起讀位置</li>
</ol>
<h3 id="case-3ack_wait-太短處理還沒完就重送風暴">Case 3：ack_wait 太短、處理還沒完就重送風暴</h3>
<p><strong>徵兆</strong>：長任務還在處理中就被重送給另一個 worker，同一任務被多個 worker 重複執行，負載放大。</p>
<p><strong>根因</strong>：<code>ack_wait</code>（等 ack 的逾時）設得比任務處理時間短。JetStream 以為訊息處理失敗（沒在 ack_wait 內 ack），重送給別人——但其實第一個 worker 還在跑。ML 長尾任務（幾秒到幾分鐘）特別容易踩。</p>
<p><strong>修法（本文層級的判讀）</strong>：ack_wait 必須涵蓋任務的 p99 處理時間，否則長任務會在處理中被重送。設值方法（量測 p99、長任務用 in-progress ack 延長 deadline、消費端冪等兜底）與實機重現（AckWait 設 1s 觀察 tries 1→2、Redelivered 計數）在 <a href="/blog/backend/03-message-queue/vendors/nats/jetstream-supercluster-design/" data-link-title="NATS JetStream 設計與 supercluster / leaf node：stream、consumer、跨區拓樸與多租戶" data-link-desc="NATS JetStream 的 implementation-layer deep article：stream 設計（storage / retention / discard / 容量上限）、consumer 設計（pull/push、explicit ack、AckWait、MaxDeliver、replay）、Cluster Raft / Supercluster gateway / Leaf node edge 三層拓樸、subject-based ACL 多租戶；含 4 個 production 故障演練（AckWait 太短重投、discard policy 選錯丟訊息、leaf node 斷線重連、stream replica 失去 quorum）。">JetStream 設計與 supercluster/leaf node</a> 的故障演練有完整步驟，採用 JetStream 後依該篇落地。</p>
<h3 id="case-4retention-選-workqueue-但想多-consumer-fanout">Case 4：retention 選 workqueue 但想多 consumer fanout</h3>
<p><strong>徵兆</strong>：想讓多個獨立服務各自消費同一 stream，但發現訊息被一個消費掉就消失、其他服務讀不到。</p>
<p><strong>根因</strong>：stream retention 設成 <code>workqueue</code>——每則訊息只被消費一次就從 stream 刪除（隊列語意）。它不適合 fanout（多個 consumer 各自要完整一份）。fanout 要 <code>limits</code> 或 <code>interest</code> retention。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>fanout（多服務各讀一份）用 <code>limits</code> retention（訊息保留、多 consumer 各自 offset）</li>
<li>單一 worker pool 競爭消費用 <code>workqueue</code>（消費即刪、省空間）</li>
<li>釐清需求：競爭消費（worker pool）vs 廣播消費（fanout）對應不同 retention</li>
<li>Clarifai 用「3 個獨立 NATS 實例做 fanout 隔離」是另一種 fanout 做法，按隔離需求選</li>
</ol>
<h3 id="case-5memory-storage-的-stream-重啟全失">Case 5：memory storage 的 stream 重啟全失</h3>
<p><strong>徵兆</strong>：broker 重啟後 stream 裡的訊息全沒了，consumer 從空的開始。</p>
<p><strong>根因</strong>：stream storage 設成 <code>memory</code>——快但不持久，broker 重啟即失。誤把它當持久 stream 用。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>需要持久的 stream 用 <code>file</code> storage（落盤、重啟不丟，實機驗證過）</li>
<li><code>memory</code> 只適合「快取式、可重建」的 stream（如即時聚合的中間狀態）</li>
<li>要更高可靠性加 <code>replicas</code>（JetStream 用 Raft 跨節點複製 stream）</li>
<li>容量規劃時 file storage 的磁碟與 memory 的 RAM 是不同維度</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>JetStream 的容量判讀：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>stream storage 用量</td>
          <td>在 max-bytes / max-age 內</td>
          <td>接近上限 → 訊息被 discard、調 limits 或加容量</td>
      </tr>
      <tr>
          <td>redelivery 次數</td>
          <td>低（多數一次 ack 成功）</td>
          <td>高 → ack_wait 太短或處理卡住</td>
      </tr>
      <tr>
          <td>consumer pending</td>
          <td>可消化</td>
          <td>持續堆高 → consumer 跟不上 producer</td>
      </tr>
      <tr>
          <td>ack_wait vs 處理時間</td>
          <td>ack_wait &gt; p99 處理時間</td>
          <td>反了 → 重送風暴</td>
      </tr>
      <tr>
          <td>storage 型別</td>
          <td>持久需求用 file</td>
          <td>誤用 memory → 重啟丟訊息</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>可丟的即時資料</strong>：不需要 JetStream 的持久化開銷，用 core NATS（更快更輕）。</li>
<li><strong>超大吞吐 + 長期保留 + 複雜 replay</strong>：JetStream 適合中等規模可靠 messaging；超大規模 event streaming + 長期保留走 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a>（log-based、生態成熟）。</li>
<li><strong>複雜 routing / 任務隊列語意</strong>：JetStream 的 subject 是樹狀，複雜 routing + DLQ 拓樸用 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a> 更直接。</li>
<li><strong>不想自管</strong>：NATS 的 managed 選項（Synadia Cloud）或其他 managed broker。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>JetStream 的邊界判斷是 NATS 使用的核心，它跟其他議題交織：</p>
<ul>
<li><strong>跟 <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></strong>：push/pull、durable/ephemeral、ack policy 是 consumer 設計的具體選項。</li>
<li><strong>跟 <a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a></strong>：JetStream 的 file storage stream 是 NATS 的 durable queue 實現。</li>
<li><strong>跟 <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency / replay</a></strong>：at-least-once + redelivery 要求消費冪等，否則重送造成重複副作用。</li>
<li><strong>跟 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/dlq-retry-escalation/" data-link-title="RabbitMQ DLQ 與分層 retry：別把失敗訊息 requeue 回隊首" data-link-desc="RabbitMQ 處理失敗訊息最常見的錯是直接 requeue 回原隊列——它回到隊首、反覆失敗、把後面的訊息全卡住（head-of-line blocking）。正解是用 dead-letter exchange &#43; TTL 組出 work → delay → DLQ 的分層 escalation。本文展開 DLX 求值模型、實機驗證的三層拓樸、5 個把 retry 寫成無限迴圈與隊列阻塞的 production 踩坑，以及 retry 拓樸的容量邊界">RabbitMQ DLQ deep article</a></strong>：max_deliver 達上限後的處理對應 RabbitMQ 的 DLQ，兩者都是「重試上限後往哪去」的問題。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a></li>
<li>對照 vendor：<a href="/blog/backend/03-message-queue/vendors/rabbitmq/dlq-retry-escalation/" data-link-title="RabbitMQ DLQ 與分層 retry：別把失敗訊息 requeue 回隊首" data-link-desc="RabbitMQ 處理失敗訊息最常見的錯是直接 requeue 回原隊列——它回到隊首、反覆失敗、把後面的訊息全卡住（head-of-line blocking）。正解是用 dead-letter exchange &#43; TTL 組出 work → delay → DLQ 的分層 escalation。本文展開 DLX 求值模型、實機驗證的三層拓樸、5 個把 retry 寫成無限迴圈與隊列阻塞的 production 踩坑，以及 retry 拓樸的容量邊界">RabbitMQ DLQ 與分層 retry</a>、<a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a></li>
<li>對應案例：<a href="/blog/backend/03-message-queue/cases/nats-clarifai-async-task-queue/" data-link-title="3.C38 Clarifai：NATS Streaming ML 平台非同步任務" data-link-desc="Clarifai custom model 訓練、rolling deploy 掉訊息、改 NATS Streaming queue group、3 週遷移 1 服務、5 月 5 服務、每日 100k&#43; 訊息 100% uptime。">3.C38 Clarifai NATS ML 非同步任務</a></li>
<li>上游概念：<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>、<a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a></li>
</ul>
]]></content:encoded></item></channel></rss>