<?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>Aws-Sqs on Tarragon</title><link>https://tarrragon.github.io/blog/tags/aws-sqs/</link><description>Recent content in Aws-Sqs 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/aws-sqs/index.xml" rel="self" type="application/rss+xml"/><item><title>AWS SQS → Google Pub/Sub：queue 模型搬到 topic + subscription 模型的跨雲遷移</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/migrate-to-google-pubsub/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/migrate-to-google-pubsub/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">AWS SQS&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/google-pubsub/" data-link-title="Google Cloud Pub/Sub" data-link-desc="GCP managed pub/sub、global routing、push/pull">Google Pub/Sub&lt;/a>。這是一個 &lt;em>跨雲 managed-to-managed&lt;/em> 遷移：兩端都是 cloud-managed、運維負擔都低、但 &lt;em>資料拓樸&lt;/em> 跟 &lt;em>消費抽象&lt;/em> 不同 — SQS 是 region-scoped 的單一 pull queue、Pub/Sub 是 global topic + 多個 first-class subscription。主結構走 operational redesign hybrid（Type C）、額外為 components / data topology 兩個高維度抽獨立段。&lt;/p>&lt;/blockquote>
&lt;h2 id="sqs-跟-pubsub-不是同一種訊息抽象">SQS 跟 Pub/Sub 不是同一種訊息抽象&lt;/h2>
&lt;p>SQS 跟 Pub/Sub 都是 cloud-managed 非同步訊息服務、都解「解耦 producer / consumer、不自管 broker」這個問題、application 程式碼裡都是「發訊息、收訊息、處理完確認」的形狀。從這層看兩者可互換、遷移像是換 SDK。&lt;/p>
&lt;p>差別在 &lt;em>消費抽象&lt;/em> 跟 &lt;em>資料拓樸&lt;/em>。SQS 的核心實體是 queue：一條 region-scoped 的訊息隊列、訊息被一個 consumer 領走（in-flight）就對其他 consumer 隱形、處理完 DeleteMessage 就消失。要讓同一筆事件送給多個下游、SQS 端的做法是在 SNS 前面 fan-out、再各接一條 SQS queue。Pub/Sub 的核心實體是 topic + subscription 兩層：topic 收訊息、subscription 是 &lt;em>first-class&lt;/em> 的消費端點、一個 topic 可掛 N 個 subscription、每個 subscription 各自維護消費進度、fan-out 是模型內建而不是外接。&lt;/p>
&lt;p>這個差別決定了遷移的形狀。如果原系統只是「一條 queue、一群 worker 競爭領取」、那 Pub/Sub 端是「一個 topic、一個 pull subscription」、對位乾淨、application 改動小。如果原系統靠 SNS-to-many-SQS 做扇出、那 Pub/Sub 端是「一個 topic、多個 subscription」、整個 fan-out 拓樸要重畫、這不是換 SDK、是重設計訊息流。先判斷自己屬於哪一種、再決定 playbook 的重量。&lt;/p>
&lt;h2 id="為什麼會跨雲遷這條路徑">為什麼會跨雲遷這條路徑&lt;/h2>
&lt;p>跨雲從 SQS 遷到 Pub/Sub 的 driver 跟同雲 vendor 切換不同、通常不是「Pub/Sub 比 SQS 好」、而是 &lt;em>整體 workload 的重心移到 GCP&lt;/em>：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>資料平台落在 GCP&lt;/strong>：下游分析走 BigQuery、streaming 走 Dataflow、容器跑 Cloud Run。事件如果留在 AWS、每筆都要跨雲搬到 GCP 才能進 BigQuery、跨雲 egress 費用跟延遲都是常態成本。把訊息層也移到 Pub/Sub、事件可以用 BigQuery subscription 直接落地、省掉中間搬運。&lt;/li>
&lt;li>&lt;strong>需要 global topic、不想管 region&lt;/strong>：SQS queue 綁 region、跨 region 要自己複製或在前面架路由。Pub/Sub topic 沒有 region 概念、publish 進去全球可訂閱、多區域服務的事件分發是 first-class。&lt;/li>
&lt;li>&lt;strong>fan-out 從外接變內建&lt;/strong>：原本靠 SNS + 多條 SQS 維護的扇出拓樸、在 Pub/Sub 是「一個 topic 掛多個 subscription」、少一層 SNS、扇出關係在 subscription 列表一覽。&lt;/li>
&lt;/ol>
&lt;p>這三條 driver 都假設 &lt;em>重心已經或即將在 GCP&lt;/em>。如果系統長期紮根 AWS、只為了「換個 queue」跨雲、會付出跨雲 IAM 重對位、雙雲計費、跨雲網路延遲的代價、ROI 通常不成立。遷移前先確認 driver 是 workload 重心轉移、不是單純偏好。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">AWS SQS</a> 跟 <a href="/blog/backend/03-message-queue/vendors/google-pubsub/" data-link-title="Google Cloud Pub/Sub" data-link-desc="GCP managed pub/sub、global routing、push/pull">Google Pub/Sub</a>。這是一個 <em>跨雲 managed-to-managed</em> 遷移：兩端都是 cloud-managed、運維負擔都低、但 <em>資料拓樸</em> 跟 <em>消費抽象</em> 不同 — SQS 是 region-scoped 的單一 pull queue、Pub/Sub 是 global topic + 多個 first-class subscription。主結構走 operational redesign hybrid（Type C）、額外為 components / data topology 兩個高維度抽獨立段。</p></blockquote>
<h2 id="sqs-跟-pubsub-不是同一種訊息抽象">SQS 跟 Pub/Sub 不是同一種訊息抽象</h2>
<p>SQS 跟 Pub/Sub 都是 cloud-managed 非同步訊息服務、都解「解耦 producer / consumer、不自管 broker」這個問題、application 程式碼裡都是「發訊息、收訊息、處理完確認」的形狀。從這層看兩者可互換、遷移像是換 SDK。</p>
<p>差別在 <em>消費抽象</em> 跟 <em>資料拓樸</em>。SQS 的核心實體是 queue：一條 region-scoped 的訊息隊列、訊息被一個 consumer 領走（in-flight）就對其他 consumer 隱形、處理完 DeleteMessage 就消失。要讓同一筆事件送給多個下游、SQS 端的做法是在 SNS 前面 fan-out、再各接一條 SQS queue。Pub/Sub 的核心實體是 topic + subscription 兩層：topic 收訊息、subscription 是 <em>first-class</em> 的消費端點、一個 topic 可掛 N 個 subscription、每個 subscription 各自維護消費進度、fan-out 是模型內建而不是外接。</p>
<p>這個差別決定了遷移的形狀。如果原系統只是「一條 queue、一群 worker 競爭領取」、那 Pub/Sub 端是「一個 topic、一個 pull subscription」、對位乾淨、application 改動小。如果原系統靠 SNS-to-many-SQS 做扇出、那 Pub/Sub 端是「一個 topic、多個 subscription」、整個 fan-out 拓樸要重畫、這不是換 SDK、是重設計訊息流。先判斷自己屬於哪一種、再決定 playbook 的重量。</p>
<h2 id="為什麼會跨雲遷這條路徑">為什麼會跨雲遷這條路徑</h2>
<p>跨雲從 SQS 遷到 Pub/Sub 的 driver 跟同雲 vendor 切換不同、通常不是「Pub/Sub 比 SQS 好」、而是 <em>整體 workload 的重心移到 GCP</em>：</p>
<ol>
<li><strong>資料平台落在 GCP</strong>：下游分析走 BigQuery、streaming 走 Dataflow、容器跑 Cloud Run。事件如果留在 AWS、每筆都要跨雲搬到 GCP 才能進 BigQuery、跨雲 egress 費用跟延遲都是常態成本。把訊息層也移到 Pub/Sub、事件可以用 BigQuery subscription 直接落地、省掉中間搬運。</li>
<li><strong>需要 global topic、不想管 region</strong>：SQS queue 綁 region、跨 region 要自己複製或在前面架路由。Pub/Sub topic 沒有 region 概念、publish 進去全球可訂閱、多區域服務的事件分發是 first-class。</li>
<li><strong>fan-out 從外接變內建</strong>：原本靠 SNS + 多條 SQS 維護的扇出拓樸、在 Pub/Sub 是「一個 topic 掛多個 subscription」、少一層 SNS、扇出關係在 subscription 列表一覽。</li>
</ol>
<p>這三條 driver 都假設 <em>重心已經或即將在 GCP</em>。如果系統長期紮根 AWS、只為了「換個 queue」跨雲、會付出跨雲 IAM 重對位、雙雲計費、跨雲網路延遲的代價、ROI 通常不成立。遷移前先確認 driver 是 workload 重心轉移、不是單純偏好。</p>
<h2 id="結構為什麼是-operational-hybrid-加兩個高維度獨立段">結構為什麼是 operational hybrid 加兩個高維度獨立段</h2>
<p>寫這篇前先跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">diff dimension audit</a>、6 維評級如下：</p>
<table>
  <thead>
      <tr>
          <th>Diff 維度</th>
          <th>評級</th>
          <th>SQS → Pub/Sub 的具體差異</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>Medium</td>
          <td>都是「發 / 收 / 確認」、但 API 名詞與參數全換（QueueUrl → topic+subscription）</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>High</td>
          <td>IAM policy → Service Account、CloudWatch → Cloud Monitoring、redrive → DLT 重訂閱</td>
      </tr>
      <tr>
          <td>Abstraction</td>
          <td>Medium</td>
          <td>都是訊息服務、但 pull queue ↔ topic/subscription 的消費抽象不同</td>
      </tr>
      <tr>
          <td>Components（數量）</td>
          <td>High</td>
          <td>單一 queue ↔ topic + N subscription 兩層實體；SNS+SQS 扇出 ↔ topic 內建扇出</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>Medium</td>
          <td>SDK 換、ack / fan-out 邏輯改、但商業邏輯多數可保留</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>High</td>
          <td>region-scoped queue ↔ global topic；single-consumer ↔ multi-subscription fan-out</td>
      </tr>
  </tbody>
</table>
<p>主導維度是 <em>operational model</em>（跨雲身份與監控全換）、所以主結構走 Type C operational redesign hybrid。但 components 跟 data topology 也是 High — 不是把它們塞進 operational 段就能講清楚的、消費抽象從「一條 queue」變「topic + 多 subscription」是讀者最容易踩雷的地方。按 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration 方法論的 multi-axis 規則</a>、高維度抽成獨立段補充、不硬塞進單一 type 標籤。所以本篇結構是：operational 對位主軸 + 「消費抽象重設計」獨立段（components / topology 軸）+ 跨雲特有的 IAM 與網路段。</p>
<h2 id="operational-對位機制名詞換語意要逐一確認">Operational 對位：機制名詞換、語意要逐一確認</h2>
<p>跨雲遷移最容易出錯的環節、是 <em>找到語意相近的功能、卻假設行為一致</em>。SQS 跟 Pub/Sub 多數機制都有對位、但每一組都有行為差、找得到對應功能只是第一步。下表先給對照、後面逐項展開語意陷阱。</p>
<table>
  <thead>
      <tr>
          <th>SQS 機制</th>
          <th>Pub/Sub 對位</th>
          <th>語意是否等價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Visibility timeout</td>
          <td>Ack deadline</td>
          <td>近似、但上限與延長機制不同</td>
      </tr>
      <tr>
          <td>DeleteMessage</td>
          <td>Ack（acknowledge）</td>
          <td>近似、但 Pub/Sub 自動 extension 改變實際行為</td>
      </tr>
      <tr>
          <td>maxReceiveCount + DLQ + redrive</td>
          <td>Dead-letter topic + 重訂閱</td>
          <td>概念對應、DLT 是 topic 不是 queue、重處理方式不同</td>
      </tr>
      <tr>
          <td>Long polling（WaitTimeSeconds）</td>
          <td>Streaming pull</td>
          <td>不等價、streaming pull 是長連線串流、不是輪詢</td>
      </tr>
      <tr>
          <td>Message attributes</td>
          <td>Message attributes</td>
          <td>概念對應、型別與大小限制不同</td>
      </tr>
      <tr>
          <td>FIFO queue（MessageGroupId）</td>
          <td>Ordering key</td>
          <td>都給順序、但去重與吞吐取捨不同</td>
      </tr>
      <tr>
          <td>IAM policy + Queue policy</td>
          <td>IAM role + Service Account</td>
          <td>跨雲身份模型完全不同、不是改語法是重對位</td>
      </tr>
      <tr>
          <td>CloudWatch metric / alarm</td>
          <td>Cloud Monitoring metric / alert</td>
          <td>metric 名詞與語意不同、alarm 邏輯要重寫</td>
      </tr>
  </tbody>
</table>
<h3 id="visibility-timeout--ack-deadline">Visibility timeout → ack deadline</h3>
<p>Visibility timeout 跟 ack deadline 都回答同一個問題：consumer 領走訊息後、多久沒確認就視為失敗、把訊息重新投遞。語意對位成立、但兩端的數字與延長機制不同。</p>
<p>SQS visibility timeout 預設 30 秒、上限 12 小時、consumer 要延長就主動呼叫 ChangeMessageVisibility。Pub/Sub ack deadline 預設 10 秒、上限 600 秒（10 分鐘）、而且 client library 預設會 <em>自動</em> 在背景延長 deadline（lease management）。這個自動延長是最容易踩到的差異：在 SQS 端習慣「設一個夠長的 visibility timeout、處理完再 delete」、搬到 Pub/Sub 如果只把 ack deadline 設成 600 秒上限、卻沒意識到 client library 在背景幫忙延長、長任務的行為會跟預期不同；反過來、如果關掉自動延長又設了預設 10 秒、處理稍久就重投。對位的正確做法是先理解 client library 的 lease 行為、再決定 ack deadline 跟 MaxAckPending、而不是把 SQS 的 timeout 數字直接搬過去。</p>
<h3 id="maxreceivecount--redrive--dead-letter-topic">maxReceiveCount / redrive → dead-letter topic</h3>
<p>兩端都用「重試 N 次仍失敗就隔離」防止 poison message 阻塞 pipeline、但隔離後的容器不同。SQS 的 DLQ 是另一條 <em>queue</em>、用 maxReceiveCount 控制門檻、修好下游後用 redrive policy 把訊息放回原 queue。Pub/Sub 的 dead-letter topic 是另一個 <em>topic</em>、用 subscription 的 max delivery attempt 控制門檻、超過就 publish 到 DLT。</p>
<p>差別在重處理路徑。SQS redrive 是把 DLQ 訊息搬回 main queue、是一個 queue-to-queue 的搬移動作。Pub/Sub 的 DLT 是 topic、要重處理得在 DLT 上再開一個 subscription 來消費、沒有內建的「放回原 topic」按鈕。<a href="/blog/backend/03-message-queue/cases/pubsub-mercari-item-feed-dlt/" data-link-title="3.C64 Mercari Item Feed：DLT 防 poison message 阻塞" data-link-desc="Mercari 商品 feed 同步、ack 整批 / nack 重送、重試多次仍失敗送 DLT、topic 同時當 load-leveling buffer。">Mercari item feed 的案例</a>就是用 DLT 把重試多次仍失敗的訊息隔離、讓後續訊息優先處理、同時把 topic 當突發流量的 load-leveling buffer。從 SQS 搬過來時、redrive 的心智模型要換成「DLT 是一個獨立 topic、重處理是另開 subscription」、不是「按一個按鈕放回去」。設定 DLT 還需要給 Pub/Sub service account 對 DLT 的 publisher 權限跟對原 subscription 的 subscriber 權限、漏設會讓訊息卡住不進 DLT。</p>
<h3 id="long-polling--streaming-pull">Long polling → streaming pull</h3>
<p>這一組不是等價對位、是機制不同。SQS long polling 是 consumer 發一個 ReceiveMessage 請求、最多等 20 秒、有訊息就回、沒有就空回、本質仍是 <em>輪詢</em>、只是把空輪詢的頻率降下來省 cost。Pub/Sub 的 pull 在 client library 預設是 <em>streaming pull</em>：consumer 跟 Pub/Sub 建一條長連線、訊息一到就推過來、不是 consumer 反覆問。</p>
<p>對位時不要把 long polling 的「WaitTimeSeconds 20 秒」翻譯成某個 Pub/Sub 參數 — 沒有對應參數、因為機制不同。要關注的是 flow control：streaming pull 因為訊息會主動推來、要用 MaxOutstandingMessages / MaxAckPending 控制同時在處理的訊息量、否則 consumer 會被一次塞太多訊息壓垮。SQS 端「一次拉最多 10 條」的批次節流、在 Pub/Sub 端變成 flow control 設定。<a href="/blog/backend/03-message-queue/cases/pubsub-spotify-autoscaling-consumers/" data-link-title="3.C61 Spotify：Autoscaling Pub/Sub consumer 反效果" data-link-desc="Spotify 下游失敗時 consumer 不 ack 仍耗 CPU、autoscaling 越拉越高、解法是 exponential backoff 抑制 CPU。">Spotify autoscaling 的案例</a>揭露了相關陷阱：下游失敗時 consumer 不 ack 仍持續消耗 CPU、autoscaling 反而把資源越拉越高 — autoscale 訊號要看處理成功率、不是 backlog 加 CPU。</p>
<h3 id="iam-policy--service-account">IAM policy → Service Account</h3>
<p>跨雲遷移裡、身份模型是 <em>重對位</em> 而不是改語法的部分。SQS 的存取控制是 IAM policy（identity-based、掛在 user / role）加 queue policy（resource-based、掛在 queue）兩層、cross-account 靠這兩層互動。Pub/Sub 是 GCP IAM role（publisher / subscriber / viewer 等）加 Service Account、push subscription 要用 Service Account 認證到目標 endpoint。</p>
<p>兩套身份模型沒有自動轉換工具、要逐條重畫：誰能 publish 對應誰有 topic 的 publisher role、誰能消費對應誰有 subscription 的 subscriber role。跨雲場景還多一層 — 如果遷移期 AWS 端的服務要 publish 到 GCP 的 topic、得用 workload identity federation 或 service account key、讓 AWS 的工作負載拿到 GCP 身份。這部分沒有 case 可引、依 GCP 官方 IAM 文件加最小權限原則設計：每個 service account 只給它實際需要的 role、不要為了遷移方便給 broad role 再說以後收緊、那個「以後」通常不會來。</p>
<h3 id="cloudwatch--cloud-monitoring">CloudWatch → Cloud Monitoring</h3>
<p>監控訊號要重建、不是改名。SQS 在 CloudWatch 看 ApproximateNumberOfMessagesVisible（queue 深度）跟 ApproximateAgeOfOldestMessage（lag）。Pub/Sub 在 Cloud Monitoring 看 num_undelivered_messages（backlog）跟 oldest_unacked_message_age（最老未確認訊息年齡）。語意相近、但 alarm 邏輯要重寫、而且 Pub/Sub 的 backlog 數字要配合 subscription 維度看 — 同一個 topic 的不同 subscription 各自有 backlog、一個堵住不代表全部堵住。遷移時要把原本對 queue 深度的告警、改成對每個 subscription 的 backlog 與 age 告警。</p>
<h2 id="消費抽象重設計從一條-queue-到-topic-加多-subscription">消費抽象重設計：從一條 queue 到 topic 加多 subscription</h2>
<p>這是 components 跟 data topology 兩個高維度的核心、也是從 SQS 搬到 Pub/Sub 最需要重新畫圖的地方。SQS 的世界裡、一條 queue 對應一群競爭領取的 worker；要扇出就在前面架 SNS、SNS 後面接多條 SQS、每條 queue 各一群 worker。Pub/Sub 把這個拓樸壓平：一個 topic 收訊息、掛多少個 subscription 就有多少條獨立的消費流、每個 subscription 各自記進度、彼此不影響。</p>
<p>重設計從盤點現有拓樸開始。先列出：哪些是「單一 queue、一群 worker」的簡單情境、哪些是「SNS fan-out 到多條 SQS」的扇出情境。簡單情境對位乾淨 — 一個 topic、一個 pull subscription、原本競爭領取的 worker 改成同一個 subscription 的多個 consumer、Pub/Sub 自動把訊息分給它們。扇出情境要把 SNS + 多 SQS 換成「一個 topic + 多 subscription」、原本每條 SQS queue 變成一個 subscription、SNS 那一層消失。</p>
<p>扇出情境裡有個方向相反的陷阱要避免：不要把「多個下游」誤設計成「多個 consumer 共用一個 subscription」。同一個 subscription 的多個 consumer 是 <em>競爭</em> 關係、訊息只會給其中一個 — 那是負載分攤、不是扇出。要每個下游都收到完整一份、就要每個下游一個 <em>獨立</em> subscription。這跟 SQS 端「一條 queue 一個下游、扇出靠 SNS 複製」的直覺方向一致、但實體換了：在 SQS 是多條 queue、在 Pub/Sub 是多個 subscription。畫遷移圖時、SQS 的每條 fan-out queue 一對一映射到 Pub/Sub 的一個 subscription、不要合併。</p>
<h2 id="application-重設計範例sqs-receive-delete-換成-pubsub-pull-ack">Application 重設計範例：SQS receive-delete 換成 Pub/Sub pull-ack</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// SQS 端：long polling receive、處理完 DeleteMessage</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nx">svc</span> <span class="o">:=</span> <span class="nx">sqs</span><span class="p">.</span><span class="nf">NewFromConfig</span><span class="p">(</span><span class="nx">cfg</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">for</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">out</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">svc</span><span class="p">.</span><span class="nf">ReceiveMessage</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">sqs</span><span class="p">.</span><span class="nx">ReceiveMessageInput</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="nx">QueueUrl</span><span class="p">:</span>            <span class="o">&amp;</span><span class="nx">queueURL</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="nx">MaxNumberOfMessages</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">WaitTimeSeconds</span><span class="p">:</span>     <span class="mi">20</span><span class="p">,</span> <span class="c1">// long polling</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">m</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">out</span><span class="p">.</span><span class="nx">Messages</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="nf">process</span><span class="p">(</span><span class="nx">m</span><span class="p">.</span><span class="nx">Body</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="nx">svc</span><span class="p">.</span><span class="nf">DeleteMessage</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">sqs</span><span class="p">.</span><span class="nx">DeleteMessageInput</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="nx">QueueUrl</span><span class="p">:</span>      <span class="o">&amp;</span><span class="nx">queueURL</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="nx">ReceiptHandle</span><span class="p">:</span> <span class="nx">m</span><span class="p">.</span><span class="nx">ReceiptHandle</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="p">})</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">}</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// Pub/Sub 端：streaming pull、處理完 Ack、用 flow control 節流</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">sub</span> <span class="o">:=</span> <span class="nx">client</span><span class="p">.</span><span class="nf">Subscription</span><span class="p">(</span><span class="s">&#34;orders-sub&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">sub</span><span class="p">.</span><span class="nx">ReceiveSettings</span><span class="p">.</span><span class="nx">MaxOutstandingMessages</span> <span class="p">=</span> <span class="mi">100</span> <span class="c1">// flow control、取代「一次拉 10 條」</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nx">err</span> <span class="o">:=</span> <span class="nx">sub</span><span class="p">.</span><span class="nf">Receive</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">msg</span> <span class="o">*</span><span class="nx">pubsub</span><span class="p">.</span><span class="nx">Message</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nf">process</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">Data</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">msg</span><span class="p">.</span><span class="nf">Ack</span><span class="p">()</span> <span class="c1">// 取代 DeleteMessage；client library 在背景自動延長 ack deadline</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><p>差異：</p>
<ul>
<li>SQS 主動輪詢（ReceiveMessage 迴圈）→ Pub/Sub 回呼模型（Receive 把訊息推進 callback）</li>
<li>SQS DeleteMessage → Pub/Sub msg.Ack()、語意都是「確認處理完、別重投」</li>
<li>SQS WaitTimeSeconds 控制輪詢等待 → Pub/Sub MaxOutstandingMessages 控制 flow control</li>
<li>SQS 一次最多 10 條的批次上限 → Pub/Sub 沒有這個上限、改用 flow control 設同時在途量</li>
<li>ack deadline 的延長在 SQS 要主動 ChangeMessageVisibility、在 Pub/Sub 由 client library 自動處理</li>
</ul>
<p>application 邏輯的商業處理部分（process 函式）多數可保留、改動集中在收訊息的框架跟確認語意、估計 20-40% 程式碼。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1fan-out-設計成共用-subscription下游各收到一部分">Case 1：fan-out 設計成共用 subscription、下游各收到一部分</h3>
<p><strong>徵兆</strong>：把原本 SNS fan-out 到 3 條 SQS 的拓樸搬到 Pub/Sub、為了省事建一個 topic + 一個 subscription、讓 3 個下游服務都連這個 subscription。上線後發現每個下游只收到大約三分之一的訊息、不是各收完整一份。</p>
<p><strong>根因</strong>：同一個 subscription 的多個 consumer 是負載分攤關係、Pub/Sub 把訊息分給其中一個 consumer、不是每個都送。這對應到 SQS 端「一條 queue 多個 worker 競爭領取」的行為、但被誤用在需要扇出的場景。SQS 端的扇出靠 SNS 複製訊息到多條 queue、那個複製動作在 Pub/Sub 應該由「多個 subscription」承擔、不是多個 consumer 共用一個 subscription。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>每個下游一個獨立 subscription</strong>：3 個下游就建 3 個 subscription 掛同一個 topic、每個各收完整一份</li>
<li><strong>遷移圖一對一映射</strong>：SQS 的每條 fan-out queue 對應一個 Pub/Sub subscription、不合併</li>
<li><strong>負載分攤跟扇出分開設計</strong>：同一下游要多 worker 分攤、是同一 subscription 多 consumer；不同下游各收一份、是多 subscription</li>
</ol>
<h3 id="case-2ack-deadline-沿用-sqs-數字太短長任務反覆重投">Case 2：ack deadline 沿用 SQS 數字太短、長任務反覆重投</h3>
<p><strong>徵兆</strong>：SQS 端 visibility timeout 設 5 分鐘跑得好好的、搬到 Pub/Sub 隨手把 ack deadline 設成預設或一個小數字、結果處理時間稍長的訊息被反覆重投、同一筆訊息處理多次、下游出現重複副作用。</p>
<p><strong>根因</strong>：Pub/Sub ack deadline 預設 10 秒、上限 600 秒、跟 SQS visibility timeout 上限 12 小時差很多。如果關掉 client library 的自動 lease extension、又把 ack deadline 設小、處理時間一超過就被判定失敗重投。SQS 的「設一個夠長的 timeout」直覺搬過來不適用、因為 Pub/Sub 的上限低很多、且延長機制是 client library 自動做。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>理解 client library 的 lease 行為</strong>：多數 client library 預設會背景自動延長 ack deadline 到處理完、優先依賴這個而不是手動設超長 deadline</li>
<li><strong>長任務拆短或改架構</strong>：單筆處理超過 10 分鐘上限的、考慮拆成多階段或把長任務移出訊息處理路徑</li>
<li><strong>下游做 idempotency</strong>：跟 SQS 一樣、Pub/Sub 是 at-least-once、重投本來就會發生、下游用 message ID 去重才是根本解</li>
</ol>
<h3 id="case-3fifo-順序需求對位到-ordering-key吞吐落差超出預期">Case 3：FIFO 順序需求對位到 ordering key、吞吐落差超出預期</h3>
<p><strong>徵兆</strong>：原系統用 SQS FIFO queue + MessageGroupId 保證同一群訊息順序處理、搬到 Pub/Sub 啟用 ordering key 對位、上線後吞吐比預期低很多、且某些情境順序仍亂。</p>
<p><strong>根因</strong>：SQS FIFO 跟 Pub/Sub ordering key 都提供順序、但取捨點不同。SQS FIFO 同時給「順序」跟「5 分鐘去重窗口」、吞吐受限（每 MessageGroupId 串行）。Pub/Sub ordering key 給「同一 key 的訊息按 publish 順序送達」、但要 publish 端跟 subscription 端都正確設定（publish 要設 ordering key、subscription 要 enableMessageOrdering）、漏一邊順序就不保證；而且啟用 ordering 後同一 key 串行、吞吐同樣受限。把 FIFO 的「去重 + 順序」一包功能、誤以為 ordering key 也一包提供、是落差來源。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>拆開「順序」跟「去重」兩個需求</strong>：Pub/Sub ordering key 只管順序、去重要 application 端自己用 message ID 做</li>
<li><strong>publish 跟 subscription 兩端都設 ordering</strong>：缺一邊順序不保證、遷移檢查清單要把兩端都列上</li>
<li><strong>重新評估是否真需要全域順序</strong>：FIFO 常被過度使用、很多場景只需要 per-entity 順序、用 ordering key 按 entity 分 key、比強制全域串行吞吐高很多</li>
</ol>
<h3 id="case-4跨雲遷移期雙雲都在跑egress-成本與延遲被低估">Case 4：跨雲遷移期雙雲都在跑、egress 成本與延遲被低估</h3>
<p><strong>徵兆</strong>：漸進 cutover 期間 AWS 跟 GCP 兩邊都在處理訊息、為了對帳把訊息在兩雲之間搬、月底帳單跨雲 egress 費用遠超預估、且跨雲呼叫的尾延遲拖慢端到端處理。</p>
<p><strong>根因</strong>：SQS 在 AWS region 內、Pub/Sub 在 GCP、遷移期的 dual publish 或對帳如果讓資料反覆跨雲、每一筆出 AWS 的訊息都計 egress 費。跨雲不只是錢、跨雲網路的延遲跟抖動比同雲高、放在同步處理路徑上會放大尾延遲。同雲 vendor 切換沒有這個維度、跨雲遷移必須把它列進成本模型。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>縮短雙雲並行窗口</strong>：dual publish 的對帳期越短越省、設明確的並行截止日、不要無限期雙跑</li>
<li><strong>對帳用抽樣不用全量搬運</strong>：驗證一致性用抽樣比對 message ID / count、不要把所有訊息都搬到對面雲比對</li>
<li><strong>生產者就近落點</strong>：遷移後讓 producer 直接 publish 到 Pub/Sub、不要繞 AWS 再跨雲、消除穩態的跨雲 egress</li>
</ol>
<h3 id="case-5dead-letter-topic-權限沒配齊毒訊息卡住不進-dlt">Case 5：dead-letter topic 權限沒配齊、毒訊息卡住不進 DLT</h3>
<p><strong>徵兆</strong>：subscription 設了 dead-letter topic 跟 max delivery attempt、預期重試超限的訊息進 DLT、實際上毒訊息一直在原 subscription 反覆重投、DLT 是空的、後續訊息被堵。</p>
<p><strong>根因</strong>：Pub/Sub 要把訊息送進 DLT、是由 Pub/Sub 的 service account 代為 publish 到 DLT topic；同時它也要對原 subscription 有 subscriber 權限才能 ack 掉原訊息。這兩個權限漏任一個、forwarding 到 DLT 就失敗、訊息卡在原 subscription。SQS 端 DLQ 是 queue 屬性、不需要額外給 service 權限、所以這個跨雲差異容易被漏掉。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>配齊 DLT 雙權限</strong>：給 Pub/Sub service account 對 DLT topic 的 publisher role、跟對原 subscription 的 subscriber role</li>
<li><strong>遷移後做毒訊息演練</strong>：故意 publish 一筆會失敗的訊息、確認它真的在 max attempt 後進 DLT、不是卡在原 subscription</li>
<li><strong>監控 DLT backlog</strong>：DLT 開一個 subscription 監控其 num_undelivered_messages、確認毒訊息有被導流且有人處理、對照 <a href="/blog/backend/03-message-queue/cases/pubsub-mercari-item-feed-dlt/" data-link-title="3.C64 Mercari Item Feed：DLT 防 poison message 阻塞" data-link-desc="Mercari 商品 feed 同步、ack 整批 / nack 重送、重試多次仍失敗送 DLT、topic 同時當 load-leveling buffer。">Mercari DLT 案例</a>的設計</li>
</ol>
<h2 id="漸進-cutoverdual-publish-加雙消費對帳">漸進 cutover：dual publish 加雙消費對帳</h2>
<p>跨雲遷移風險高、不適合一次切換、走漸進 cutover 把可逆邊界拉長：</p>
<ol>
<li><strong>Phase 0：拓樸盤點</strong> — 列出所有 SQS queue、標記哪些是單一 queue、哪些是 SNS fan-out、各自映射到 Pub/Sub 的 topic / subscription 結構</li>
<li><strong>Phase 1：Pub/Sub 端建好對位資源</strong> — 建 topic / subscription / DLT、配齊 IAM 與 service account、重建 Cloud Monitoring 告警、application 寫好 Pub/Sub consumer 但先不收流量</li>
<li><strong>Phase 2：dual publish</strong> — producer 同時 publish 到 SQS 跟 Pub/Sub、兩邊 consumer 都跑、Pub/Sub 端的處理結果先寫到隔離區或標記、不影響正式下游</li>
<li><strong>Phase 3：雙消費對帳</strong> — 抽樣比對兩邊處理的訊息 ID 與數量、確認 Pub/Sub 端沒漏、沒重複到無法接受的程度、ack deadline / fan-out / ordering 行為都符合預期</li>
<li><strong>Phase 4：流量切換</strong> — 對帳通過後、把正式下游切到 Pub/Sub 端、SQS 端轉成備援、保留一段觀察期可回切</li>
<li><strong>Phase 5：下線 SQS</strong> — 觀察期穩定後停掉 dual publish、移除 SQS 資源、消除穩態跨雲 egress（這是不可逆階段、不要在對帳沒過時提前做）</li>
</ol>
<p>對帳期是這套流程的核心保險、也是 Case 4 跨雲成本的來源 — 對帳用抽樣、並行窗口設明確截止日、平衡「驗證信心」跟「雙雲成本」。</p>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>AWS SQS</th>
          <th>Google Pub/Sub</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>計費模型</td>
          <td>每百萬 request（含 send / receive / delete）</td>
          <td>按 throughput（publish + subscribe 的資料量計費）</td>
      </tr>
      <tr>
          <td>Region 模型</td>
          <td>Region-scoped、跨 region 自己處理</td>
          <td>Global topic、無 region 概念</td>
      </tr>
      <tr>
          <td>扇出成本</td>
          <td>SNS + 多 SQS、每條 queue 各計費</td>
          <td>一個 topic 多 subscription、按各 subscription throughput</td>
      </tr>
      <tr>
          <td>訊息保留</td>
          <td>預設 4 天、上限 14 天</td>
          <td>預設 7 天、可調</td>
      </tr>
      <tr>
          <td>順序成本</td>
          <td>FIFO queue 比 standard 貴</td>
          <td>ordering key 啟用後吞吐受限、計費同 standard</td>
      </tr>
      <tr>
          <td>跨雲 egress</td>
          <td>出 AWS 計 egress</td>
          <td>出 GCP 計 egress；穩態應讓 producer 就近 publish</td>
      </tr>
      <tr>
          <td>監控</td>
          <td>CloudWatch（隨用量計費）</td>
          <td>Cloud Monitoring</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：穩態成本兩者量級相近、真正的成本差在 <em>遷移期</em> — dual publish 雙雲並行加跨雲對帳搬運是一次性高峰、不是穩態。把這段窗口縮短、是控制跨雲遷移成本的關鍵、不是去比 SQS 跟 Pub/Sub 的單價。扇出重度的系統遷到 Pub/Sub 後、少掉 SNS 那一層、扇出的計費結構也變簡單。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="遷移後事件可直接落-gcp-資料平台">遷移後事件可直接落 GCP 資料平台</h3>
<p>遷到 Pub/Sub 的一個結構性好處、是事件可以用 BigQuery subscription 直接寫進 BigQuery、不需要再寫 Dataflow pipeline 搬運；或用 Cloud Storage subscription 批次落 GCS。這正是「workload 重心在 GCP」這條 driver 的回報 — 事件層跟資料平台同雲、省掉跨雲搬運。這也是評估是否該跨雲遷移時、要放進 ROI 的一邊。</p>
<h3 id="跟-kafka-遷移的結構對照">跟 Kafka 遷移的結構對照</h3>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>主導差異維度</th>
          <th>結構</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a></td>
          <td>Paradigm（高）</td>
          <td>partial + 長期混合</td>
      </tr>
      <tr>
          <td>SQS → Pub/Sub（本篇）</td>
          <td>Operational（高）+ components / topology（高）</td>
          <td>operational hybrid + 高維度獨立段</td>
      </tr>
  </tbody>
</table>
<p><strong>結論</strong>：SQS → Pub/Sub 不是 paradigm shift（兩端都是 cloud-managed 訊息服務、可收斂成單一目標）、是 operational redesign 為主、消費抽象重設計為輔的跨雲遷移；結構由主導差異維度（operational）決定主軸、高維度（components / topology）抽獨立段補充。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source / target vendor：<a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">AWS SQS</a> / <a href="/blog/backend/03-message-queue/vendors/google-pubsub/" data-link-title="Google Cloud Pub/Sub" data-link-desc="GCP managed pub/sub、global routing、push/pull">Google Pub/Sub</a></li>
<li>平行 vendor：<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> / <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a> / <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a></li>
<li>平行 migration playbook：<a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a></li>
<li>引用案例：<a href="/blog/backend/03-message-queue/cases/pubsub-mercari-item-feed-dlt/" data-link-title="3.C64 Mercari Item Feed：DLT 防 poison message 阻塞" data-link-desc="Mercari 商品 feed 同步、ack 整批 / nack 重送、重試多次仍失敗送 DLT、topic 同時當 load-leveling buffer。">3.C64 Mercari Item Feed DLT</a> / <a href="/blog/backend/03-message-queue/cases/pubsub-spotify-autoscaling-consumers/" data-link-title="3.C61 Spotify：Autoscaling Pub/Sub consumer 反效果" data-link-desc="Spotify 下游失敗時 consumer 不 ack 仍耗 CPU、autoscaling 越拉越高、解法是 exponential backoff 抑制 CPU。">3.C61 Spotify autoscaling</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 寫作方法論</a></li>
<li>上游概念：<a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3 非同步選型</a> / <a href="/blog/backend/06-reliability/idempotency-replay/" data-link-title="6.12 Idempotency 與 Replay 驗證" data-link-desc="把重試、重播與冪等性從口頭約定變成可驗證屬性">6.12 idempotency / replay</a></li>
</ul>
]]></content:encoded></item><item><title>AWS SQS：Visibility timeout、long polling 與 Lambda event source 的成本與失敗形狀</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/visibility-polling-lambda-cost/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/visibility-polling-lambda-cost/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">AWS SQS&lt;/a> overview 的 implementation-layer deep article。本文的 CLI 指令語法經 LocalStack round-trip 驗證、真實 AWS 的 scaling 行為、Lambda event source mapping 細節與計費數字依 AWS 官方文件。&lt;/p>&lt;/blockquote>
&lt;h2 id="sqs-沒有-broker-ackdelivery-控制全靠-visibility-timeout">SQS 沒有 broker ACK，delivery 控制全靠 visibility timeout&lt;/h2>
&lt;p>SQS 跟自管 broker（RabbitMQ / Kafka）最大的操作差異是：consumer 不會跟 broker 維持一條長連線、也沒有 channel-level 的 ack / nack 協議。SQS 的整個 delivery 保證建立在一個計時器上 — visibility timeout。訊息被 &lt;code>ReceiveMessage&lt;/code> 拉走後進入 in-flight 狀態、在 timeout 視窗內對其他 consumer 不可見；consumer 處理成功就呼叫 &lt;code>DeleteMessage&lt;/code> 把它移除、處理失敗或當機則什麼都不做、等 timeout 到期訊息自動回到 queue 重新可見。&lt;/p>
&lt;p>這個設計把「確認處理完成」的責任從 broker 連線狀態轉移到 consumer 的主動刪除。好處是 consumer 可以隨時死掉、重啟、水平擴縮、不需要維持任何 session 狀態 — 訊息不會因為連線斷掉而遺失。代價是 visibility timeout 這個數字變成最容易設錯、後果最隱蔽的參數：設太短訊息會在 consumer 還在處理時就重新可見、被另一個 consumer 重複領走；設太長則 consumer 當機後訊息要等很久才回到 queue、retry 延遲拉長。&lt;/p>
&lt;p>實機建立一個 queue 並查 default、可以確認這個視窗的起點。新建 queue 的 &lt;code>VisibilityTimeout&lt;/code> 預設 30 秒：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 不帶任何 attribute 建 queue&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">aws sqs create-queue --queue-name demo-default
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 查 default visibility timeout&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">aws sqs get-queue-attributes &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --queue-url &amp;lt;url&amp;gt; &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --attribute-names VisibilityTimeout
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># =&amp;gt; &amp;#34;VisibilityTimeout&amp;#34;: &amp;#34;30&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>30 秒對「處理時間穩定在數百毫秒」的 task 綽綽有餘、對「呼叫第三方 API、跑批次轉檔、寫多個下游」的 task 則經常不夠。下一節先把這個參數設對，後面的故障演練再展開它設錯時的具體徵兆。&lt;/p>
&lt;h2 id="對齊-visibility-timeout-與-consumer-處理時間">對齊 visibility timeout 與 consumer 處理時間&lt;/h2>
&lt;p>設定 visibility timeout 的判準是「略高於 consumer 處理單則訊息的最大時間」、不是平均時間。Capital One 的官方 tech blog 在講 SQS + Lambda 時明示這條原則：visibility timeout 應比最大處理時間略高 — 因為決定 redelivery 的是尾端那幾則最慢的訊息、不是中位數。處理時間 p50 是 2 秒、p99 是 25 秒時、visibility timeout 要對齊 p99 加緩衝、設到 30-40 秒、而不是看 p50 設 10 秒。&lt;/p>
&lt;p>建 queue 時直接帶 &lt;code>VisibilityTimeout&lt;/code> attribute，或對既有 queue 用 &lt;code>set-queue-attributes&lt;/code> 調整：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 建立時指定（單位：秒；上限 12 小時 = 43200）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">aws sqs create-queue &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --queue-name demo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --attributes &lt;span class="nv">VisibilityTimeout&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">60&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># 對既有 queue 調整&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">aws sqs set-queue-attributes &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --queue-url &amp;lt;url&amp;gt; &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --attributes &lt;span class="nv">VisibilityTimeout&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">120&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>處理時間本身不可預測的場景（例如轉檔大小差異大、下游 API 偶發慢）、用一個固定的 queue-level visibility timeout 會兩頭不討好：對齊最壞情況會讓正常訊息當機後 retry 太慢、對齊正常情況會讓慢訊息 redelivery。SQS 給的工具是 &lt;code>ChangeMessageVisibility&lt;/code> — consumer 在處理過程中發現這則會花更久時，主動延長這一則訊息的 visibility timeout，而不影響 queue default：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">AWS SQS</a> overview 的 implementation-layer deep article。本文的 CLI 指令語法經 LocalStack round-trip 驗證、真實 AWS 的 scaling 行為、Lambda event source mapping 細節與計費數字依 AWS 官方文件。</p></blockquote>
<h2 id="sqs-沒有-broker-ackdelivery-控制全靠-visibility-timeout">SQS 沒有 broker ACK，delivery 控制全靠 visibility timeout</h2>
<p>SQS 跟自管 broker（RabbitMQ / Kafka）最大的操作差異是：consumer 不會跟 broker 維持一條長連線、也沒有 channel-level 的 ack / nack 協議。SQS 的整個 delivery 保證建立在一個計時器上 — visibility timeout。訊息被 <code>ReceiveMessage</code> 拉走後進入 in-flight 狀態、在 timeout 視窗內對其他 consumer 不可見；consumer 處理成功就呼叫 <code>DeleteMessage</code> 把它移除、處理失敗或當機則什麼都不做、等 timeout 到期訊息自動回到 queue 重新可見。</p>
<p>這個設計把「確認處理完成」的責任從 broker 連線狀態轉移到 consumer 的主動刪除。好處是 consumer 可以隨時死掉、重啟、水平擴縮、不需要維持任何 session 狀態 — 訊息不會因為連線斷掉而遺失。代價是 visibility timeout 這個數字變成最容易設錯、後果最隱蔽的參數：設太短訊息會在 consumer 還在處理時就重新可見、被另一個 consumer 重複領走；設太長則 consumer 當機後訊息要等很久才回到 queue、retry 延遲拉長。</p>
<p>實機建立一個 queue 並查 default、可以確認這個視窗的起點。新建 queue 的 <code>VisibilityTimeout</code> 預設 30 秒：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 不帶任何 attribute 建 queue</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws sqs create-queue --queue-name demo-default
</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"># 查 default visibility timeout</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">aws sqs get-queue-attributes <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --queue-url &lt;url&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --attribute-names VisibilityTimeout
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># =&gt; &#34;VisibilityTimeout&#34;: &#34;30&#34;</span></span></span></code></pre></div><p>30 秒對「處理時間穩定在數百毫秒」的 task 綽綽有餘、對「呼叫第三方 API、跑批次轉檔、寫多個下游」的 task 則經常不夠。下一節先把這個參數設對，後面的故障演練再展開它設錯時的具體徵兆。</p>
<h2 id="對齊-visibility-timeout-與-consumer-處理時間">對齊 visibility timeout 與 consumer 處理時間</h2>
<p>設定 visibility timeout 的判準是「略高於 consumer 處理單則訊息的最大時間」、不是平均時間。Capital One 的官方 tech blog 在講 SQS + Lambda 時明示這條原則：visibility timeout 應比最大處理時間略高 — 因為決定 redelivery 的是尾端那幾則最慢的訊息、不是中位數。處理時間 p50 是 2 秒、p99 是 25 秒時、visibility timeout 要對齊 p99 加緩衝、設到 30-40 秒、而不是看 p50 設 10 秒。</p>
<p>建 queue 時直接帶 <code>VisibilityTimeout</code> attribute，或對既有 queue 用 <code>set-queue-attributes</code> 調整：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 建立時指定（單位：秒；上限 12 小時 = 43200）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws sqs create-queue <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --queue-name demo <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --attributes <span class="nv">VisibilityTimeout</span><span class="o">=</span><span class="m">60</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># 對既有 queue 調整</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">aws sqs set-queue-attributes <span class="se">\
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="se"></span>  --queue-url &lt;url&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="se"></span>  --attributes <span class="nv">VisibilityTimeout</span><span class="o">=</span><span class="m">120</span></span></span></code></pre></div><p>處理時間本身不可預測的場景（例如轉檔大小差異大、下游 API 偶發慢）、用一個固定的 queue-level visibility timeout 會兩頭不討好：對齊最壞情況會讓正常訊息當機後 retry 太慢、對齊正常情況會讓慢訊息 redelivery。SQS 給的工具是 <code>ChangeMessageVisibility</code> — consumer 在處理過程中發現這則會花更久時，主動延長這一則訊息的 visibility timeout，而不影響 queue default：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># consumer 拿到 ReceiptHandle 後，動態把這則延長到 120 秒</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws sqs change-message-visibility <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --queue-url &lt;url&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --receipt-handle &lt;receipt-handle&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --visibility-timeout <span class="m">120</span></span></span></code></pre></div><p>實務上長任務 consumer 的常見寫法是「heartbeat extension」：每處理一段就呼叫一次 <code>ChangeMessageVisibility</code> 往後推、形成一個續命迴圈、直到處理完成才 <code>DeleteMessage</code>。這把「我還活著、還在處理這則」的訊號明確化、避免用一個保守的 queue-level 大數字一刀切。<code>ReceiptHandle</code> 是每次 <code>ReceiveMessage</code> 回傳的一次性 token、不是 message id — 同一則訊息被重新領取後 ReceiptHandle 會變、延長操作必須用當次領取拿到的那一個。</p>
<h2 id="long-polling-決定空輪詢成本short-polling-是預設陷阱">Long polling 決定空輪詢成本，short polling 是預設陷阱</h2>
<p>Polling 模式直接決定 SQS 的 request 帳單，因為 SQS 按 request 數計費、而 <code>ReceiveMessage</code> 即使沒拿到訊息也算一次 request。Short polling（預設、<code>WaitTimeSeconds=0</code>）的行為是「立即回應」：consumer 發 <code>ReceiveMessage</code>、SQS 抽樣一部分 server 立刻回、queue 空的時候回一個空 response。Consumer 為了即時拿到訊息會緊接著再發一次、形成高頻空輪詢 — 在低流量 queue 上、絕大多數 request 都是空回、帳單全花在「問有沒有訊息」上。</p>
<p>Long polling（<code>WaitTimeSeconds</code> 設 1-20 秒）改變這個行為：SQS 收到 <code>ReceiveMessage</code> 後、若 queue 當下沒訊息、會 hold 住這條連線最多 <code>WaitTimeSeconds</code> 秒、期間一有訊息到達就立刻回傳、整段時間都沒訊息才回空。對 consumer 端來說一個 20 秒的 long poll 取代了 20 秒內可能發出的數十次 short poll、空 request 數量大幅下降。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># long polling：等到有訊息或最多 20 秒才回</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws sqs receive-message <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --queue-url &lt;url&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --wait-time-seconds <span class="m">20</span></span></span></code></pre></div><p>設定 long polling 有兩個位置：per-request 帶 <code>--wait-time-seconds</code>、或 queue-level 設 <code>ReceiveMessageWaitTimeSeconds</code> attribute 讓所有 receive 預設走 long polling。後者更穩、不依賴每個 consumer 都記得帶參數。20 秒幾乎總是對的選擇：它把空輪詢壓到最低、而 latency 代價只在「queue 剛好空、訊息在 poll 結束後才到」這個邊界出現 — 大多數有持續流量的 queue 根本碰不到 20 秒上限。唯一要留意的是 consumer 的 socket timeout 必須大於 <code>WaitTimeSeconds</code>、否則 client 會在 SQS 還在 hold 連線時自己先 timeout 斷線。</p>
<h2 id="sqs--lambdaevent-source-mapping-把-polling-交給-aws">SQS + Lambda：event source mapping 把 polling 交給 AWS</h2>
<p>把 SQS 接上 Lambda 時、polling 這件事整個從應用程式碼消失、改由 Lambda 的 event source mapping 接管。Event source mapping 是 Lambda service 內部一組 managed poller、持續對 queue 做 long polling、把拉到的訊息打包成 batch 同步 invoke 函式、函式正常返回就由 service 代為 <code>DeleteMessage</code>。Consumer 端不再寫 receive / delete 迴圈、只寫處理單一 batch 的 handler。</p>
<p>這套 managed poller 的 scaling 不是線性的、有 ramp-up 上限。Capital One 觀察到的行為是：Lambda 初始開 5 個並行的 long polling 連線、隨 queue 累積每分鐘最多增加 60 個 instance、standard queue 的並行 batch 上限到 1000。這意味著 queue 突然湧入大量訊息時、Lambda 不會瞬間炸開到滿並行、而是分鐘級爬升 — 容量規劃時要把這段 ramp-up 期算進 backlog 消化時間、不能假設「訊息一到就有足夠 consumer」。</p>
<p>兩個核心參數決定每次 invoke 的形狀：</p>
<table>
  <thead>
      <tr>
          <th>參數</th>
          <th>作用</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Batch size</td>
          <td>一次 invoke 最多打包幾則訊息（standard 上限 10000、FIFO 上限 10）</td>
          <td>大 batch 省 invoke 數與成本、但放大「部分失敗整批重投」風險</td>
      </tr>
      <tr>
          <td>Batch window</td>
          <td>累積訊息的最長等待時間（<code>MaximumBatchingWindowInSeconds</code>、0-300 秒）</td>
          <td>拉長視窗讓 batch 更滿、代價是 latency；流量稀疏時尤其明顯</td>
      </tr>
  </tbody>
</table>
<p>Batch size 拉大表面上省錢 — invoke 次數少、每則訊息分攤的 request 成本低。但它跟下一節的部分失敗處理直接耦合：batch 越大、一則毒訊息拖累整批重投的範圍越大。Batch window 則是流量稀疏時讓 batch 攢滿的手段、流量本來就密集時設不設都差不多、反而會引入不必要的 latency。</p>
<h2 id="dlq-與-redrive-policy用-maxreceivecount-隔離毒訊息">DLQ 與 redrive policy：用 maxReceiveCount 隔離毒訊息</h2>
<p>毒訊息（永遠處理失敗的訊息 — 格式損壞、引用了已刪除的資源、觸發 consumer 確定性 bug）會在 visibility timeout 機制下無限重投：處理失敗、timeout 到期、重新可見、再次被領取、再次失敗。沒有上限的話這則訊息會永遠佔用 consumer 資源、且其他正常訊息的處理被它反覆插隊。Dead-letter queue（DLQ）加 <code>maxReceiveCount</code> 是 SQS 對這個問題的標準解 — 訊息被接收超過 N 次後、SQS 自動把它移到另一個指定的 queue（DLQ）、主 queue 不再被它卡住。</p>
<p>設定分兩步：先建一個普通 queue 當 DLQ、取它的 ARN、再對主 queue 設 redrive policy 指向這個 ARN 並設 <code>maxReceiveCount</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 建 DLQ 並取得 ARN</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">aws sqs create-queue --queue-name demo-dlq
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">aws sqs get-queue-attributes <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --queue-url &lt;dlq-url&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --attribute-names QueueArn
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># =&gt; &#34;QueueArn&#34;: &#34;arn:aws:sqs:us-east-1:000000000000:demo-dlq&#34;</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. 對主 queue 設 redrive policy（被接收 5 次後送 DLQ）</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">aws sqs set-queue-attributes <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --queue-url &lt;main-url&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --attributes <span class="s1">&#39;{&#34;RedrivePolicy&#34;:&#34;{\&#34;deadLetterTargetArn\&#34;:\&#34;arn:aws:sqs:us-east-1:000000000000:demo-dlq\&#34;,\&#34;maxReceiveCount\&#34;:\&#34;5\&#34;}&#34;}&#39;</span></span></span></code></pre></div><p>DLQ 不是訊息的墳場、是待診斷的暫存區。對應 <a href="/blog/backend/knowledge-cards/poison-message-quarantine/" data-link-title="Poison-Message Quarantine" data-link-desc="說明把毒訊息從主處理路徑隔離出來的機制，讓正常訊息繼續前進">poison message quarantine</a> 的思路、DLQ 累積要分兩種根因處理：訊息格式錯（永遠失敗、需要修 producer 或人工丟棄）vs 下游服務暫時 down（訊息本身沒問題、修好下游後可以重放）。後者用 redrive 把訊息從 DLQ 批次放回主 queue 重新處理、對應 <a href="/blog/backend/knowledge-cards/dlq-drain/" data-link-title="DLQ Drain" data-link-desc="說明把 dead-letter queue 累積的訊息重新處理或排空的受控流程">dlq drain</a> 的排空流程。判斷之前先看 DLQ 裡訊息的內容、不要不加判斷地 redrive — 把毒訊息 redrive 回去只會再走一輪 maxReceiveCount 又回到 DLQ。</p>
<p><code>maxReceiveCount</code> 設多少是取捨：太小（例如 1-2）會讓「下游短暫抖動」這種暫時性失敗被誤判成毒訊息、過早送進 DLQ；太大（例如 100）會讓真正的毒訊息浪費大量 consumer 重試。多數 task queue 設 3-5 是合理起點 — 足以吸收幾次暫時性失敗、又不至於讓確定性失敗的訊息空轉太久。</p>
<h2 id="message-size-限制與-extended-client">Message size 限制與 extended client</h2>
<p>SQS 單則訊息上限是 256 KB（含 message body 與 attributes）。這對純事件通知、id 引用、小型 payload 足夠、但對「訊息本身要攜帶大檔案內容」的場景不夠 — 例如要傳一份報表、一張圖、一段長文字。直接的反模式是把大內容塞進 message body、撞上 256 KB 限制後 <code>SendMessage</code> 直接報錯。</p>
<p>標準解是 claim-check 模式：大 payload 寫到 S3、訊息只攜帶 S3 的物件引用（bucket + key）、consumer 收到訊息後再去 S3 取內容。AWS 提供的 Extended Client Library（Java / Python 等 SDK）把這個模式封裝起來 — <code>SendMessage</code> 時若 payload 超過門檻、library 自動把內容寫 S3、訊息只帶 pointer；consumer 端 <code>ReceiveMessage</code> 時 library 自動從 S3 取回、對應用程式碼透明。</p>
<p>選擇門檻時要把 S3 的 request 成本與 latency 算進來：每則大訊息變成「一次 S3 PUT + 一次 SQS Send」、consumer 端「一次 SQS Receive + 一次 S3 GET」。對大多數 payload 都超過 256 KB 的 queue、這是必要成本；對 payload 多數很小、偶爾爆量的 queue、extended client 只在超門檻時走 S3、混合成本可接受。Payload 普遍很大且高頻的場景、要重新評估 SQS 是否適合 — 可能該改用 streaming（Kinesis / Kafka）或乾脆讓 producer / consumer 直接交換 S3 引用、SQS 只傳通知。</p>
<h2 id="cost按-request-計費每一次操作都是一個-request">Cost：按 request 計費，每一次操作都是一個 request</h2>
<p>SQS 的計費模型是 per-request、不是 per-message-stored、也沒有固定月費。每一次 API call — <code>SendMessage</code>、<code>ReceiveMessage</code>（含空回）、<code>DeleteMessage</code>、<code>ChangeMessageVisibility</code> — 都算一個 request。這個模型對成本估算的影響是：帳單由「操作次數」驅動、而非「訊息量」或「儲存時長」。一則訊息從 producer 到 consumer 的最小生命週期是 send（1）+ receive（1）+ delete（1）= 3 個 request；空輪詢、retry、visibility 延長都會額外加 request。</p>
<p>兩個降低 request 數的主要手段：</p>
<p>第一是 batch 操作。<code>SendMessageBatch</code> 與 <code>DeleteMessageBatch</code> 一次最多打包 10 則、而 SQS 把一個 batch call 算作一個 request（實際計費以 64 KB 為一個 request 單位、一個 batch 在此範圍內仍是少數 request）。把 10 則訊息的 send 從 10 個 request 壓成 1 個 batch request、在高頻 queue 上是數量級的成本差異：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">aws sqs send-message-batch <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --queue-url &lt;url&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --entries <span class="s1">&#39;Id=m1,MessageBody=a&#39;</span> <span class="s1">&#39;Id=m2,MessageBody=b&#39;</span></span></span></code></pre></div><p>第二是 long polling 消滅空 request — 前面 polling 段已經展開。低流量 queue 的帳單若異常高、第一個要查的就是有沒有開 long polling、consumer 是不是在 short polling 下高頻空轉。</p>
<p>Data transfer cost 只在跨 region 時出現 — 同 region 內 producer / consumer 與 SQS 之間的傳輸不計流量費。把 producer、consumer、queue 放在同一個 region 是預設、跨 region 設計要把 egress 成本明確算進來。FIFO queue 的 per-request 單價比 standard 高、是用成本換 ordering 與去重保證 — 不需要嚴格順序的場景用 standard、把這筆溢價省下來。</p>
<p>Rapid7 的規模參考點說明這個計費模型在極端規模下的份量：Rapid7 公開引述 SQS 撐住「每天數十億則訊息」。在這個量級、per-request 計費乘以訊息數是一筆需要認真建模的成本 — batch、long polling、避免不必要的 visibility 延長、控制 retry 次數、每一項節省都被訊息量放大。SQS 在數十億級可用、但成本結構必須被當作架構參數對待、不是事後才看帳單。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="故障一visibility-timeout-短於處理時間訊息被重複處理">故障一：visibility timeout 短於處理時間，訊息被重複處理</h3>
<p><strong>徵兆</strong>：consumer log 顯示同一個 message id 在短時間內被處理多次、下游出現重複的副作用（重複扣款、重複寄信、重複寫入）；CloudWatch 的 <code>ApproximateNumberOfMessagesNotVisible</code>（in-flight 數）異常高、<code>NumberOfMessagesReceived</code> 遠大於 <code>NumberOfMessagesDeleted</code>。</p>
<p><strong>根因</strong>：visibility timeout 設定值低於 consumer 實際處理單則訊息的時間。訊息在 consumer 還沒處理完、還沒呼叫 <code>DeleteMessage</code> 之前、timeout 就到期、訊息重新可見、被另一個 consumer（或同一個 consumer 的下一輪 poll）領走。新建 queue 的 default 是 30 秒 — 處理時間長於此就會踩到：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">aws sqs get-queue-attributes <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --queue-url &lt;url&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --attribute-names VisibilityTimeout
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 看到 30 而 consumer 處理時間 &gt; 30s，就是這個問題</span></span></span></code></pre></div><p><strong>修法</strong>：把 visibility timeout 對齊 consumer 處理時間的 p99 加緩衝、用 <code>set-queue-attributes</code> 調高；處理時間變異大的長任務改用 <code>ChangeMessageVisibility</code> heartbeat 在處理中動態延長。同時、因為 SQS standard 是 at-least-once、重複投遞在故障與 retry 下本來就會發生、consumer 的處理邏輯必須冪等 — 對齊 visibility timeout 降低重複頻率、冪等性才是真正消除重複副作用的防線。</p>
<h3 id="故障二short-polling-預設導致低流量-queue-帳單異常">故障二：short polling 預設導致低流量 queue 帳單異常</h3>
<p><strong>徵兆</strong>：一個訊息量很低的 queue、月度 SQS 帳單卻很高；CloudWatch 顯示 <code>NumberOfEmptyReceives</code> 佔 <code>ReceiveMessage</code> 總數的絕大比例 — 大量 request 是空回。</p>
<p><strong>根因</strong>：consumer 走 short polling（<code>WaitTimeSeconds=0</code>、預設值）、在 queue 空的時候緊密地反覆發 <code>ReceiveMessage</code>、每次都立即空回、每次都計一個 request。流量越低、空回比例越高、帳單越是花在「問有沒有訊息」上。</p>
<p><strong>修法</strong>：在 queue-level 設 <code>ReceiveMessageWaitTimeSeconds=20</code> 讓所有 receive 預設走 long polling、或在每個 <code>ReceiveMessage</code> 帶 <code>--wait-time-seconds 20</code>。Queue-level 設定更穩、不依賴每個 consumer 記得帶參數。設定後 consumer 在 queue 空時會 hold 住連線最多 20 秒、空 request 數量級下降、帳單同步下降。同時確認 consumer 的 socket timeout 大於 20 秒、避免 client 先於 SQS 斷線。</p>
<h3 id="故障三lambda-batch-部分失敗整批訊息被重投">故障三：Lambda batch 部分失敗，整批訊息被重投</h3>
<p><strong>徵兆</strong>：一個 batch 裡只有少數訊息處理失敗、但整批訊息（含已成功的）全部回到 queue 重新處理；下游對已成功的訊息出現重複副作用；DLQ 累積速度遠超實際毒訊息數量。</p>
<p><strong>根因</strong>：Lambda event source mapping 的 default 行為是「整批成敗一體」— 函式只要拋出錯誤、整個 batch 被視為失敗、所有訊息（包含已經處理成功的）都不會被刪除、全部重新可見重投。Batch size 越大、一則失敗拖累的成功訊息越多。</p>
<p><strong>修法</strong>：啟用 partial batch response — event source mapping 設 <code>ReportBatchItemFailures</code>、handler 返回時只回報失敗的 message id 清單、SQS 只把這些重投、已成功的正常刪除。這把失敗的爆炸半徑從「整批」縮到「真正失敗的那幾則」。配合縮小 batch size 進一步降低單批風險、並確保 handler 冪等以承受不可避免的重投。Handler 必須正確實作 partial response 的返回格式 — 漏回報某則失敗會讓它被當成成功刪除、訊息靜默遺失。</p>
<h3 id="故障四maxreceivecount-設定不當毒訊息空轉或誤判">故障四：maxReceiveCount 設定不當，毒訊息空轉或誤判</h3>
<p><strong>徵兆</strong>：兩種相反的故障形狀。一是 DLQ 幾乎為空但主 queue 有訊息反覆重試數十次、consumer log 同一 message id 重複出現、佔用處理容量 — maxReceiveCount 設太大。二是 DLQ 快速累積大量其實沒問題的訊息、redrive 回去又能正常處理 — maxReceiveCount 設太小、把下游短暫抖動誤判成毒訊息。</p>
<p><strong>根因</strong>：redrive policy 沒設、或 <code>maxReceiveCount</code> 與「暫時性失敗的正常重試次數」不匹配。沒設 redrive policy 時毒訊息無限重投；設太大時毒訊息空轉太久才進 DLQ；設太小時正常訊息在下游抖動期間被過早判死。</p>
<p><strong>修法</strong>：對主 queue 設 redrive policy、<code>maxReceiveCount</code> 取 3-5 作為起點 — 足以吸收幾次暫時性失敗、又不讓確定性失敗的訊息空轉太久。觀察 DLQ 的累積模式再微調：DLQ 累積的多是「下游修好後 redrive 能成功」的訊息就調高、累積的多是「redrive 回去又進 DLQ」的真毒訊息就維持或調低。對 DLQ 設 CloudWatch alarm 監控 <code>ApproximateNumberOfMessagesVisible</code>、累積超過閾值就告警人工介入、區分 redrive vs 丟棄。</p>
<h3 id="故障五fifo-queue-撞上吞吐上限">故障五：FIFO queue 撞上吞吐上限</h3>
<p><strong>徵兆</strong>：把 standard queue 換成 FIFO 取得 ordering 後、高峰流量下 producer 端開始收到 throttling、訊息積壓、<code>SendMessage</code> 報限流錯誤；吞吐怎麼加 consumer 都上不去。</p>
<p><strong>根因</strong>：FIFO queue 為了維持順序與去重、吞吐遠低於 standard。FIFO 的基礎吞吐是每秒 300 則訊息（API call）、開啟 batching 後到每秒 3000 則。更關鍵的是順序保證的粒度在 <code>MessageGroupId</code> — 同一個 group 內的訊息嚴格串行處理、跨 group 才能並行。若所有訊息共用一個 group id、實際並行度退化成 1、無論加多少 consumer 都無法並行消化。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># FIFO send 必須帶 MessageGroupId（決定順序與並行粒度）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws sqs send-message <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --queue-url &lt;fifo-url&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --message-body <span class="s2">&#34;ordered-1&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --message-group-id <span class="s2">&#34;group-a&#34;</span></span></span></code></pre></div><p><strong>修法</strong>：先確認是否真的需要全域順序 — 多數場景只需要「同一個實體（同一用戶、同一訂單）內部有序」、不需要跨實體有序。把 <code>MessageGroupId</code> 設成業務實體 id（用戶 id、訂單 id）、讓不同實體的訊息能跨 group 並行、吞吐隨 group 數量擴展。確定需要嚴格全域順序且吞吐撞頂的場景、FIFO 的設計上限就是天花板 — 此時要重新評估是否該換成 streaming（Kafka 的 partition 模型在 per-key 有序下提供更高並行）、或拆分 queue。不需要任何順序保證的場景、退回 standard queue、把 FIFO 的吞吐限制與成本溢價一起省掉。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<h3 id="跟-consumer-設計能力對接">跟 consumer 設計能力對接</h3>
<p>本文的 visibility timeout heartbeat、partial batch response、冪等處理都是 <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> 的具體落地 — consumer-design 講語言無關的 consumer 模式、本文是 SQS 上的實作形狀。retry 與 replay 的交接路徑見 <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。">queue consumer retry replay handoff</a>。</p>
<h3 id="跟知識卡對位">跟知識卡對位</h3>
<p>DLQ 段對應 <a href="/blog/backend/knowledge-cards/poison-message-quarantine/" data-link-title="Poison-Message Quarantine" data-link-desc="說明把毒訊息從主處理路徑隔離出來的機制，讓正常訊息繼續前進">poison message quarantine</a>（毒訊息隔離）與 <a href="/blog/backend/knowledge-cards/dlq-drain/" data-link-title="DLQ Drain" data-link-desc="說明把 dead-letter queue 累積的訊息重新處理或排空的受控流程">dlq drain</a>（DLQ 排空）兩張卡 — SQS 的 redrive policy + maxReceiveCount 是這兩個概念在 managed queue 上的具體機制。visibility timeout 的 in-flight 概念見 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a>。</p>
<h3 id="跟-case-對位">跟 case 對位</h3>
<p>visibility timeout 與 Lambda event source 的 ramp-up 行為來自 <a href="/blog/backend/03-message-queue/cases/sqs-capital-one-visibility-timeout/" data-link-title="3.C50 Capital One：Visibility timeout 設計與 Lambda event source" data-link-desc="Capital One tech blog 講 SQS &#43; Lambda：visibility timeout 應略高於最大處理時間、Lambda 初 5 個 long polling、可擴 60/min。">3.C50 Capital One</a>；at-least-once + DLQ 在工作排程的取捨來自 <a href="/blog/backend/03-message-queue/cases/sqs-airbnb-dynein-delayed-jobs/" data-link-title="3.C48 Airbnb Dynein：SQS 分散式延遲任務排程" data-link-desc="Airbnb 用 SQS at-least-once &#43; DLQ 取代 Resque 單 Redis 限制、每 scheduler 1000 QPS、SQS wrap DynamoDB 處理 &gt; 15 分鐘 delay。">3.C48 Airbnb Dynein</a>；per-request cost 在極端規模的份量來自 <a href="/blog/backend/03-message-queue/cases/sqs-rapid7-scale-billion-messages/" data-link-title="3.C59 Rapid7：SQS 100 億 message/day 規模" data-link-desc="Rapid7 公開引述：SQS 撐 10s of billions of messages per day、是架構關鍵元件、scale 量級的具體參考。">3.C59 Rapid7</a>。</p>
<h3 id="何時-revisit">何時 revisit</h3>
<p>FIFO 吞吐撞頂、需要 replay / streaming、或 cost 在 streaming 模型下更划算時、回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS overview 的「何時改走其他服務」</a> 重新選型。跨雲 managed queue 的對照見 <a href="/blog/backend/03-message-queue/vendors/google-pubsub/" data-link-title="Google Cloud Pub/Sub" data-link-desc="GCP managed pub/sub、global routing、push/pull">Google Pub/Sub</a>。</p>
]]></content:encoded></item><item><title>RabbitMQ → AWS SQS：交出 broker 維運、把 routing 收斂進 application</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/migrate-to-aws-sqs/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/migrate-to-aws-sqs/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">AWS SQS&lt;/a>。對照 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&amp;#39;migration&amp;#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &amp;#43; 混合架構">Kafka ↔ NATS&lt;/a> 的 paradigm shift、本篇主導差異維度是 &lt;em>operational model&lt;/em>：source 跟 target 都是任務隊列、能力大致對得上、但運維責任從「自管 broker 叢集」整批交給 AWS managed 服務。&lt;/p>&lt;/blockquote>
&lt;p>RabbitMQ → AWS SQS 的核心是把 broker 運維責任轉移給 managed 服務、同時接受 SQS 沒有 exchange routing 這個事實、把路由邏輯收斂回 application 或改用 SNS fan-out。這個遷移不是 protocol drop-in（AMQP client 不能直接連 SQS）、application 端需要改 delivery 控制機制（manual ack → visibility timeout + delete）；但它也不是 paradigm shift（兩端都是 at-least-once 任務隊列、DLQ / 重試 / 解耦的語意一致）。主導差異落在 operational 維度、所以本文走 Type C operational redesign hybrid 結構。&lt;/p>
&lt;h2 id="為什麼遷不想再養-rabbitmq-叢集">為什麼遷：不想再養 RabbitMQ 叢集&lt;/h2>
&lt;p>觸發評估 SQS 的最常見壓力是 broker 維運成本、不是功能缺口。自管 RabbitMQ 叢集要承擔的運維責任包含 Erlang cluster 拓樸維護、network partition（腦裂）處理、quorum queue 的 Raft 一致性調校、disk / memory alarm 的容量規劃、版本升級的 rolling restart。這些責任需要至少 0.5-1 FTE 的持續投入、且在 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">network partition&lt;/a> 這類事故發生時需要熟悉 Erlang runtime 的人即時介入。&lt;/p>
&lt;p>SQS 把這整層責任移除。沒有 broker 實例、沒有 cluster 拓樸、沒有 disk / memory watermark、沒有版本升級。換來的代價是 routing 能力消失（SQS 沒有 exchange）、application 要改 delivery 控制機制、以及 AWS 生態綁定。這個交換在三種情境下成立：&lt;/p>
&lt;p>第一種是 AWS 生態原生服務。若 producer / consumer 已經跑在 Lambda、ECS、EKS 上、SQS 的 event source mapping 跟 IAM 整合讓 application 不必自管連線池跟認證。RabbitMQ 在 AWS 上要嘛自管 EC2 叢集、要嘛用 Amazon MQ（仍是 broker 模型、運維責任只是部分轉移）、都不如 SQS 的 serverless 整合直接。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a> 跟 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">AWS SQS</a>。對照 <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> 的 paradigm shift、本篇主導差異維度是 <em>operational model</em>：source 跟 target 都是任務隊列、能力大致對得上、但運維責任從「自管 broker 叢集」整批交給 AWS managed 服務。</p></blockquote>
<p>RabbitMQ → AWS SQS 的核心是把 broker 運維責任轉移給 managed 服務、同時接受 SQS 沒有 exchange routing 這個事實、把路由邏輯收斂回 application 或改用 SNS fan-out。這個遷移不是 protocol drop-in（AMQP client 不能直接連 SQS）、application 端需要改 delivery 控制機制（manual ack → visibility timeout + delete）；但它也不是 paradigm shift（兩端都是 at-least-once 任務隊列、DLQ / 重試 / 解耦的語意一致）。主導差異落在 operational 維度、所以本文走 Type C operational redesign hybrid 結構。</p>
<h2 id="為什麼遷不想再養-rabbitmq-叢集">為什麼遷：不想再養 RabbitMQ 叢集</h2>
<p>觸發評估 SQS 的最常見壓力是 broker 維運成本、不是功能缺口。自管 RabbitMQ 叢集要承擔的運維責任包含 Erlang cluster 拓樸維護、network partition（腦裂）處理、quorum queue 的 Raft 一致性調校、disk / memory alarm 的容量規劃、版本升級的 rolling restart。這些責任需要至少 0.5-1 FTE 的持續投入、且在 <a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">network partition</a> 這類事故發生時需要熟悉 Erlang runtime 的人即時介入。</p>
<p>SQS 把這整層責任移除。沒有 broker 實例、沒有 cluster 拓樸、沒有 disk / memory watermark、沒有版本升級。換來的代價是 routing 能力消失（SQS 沒有 exchange）、application 要改 delivery 控制機制、以及 AWS 生態綁定。這個交換在三種情境下成立：</p>
<p>第一種是 AWS 生態原生服務。若 producer / consumer 已經跑在 Lambda、ECS、EKS 上、SQS 的 event source mapping 跟 IAM 整合讓 application 不必自管連線池跟認證。RabbitMQ 在 AWS 上要嘛自管 EC2 叢集、要嘛用 Amazon MQ（仍是 broker 模型、運維責任只是部分轉移）、都不如 SQS 的 serverless 整合直接。</p>
<p>第二種是 routing 邏輯本來就簡單。若 RabbitMQ 的用法是 direct exchange + 少數固定 routing key、或單純 worker pool 消費單一 queue、那 exchange 的靈活性本來就沒被用到、遷到 SQS 不損失能力。Airbnb 的 Dynein 分散式延遲任務系統就是這個形狀：用 SQS at-least-once + DLQ 取代原本受限於單 Redis 的 Resque、每 scheduler instance 達約 1000 QPS、水平擴展（見 <a href="/blog/backend/03-message-queue/cases/sqs-airbnb-dynein-delayed-jobs/" data-link-title="3.C48 Airbnb Dynein：SQS 分散式延遲任務排程" data-link-desc="Airbnb 用 SQS at-least-once &#43; DLQ 取代 Resque 單 Redis 限制、每 scheduler 1000 QPS、SQS wrap DynamoDB 處理 &gt; 15 分鐘 delay。">3.C48 Airbnb Dynein</a>）。任務排程對「不丟資料」的需求 at-least-once 足夠、不需要 broker 級 routing。</p>
<p>第三種是團隊規模不支撐 broker 專業。小團隊養一套 RabbitMQ 叢集、真正用到的是「可靠的任務隊列 + DLQ」、但要付出整套 Erlang 運維學習曲線。把這層交給 SQS、團隊把精力放回 application 邏輯。</p>
<h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit</h2>
<p>遷移前先跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">diff dimension audit</a>、對每個維度評估 source 跟 target 的差異程度、決定主導維度跟結構：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>RabbitMQ（self-managed）</th>
          <th>AWS SQS（managed）</th>
          <th>差異</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>AMQP 0-9-1 協議、exchange / queue</td>
          <td>HTTP API、SendMessage / ReceiveMessage</td>
          <td>中</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>自管 Erlang 叢集、cluster / disk / 升級</td>
          <td>Fully managed、無實例、無版本</td>
          <td>高</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>任務隊列 + 重試 + DLQ</td>
          <td>任務隊列 + 重試 + DLQ</td>
          <td>低</td>
      </tr>
      <tr>
          <td>Components（1 vs N）</td>
          <td>broker 一站式（routing 內建）</td>
          <td>SQS + 需要 SNS 補 fan-out routing</td>
          <td>中</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>manual ack / nack、prefetch、AMQP client</td>
          <td>visibility timeout + delete、batch、SDK</td>
          <td>中高</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>單叢集 / federation 拓樸</td>
          <td>region-scoped queue、無拓樸概念</td>
          <td>低</td>
      </tr>
  </tbody>
</table>
<p><strong>主導維度是 operational（高）</strong>：遷移的核心價值跟核心風險都在「broker 運維責任整批轉移」。Application change 維度評中高、因為 delivery 控制機制要改、但這是受控的 SDK 層改寫、不是 paradigm 重設計。Components 維度評中、因為 exchange routing 在 SQS 沒有對等物、要靠 SNS fan-out 或多 queue 補回來。其餘三維度低或中。</p>
<p>主導維度落在 operational、所以主結構走 Type C：以 operational redesign 對位開頭、phased 執行、故障演練聚焦在「以為對等其實不對等」的運維陷阱。Application change 跟 Components 兩個次高維度不硬塞進主結構、各自抽出獨立段（下面「application 改寫」跟「routing 收斂」兩段）。</p>
<h3 id="operational-redesign-對位">Operational redesign 對位</h3>
<p>Operational 維度差異最大、先逐項對位「原本自己做的事、現在誰做、怎麼做」：</p>
<table>
  <thead>
      <tr>
          <th>運維責任</th>
          <th>RabbitMQ（自己做）</th>
          <th>SQS（managed / application）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>高可用</td>
          <td>quorum queue + cluster + partition 處理</td>
          <td>AWS 跨 AZ 自動冗餘、無需配置</td>
      </tr>
      <tr>
          <td>容量規劃</td>
          <td>disk / memory watermark、queue length 限</td>
          <td>自動擴展、無實例容量概念</td>
      </tr>
      <tr>
          <td>版本升級</td>
          <td>rolling restart、相容性驗證</td>
          <td>無、AWS 維護</td>
      </tr>
      <tr>
          <td>監控</td>
          <td>Management UI + Prometheus exporter</td>
          <td>CloudWatch metric（depth / age）</td>
      </tr>
      <tr>
          <td>Delivery 控制</td>
          <td>broker-side ack / nack 狀態機</td>
          <td>client-side visibility timeout + delete</td>
      </tr>
      <tr>
          <td>重試 / DLQ</td>
          <td>DLX + dead-letter routing key</td>
          <td>redrive policy + maxReceiveCount</td>
      </tr>
      <tr>
          <td>Routing</td>
          <td>exchange + binding（broker 內建）</td>
          <td>application 或 SNS（broker 外）</td>
      </tr>
  </tbody>
</table>
<p>前四列是純收益：責任消失、不需要對等實作。後三列是責任轉移、不是消失 — delivery 控制從 broker 移到 client、重試從 DLX 移到 redrive policy、routing 從 broker 移到 application。這三列正是故障演練聚焦的地方、因為「以為功能還在、其實機制換了」是這類遷移的主要事故來源。</p>
<p>監控這列值得展開。RabbitMQ 的 queue depth、unacked、consumer 數量是從 broker 直接讀；SQS 改看 CloudWatch 的 <code>ApproximateNumberOfMessagesVisible</code>（queue depth）跟 <code>ApproximateAgeOfOldestMessage</code>（lag 訊號）。差異在於 SQS 的 metric 是 approximate、且有分鐘級延遲、不適合用來做秒級的 backpressure 決策。原本靠 RabbitMQ Management UI 即時看 queue 狀態的 runbook 要改寫成 CloudWatch alarm 驅動。</p>
<h2 id="application-改寫manual-ack--visibility-timeout--delete">Application 改寫：manual ack → visibility timeout + delete</h2>
<p>Application change 維度的核心是 delivery 控制機制換了一套模型。RabbitMQ 是 broker-side 維護訊息狀態、consumer 用 <a href="/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack/nack</a> 回報處理結果；SQS 是 client-side 用 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">visibility timeout</a> + 顯式 delete、broker 不維護「處理中」以外的狀態。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># RabbitMQ 端：manual ack pattern</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">channel</span><span class="o">.</span><span class="n">basic_qos</span><span class="p">(</span><span class="n">prefetch_count</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>  <span class="c1"># 一次最多領 10 條未 ack</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="k">def</span> <span class="nf">callback</span><span class="p">(</span><span class="n">ch</span><span class="p">,</span> <span class="n">method</span><span class="p">,</span> <span class="n">properties</span><span class="p">,</span> <span class="n">body</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">process</span><span class="p">(</span><span class="n">body</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">ch</span><span class="o">.</span><span class="n">basic_ack</span><span class="p">(</span><span class="n">delivery_tag</span><span class="o">=</span><span class="n">method</span><span class="o">.</span><span class="n">delivery_tag</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="c1"># nack + requeue，或丟 DLX</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="n">ch</span><span class="o">.</span><span class="n">basic_nack</span><span class="p">(</span><span class="n">delivery_tag</span><span class="o">=</span><span class="n">method</span><span class="o">.</span><span class="n">delivery_tag</span><span class="p">,</span> <span class="n">requeue</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">channel</span><span class="o">.</span><span class="n">basic_consume</span><span class="p">(</span><span class="n">queue</span><span class="o">=</span><span class="s2">&#34;orders&#34;</span><span class="p">,</span> <span class="n">on_message_callback</span><span class="o">=</span><span class="n">callback</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">channel</span><span class="o">.</span><span class="n">start_consuming</span><span class="p">()</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># SQS 端：visibility timeout + delete pattern</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">while</span> <span class="kc">True</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">resp</span> <span class="o">=</span> <span class="n">sqs</span><span class="o">.</span><span class="n">receive_message</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="n">QueueUrl</span><span class="o">=</span><span class="n">queue_url</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="n">MaxNumberOfMessages</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span>        <span class="c1"># batch、對應 prefetch</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">WaitTimeSeconds</span><span class="o">=</span><span class="mi">20</span><span class="p">,</span>            <span class="c1"># long polling</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">VisibilityTimeout</span><span class="o">=</span><span class="mi">60</span><span class="p">,</span>          <span class="c1"># 處理中對其他 consumer 隱藏</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">for</span> <span class="n">msg</span> <span class="ow">in</span> <span class="n">resp</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;Messages&#34;</span><span class="p">,</span> <span class="p">[]):</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="n">process</span><span class="p">(</span><span class="n">msg</span><span class="p">[</span><span class="s2">&#34;Body&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="n">sqs</span><span class="o">.</span><span class="n">delete_message</span><span class="p">(</span>           <span class="c1"># 顯式 delete = ack</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">                <span class="n">QueueUrl</span><span class="o">=</span><span class="n">queue_url</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">                <span class="n">ReceiptHandle</span><span class="o">=</span><span class="n">msg</span><span class="p">[</span><span class="s2">&#34;ReceiptHandle&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">            <span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">            <span class="k">pass</span>  <span class="c1"># 不 delete、visibility timeout 後自動回 queue 重試</span></span></span></code></pre></div><p>對應關係：</p>
<ul>
<li>RabbitMQ <code>basic_ack</code> → SQS <code>delete_message</code>：處理成功的訊息要顯式刪除、否則 visibility timeout 後重新可見。「不做事」在 SQS 等於「重試」、在 RabbitMQ 等於「卡住 unacked」。</li>
<li>RabbitMQ <code>prefetch_count</code> → SQS <code>MaxNumberOfMessages</code>（上限 10）+ visibility timeout：併發控制從「broker 限制未 ack 數量」變成「一次 receive 的 batch 大小 + 隱藏時間窗」。</li>
<li>RabbitMQ <code>basic_nack(requeue=False)</code>（丟 DLX）→ SQS redrive policy：失敗不再是 application 主動丟 DLX、而是「達到 maxReceiveCount 次數後 SQS 自動送 DLQ」。</li>
<li>RabbitMQ push 模型（broker 主動推給 consumer）→ SQS pull 模型（consumer 主動 long polling）：consumer loop 結構不同、SQS 沒有 broker 主動推送、要嘛自己 poll、要嘛交給 Lambda event source mapping 代 poll。</li>
</ul>
<p>application 邏輯改動集中在 consumer 的 receive / ack / 重試三段、producer 端從 <code>basic_publish</code> 改成 <code>send_message</code> 相對單純。整體改動量取決於原本用了多少 AMQP 特性、典型情境是 consumer 端 20-40% 改寫。</p>
<h2 id="routing-收斂exchange-沒了靠-sns-fan-out-或多-queue">Routing 收斂：exchange 沒了、靠 SNS fan-out 或多 queue</h2>
<p>Components 維度的核心是 SQS 沒有 exchange、RabbitMQ 的 routing 能力要在 broker 外重建。RabbitMQ 的 <a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">exchange</a> 在 broker 內承擔分流：一條訊息經 routing key 跟 binding 決定進哪些 queue。SQS 是裸 queue、producer 直接指定 queue、沒有中間分流層。</p>
<table>
  <thead>
      <tr>
          <th>RabbitMQ routing 模式</th>
          <th>SQS 對應方案</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Direct（固定 key）</td>
          <td>直接 send 到對應 queue、routing 收斂進 producer 程式碼</td>
      </tr>
      <tr>
          <td>Fanout（廣播）</td>
          <td>SNS topic → 多個 SQS queue 訂閱（SNS-to-SQS fan-out）</td>
      </tr>
      <tr>
          <td>Topic（層級 key 匹配）</td>
          <td>SNS + message filtering（subscription filter policy）</td>
      </tr>
      <tr>
          <td>Headers</td>
          <td>SNS message attribute filtering</td>
      </tr>
  </tbody>
</table>
<p>判讀：</p>
<ul>
<li><strong>Direct exchange + 少數固定 key</strong>：最容易遷。routing 邏輯本來就是「key X 進 queue X」、改成 producer 直接 <code>send_message</code> 到對應 queue url。routing 從 broker 收斂進 application、程式碼多幾行 if/else 或 map 查表。</li>
<li><strong>Fanout（一條訊息給多個 downstream）</strong>：用 SNS-to-SQS。SNS topic 當 fan-out 點、每個 downstream 訂閱一個自己的 SQS queue。Twitch EventSub 就是這個形狀（見 <a href="/blog/backend/03-message-queue/cases/sqs-twitch-eventsub-fanout/" data-link-title="3.C54 Twitch EventSub：SNS&#43;SQS fan-out 給第三方" data-link-desc="Twitch Event Bus ~1660 events/sec 進 SNS、EventSub 用 SQS 接收 &#43; Dispatcher fan-out 給訂閱者。">3.C54 Twitch EventSub</a>）：SNS fan-out 到多個 SQS、各 consumer 獨立消費。這比 RabbitMQ fanout exchange 多一層 SNS、但換來 managed 運維。</li>
<li><strong>Topic exchange（複雜層級匹配）</strong>：SNS 的 subscription filter policy 能做 attribute-based 過濾、但表達力不如 AMQP topic 的 <code>*</code> / <code>#</code> 通配。複雜 topic routing 是「不該遷」的訊號（見下節）。</li>
</ul>
<p>關鍵取捨：SQS + SNS 把 RabbitMQ 的單一 broker（routing 內建）拆成兩個 managed 服務（SQS 排隊 + SNS 分流）。好處是各自 managed、壞處是 routing 從宣告式 binding 變成要管 SNS topic + subscription + filter policy 的組合、跨服務除錯多一層。</p>
<h2 id="什麼不該遷保留-rabbitmq-的訊號">什麼不該遷：保留 RabbitMQ 的訊號</h2>
<p>SQS 的 managed 簡潔有代價、三類用法遷過去會損失能力或增加複雜度：</p>
<p><strong>複雜 topic routing</strong>。若 RabbitMQ 重度使用 topic exchange 的 <code>*</code> / <code>#</code> 層級通配、binding 規則數十條、那 routing 的表達力是核心價值。SNS subscription filter 的 attribute 匹配做不到對等表達、勉強遷會把 broker 內的宣告式 routing 拆成散落在 SNS filter policy + application 程式碼的命令式邏輯、維護成本反而上升。GoCardless 用單一 topic exchange 當服務 mesh（見 <a href="/blog/backend/03-message-queue/cases/rabbitmq-gocardless-hutch-service-mesh/" data-link-title="3.C26 GoCardless：Hutch &#43; 單一 topic exchange service mesh" data-link-desc="GoCardless 單一 RabbitMQ cluster 作所有 service 通訊中樞、routing key 用 service.subject.action 格式、JSON 多語言可讀。">3.C26 GoCardless Hutch</a>）這類設計、routing 就是架構本身、不該拆。</p>
<p><strong>需要 broker 級 ordering</strong>。RabbitMQ 單 queue 預設 FIFO、consistent hash exchange 還能做 per-key ordering（見 <a href="/blog/backend/03-message-queue/cases/rabbitmq-wework-consistent-hash-ordering/" data-link-title="3.C28 WeWork：Consistent hash exchange 保證帳戶順序" data-link-desc="WeWork 固定數量 queue &#43; account ID hash 路由、每 queue 一個 worker &#43; exclusive consumer 保 partition-level ordering。">3.C28 WeWork hash ordering</a>）。SQS standard queue <em>無 ordering</em>；要 ordering 只能用 FIFO queue、而 FIFO 吞吐受限（每 MessageGroupId 有序、整體 3000 msg/sec with batching）。若 workload 同時要高吞吐跟嚴格 ordering、SQS FIFO 兩者不可兼得、RabbitMQ 反而更適合。</p>
<p><strong>RPC over messaging（request-reply）</strong>。RabbitMQ 的 reply-to + correlation-id 做同步 RPC 模式、SQS 沒有原生 request-reply、要自己用兩條 queue + correlation 拼、延遲也不適合（SQS 是 task queue 不是低延遲傳輸）。這類用法該考慮 <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a> 的 request-reply 或直接 HTTP。</p>
<h2 id="migration-結構漸進-cutover">Migration 結構：漸進 cutover</h2>
<p>operational redesign 的 cutover 走 dual-run、按 queue（不是按整個叢集）漸進切、每步都保留回退邊界：</p>
<ol>
<li><strong>Phase 0：scope 盤點</strong> — 列出所有 exchange / queue / binding、標註 routing 模式（direct / fanout / topic）跟 ordering 需求。判斷哪些 queue 適合遷（簡單 routing、at-least-once 夠用）、哪些保留（複雜 topic、需 broker ordering、RPC）。</li>
<li><strong>Phase 1：SQS / SNS 基礎建設</strong> — 對適合遷的 queue 建對應 SQS queue + DLQ（設 redrive policy + maxReceiveCount）、fanout 場景建 SNS topic + subscription。設好 IAM policy、visibility timeout 對齊 consumer 最大處理時間。</li>
<li><strong>Phase 2：consumer 改寫 + dual-consume</strong> — application consumer 改成 SQS pull 模型（或 Lambda event source）、先讓新 consumer 跟舊 RabbitMQ consumer <em>並存</em>、producer 暫時雙寫到 RabbitMQ + SQS、驗證 SQS 端處理正確。</li>
<li><strong>Phase 3：producer cutover</strong> — 逐 queue 把 producer 從 RabbitMQ 切到 SQS / SNS、停掉該 queue 的雙寫。這步可逆：發現問題切回 RabbitMQ producer 即可。</li>
<li><strong>Phase 4：下線 RabbitMQ queue</strong> — 確認某 queue 在 SQS 穩定運行、且 RabbitMQ 端該 queue 已排空、才停掉 RabbitMQ 對應的 exchange / queue。這是不可逆步驟、不該過早。</li>
<li><strong>Phase 5：叢集退役</strong> — 所有適合遷的 queue 都切完、RabbitMQ 只剩保留的複雜 routing queue（或完全清空）、才縮編或退役叢集。</li>
</ol>
<p>漸進 cutover 的關鍵是 <em>按 queue 切、不按叢集切</em>。每條 queue 是獨立的遷移單元、各自走 Phase 2-4、互不阻塞。複雜 routing 的 queue 可以永遠留在 RabbitMQ、形成 RabbitMQ + SQS 長期共存的混合架構。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1dlx-改-redrive-policy重試語意不對等">Case 1：DLX 改 redrive policy，重試語意不對等</h3>
<p><strong>徵兆</strong>：RabbitMQ 端用 DLX 配 message TTL 做「延遲重試 + 多層 escalation」（如 <a href="/blog/backend/03-message-queue/cases/rabbitmq-indeed-delay-dlq-escalation/" data-link-title="3.C25 Indeed：Delay queue &#43; DLQ 三層 escalation" data-link-desc="Indeed 每天 35M&#43; 職缺、設計 Requeue → Delay queue → DLQ 三層 escalation 避開 head-of-line blocking。">3.C25 Indeed Delay + DLQ</a> 的三層 retry）；遷到 SQS 後發現 redrive policy 只能設「失敗 N 次直接進 DLQ」、做不出原本的延遲重試階梯。</p>
<p><strong>根因</strong>：RabbitMQ DLX 是 routing 機制、能配 TTL + 多個中繼 queue 組出任意 escalation 拓樸；SQS redrive policy 是單一規則（maxReceiveCount 到了就送 DLQ）、沒有中繼層。兩者都叫「DLQ」、但 RabbitMQ 的是可編程 routing、SQS 的是固定計數。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>指數退避用 visibility timeout 做</strong>：失敗時 application 主動 <code>ChangeMessageVisibility</code> 延長隱藏時間、實現退避、而不是依賴 DLX TTL。</li>
<li><strong>多層 escalation 用多 queue 串</strong>：若真需要 N 層、建 N 個 SQS queue、application 失敗時把訊息 send 到下一層 queue、每層設不同 redrive policy。複雜度比 DLX 高、是「複雜 routing 不該遷」的訊號之一。</li>
<li><strong>接受簡化</strong>：多數 task queue 的重試需求是「重試幾次後進 DLQ 人工檢視」、SQS redrive policy 直接對應、不需要重建 escalation 階梯。</li>
</ol>
<h3 id="case-2prefetch-改-batch--visibility併發控制行為變了">Case 2：prefetch 改 batch + visibility，併發控制行為變了</h3>
<p><strong>徵兆</strong>：RabbitMQ 端 <code>prefetch_count=1</code> 確保 worker 一次只處理一條（公平派發、慢任務不囤積）；遷 SQS 後 consumer 一次 <code>receive_message</code> 領 10 條、其中一條慢任務拖累整批、且 visibility timeout 對整批同時計時、處理到一半超時導致前面已處理的訊息重複。</p>
<p><strong>根因</strong>：RabbitMQ prefetch 是 per-message 的未 ack 上限、broker 逐條控制；SQS 的 batch 是一次領多條、visibility timeout 對 batch 內每條<em>獨立</em>計時、但 application 若同步處理整批、慢的那條會讓後面的訊息在處理前就接近超時。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>慢任務用 batch size 1</strong>：對等 RabbitMQ <code>prefetch=1</code> 就設 <code>MaxNumberOfMessages=1</code>、一次領一條、避免批內互相拖累。</li>
<li><strong>visibility timeout 設成略高於最大處理時間</strong>：Capital One 的 SQS + Lambda 實務明示這點（見 <a href="/blog/backend/03-message-queue/cases/sqs-capital-one-visibility-timeout/" data-link-title="3.C50 Capital One：Visibility timeout 設計與 Lambda event source" data-link-desc="Capital One tech blog 講 SQS &#43; Lambda：visibility timeout 應略高於最大處理時間、Lambda 初 5 個 long polling、可擴 60/min。">3.C50 Capital One</a>）— timeout 太短重複處理、太長延遲 retry。長任務處理中主動 <code>ChangeMessageVisibility</code> 續期。</li>
<li><strong>逐條 delete 不等整批</strong>：每條處理完立刻 <code>delete_message</code>、不要等整批做完才一起刪、降低整批超時導致部分重複的風險。</li>
</ol>
<h3 id="case-3fanout-改-sns-to-sqs漏訂閱導致部分-downstream-收不到">Case 3：fanout 改 SNS-to-SQS，漏訂閱導致部分 downstream 收不到</h3>
<p><strong>徵兆</strong>：RabbitMQ fanout exchange 廣播到所有 binding queue、新增 downstream 只要 bind 上去就收得到；遷成 SNS-to-SQS 後、某個新 downstream 的 SQS queue 沒訂閱到 SNS topic、或 subscription filter policy 設錯、導致該 downstream 靜默漏訊息。</p>
<p><strong>根因</strong>：RabbitMQ fanout 的廣播是 broker 內建語意、binding 一建立就生效；SNS-to-SQS 的 fan-out 是「每個 downstream 各自建 SQS queue + 訂閱 SNS topic + 設 queue policy 允許 SNS 投遞」三步、任一步漏掉或 filter policy 寫錯就靜默漏。多一層服務 = 多一層配置出錯點。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>訂閱關係 IaC 管理</strong>：SNS subscription + SQS queue policy 用 Terraform / CloudFormation 宣告、避免手動建漏。</li>
<li><strong>驗證 fan-out 完整性</strong>：cutover 前發測試訊息、確認<em>每個</em> downstream queue 都收到（對照 RabbitMQ 端 binding 清單逐一核對）。</li>
<li><strong>filter policy 預設寬鬆</strong>：除非明確要過濾、subscription 不設 filter policy（全收）、避免「以為廣播、實際被 filter 擋掉」。</li>
</ol>
<h3 id="case-4訊息超過-256kbsqs-拒收">Case 4：訊息超過 256KB，SQS 拒收</h3>
<p><strong>徵兆</strong>：RabbitMQ 對單訊息大小無硬性低上限（受 frame_max / memory 限制、實務常見 MB 級 payload）；遷 SQS 後、原本能傳的大 payload 訊息被拒、SendMessage 報 message 超過 256KB 上限。</p>
<p><strong>根因</strong>：SQS 單訊息上限 256KB（含 message attribute）。RabbitMQ 沒有這個低上限、application 可能習慣直接把大 payload（如完整文件、序列化大物件）塞進訊息體。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Claim-check pattern</strong>：大 payload 存 S3、訊息只放 S3 物件的引用（key / presigned URL）、consumer 收到後從 S3 取。FINRA 的大檔案處理是 S3 event notification → SQS（檔案上傳 S3 後由 S3 推通知），結果同樣讓訊息只帶 S3 物件引用，但機制是 S3 觸發、不是 producer 主動 offload（見 <a href="/blog/backend/03-message-queue/cases/sqs-finra-large-file-service/" data-link-title="3.C53 FINRA：S3 → SQS notification 大檔上傳" data-link-desc="FINRA 金融監管、broker 上傳大檔、S3 → SQS notification → LFS、KMS &#43; bucket policy &#43; queue policy 三層稽核。">3.C53 FINRA Large File</a>）。</li>
<li><strong>SQS Extended Client Library</strong>：AWS 官方 library 自動把超過上限的 payload 透明存 S3、訊息存指標、consumer 端自動取回、application 程式碼幾乎不改。</li>
<li><strong>盤點 payload 大小分佈</strong>：Phase 0 audit 時量測現有訊息大小、超 256KB 的比例決定是否需要 claim-check、避免 cutover 後才發現大量訊息被拒。</li>
</ol>
<h3 id="case-5ordering-從-rabbitmq-到-sqs-fifo吞吐撞天花板">Case 5：ordering 從 RabbitMQ 到 SQS FIFO，吞吐撞天花板</h3>
<p><strong>徵兆</strong>：RabbitMQ 單 queue 提供順序消費、原本靠這個保證同一筆訂單的事件有序處理；遷 SQS standard queue 後 ordering 消失、改用 SQS FIFO queue 恢復 ordering、但吞吐從原本的數萬 msg/sec 掉到 3000 msg/sec 上限、隊列堆積。</p>
<p><strong>根因</strong>：SQS standard queue 無 ordering（為了吞吐跟可用性的設計取捨）；FIFO queue 提供 per-MessageGroupId 有序 + 去重、但整體吞吐上限 3000 msg/sec（with batching）。RabbitMQ 單 queue 的有序消費吞吐遠高於此。SQS FIFO 的吞吐上限是 300 TPS（不 batch）／ 3000 TPS（batch，後者為通用 SQS FIFO 數值）。Twilio 的 webhook buffer 文件特別點出 FIFO 300 TPS 這個限制（見 <a href="/blog/backend/03-message-queue/cases/sqs-twilio-webhook-buffer/" data-link-title="3.C58 Twilio：SQS 緩衝高流量 webhook" data-link-desc="Twilio 教用 SQS 緩衝 SMS / status callback webhook、分 queue（SMS vs callback）、long polling 減 cost、FIFO 300 TPS 上限要分片。">3.C58 Twilio webhook</a>）。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>重新審視 ordering 粒度</strong>：用 MessageGroupId 把 ordering 限縮到真正需要的範圍（如 per-訂單、per-用戶）、不同 group 平行處理、整體吞吐 = group 數 × per-group 吞吐、繞過單 queue 3000 上限。</li>
<li><strong>拆分 ordered 跟 unordered 流量</strong>：只有真需要 ordering 的訊息走 FIFO、其餘走 standard queue 拿高吞吐。多數 workload 只有一小部分需要嚴格 ordering。</li>
<li><strong>ordering 是「不該遷」的硬訊號</strong>：若 workload 整體都需要高吞吐 + 嚴格 ordering、SQS FIFO 兩者不可兼得、保留 RabbitMQ 或考慮 <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>（per-partition ordering + 高吞吐）。</li>
</ol>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>RabbitMQ（self-managed EC2）</th>
          <th>AWS SQS（managed）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>叢集 baseline</td>
          <td>3 broker（HA）+ EBS</td>
          <td>無實例</td>
      </tr>
      <tr>
          <td>運維 FTE</td>
          <td>0.5-1 FTE</td>
          <td>~0.1 FTE（IAM / alarm 配置）</td>
      </tr>
      <tr>
          <td>計費模型</td>
          <td>EC2 instance hour + EBS + 流量</td>
          <td>per-request（每百萬 request）+ 跨 region 流量</td>
      </tr>
      <tr>
          <td>吞吐上限</td>
          <td>受 broker 規格 / 網路限制</td>
          <td>standard 近乎無限、FIFO 3000 msg/sec</td>
      </tr>
      <tr>
          <td>Ordering</td>
          <td>單 queue 有序、consistent hash per-key</td>
          <td>standard 無、FIFO per-group</td>
      </tr>
      <tr>
          <td>Routing</td>
          <td>broker 內建 exchange</td>
          <td>無（需 SNS / application）</td>
      </tr>
      <tr>
          <td>訊息大小上限</td>
          <td>受 frame_max / memory（MB 級可行）</td>
          <td>256KB（超過用 S3 claim-check）</td>
      </tr>
      <tr>
          <td>監控延遲</td>
          <td>即時（Management UI）</td>
          <td>CloudWatch approximate、分鐘級</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：低到中吞吐、簡單 routing、AWS 生態的 task queue、SQS 在運維成本上顯著划算（FTE 從 0.5-1 降到約 0.1）。高吞吐 + 嚴格 ordering、或重度 exchange routing 的 workload、SQS 的 per-request 成本跟能力限制可能讓 RabbitMQ（或 Kafka）反而合適。SQS 的 cost 是用量驅動、流量大時 per-request 費用要納入評估、對照 <a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本取捨</a>。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="混合架構是常見終態">混合架構是常見終態</h3>
<p>多數遷移不會把 RabbitMQ 完全清空。簡單 task queue 遷 SQS、複雜 topic routing / broker ordering / RPC 留 RabbitMQ、形成長期共存：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[簡單 task queue / fanout]              [複雜 topic routing / RPC / ordering]
</span></span><span class="line"><span class="ln">2</span><span class="cl">        AWS SQS / SNS                              RabbitMQ
</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">   Lambda / ECS consumer                    自管叢集（縮編後）</span></span></code></pre></div><p>按 queue 漸進切的結果就是混合架構 — 不需要為了「遷乾淨」勉強把不適合的 queue 也搬過去。</p>
<h3 id="跟-rabbitmq--kafka-的對照">跟 RabbitMQ → Kafka 的對照</h3>
<p>RabbitMQ 還有另一條遷移路徑是 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">RabbitMQ → Kafka</a>（work queue → event streaming）。兩條路的差異：遷 SQS 是 <em>交出運維、能力對等簡化</em>（仍是 task queue）；遷 Kafka 是 <em>換 paradigm、要 replay / 高吞吐 streaming</em>（從任務隊列變 event log）。選哪條看的是「想擺脫運維」還是「需要 streaming 能力」、不是同一個決策。</p>
<h3 id="跟前面-migration-playbook-的結構對照">跟前面 migration playbook 的結構對照</h3>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>主導差異維度</th>
          <th>結構</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Kafka ↔ NATS</td>
          <td>Paradigm（高）</td>
          <td>partial + 混合</td>
      </tr>
      <tr>
          <td>RabbitMQ → SQS（本篇）</td>
          <td>Operational（高）</td>
          <td>Type C operational hybrid</td>
      </tr>
  </tbody>
</table>
<p><strong>結論</strong>：兩篇都是 message queue 跨 vendor、但主導差異維度不同 — Kafka ↔ NATS 卡在 paradigm（不同抽象層）、RabbitMQ → SQS 卡在 operational（運維責任轉移）。結構由主導維度決定、不是 universal phased playbook。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source / target vendor：<a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a> / <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">AWS SQS</a></li>
<li>平行 vendor：<a href="/blog/backend/03-message-queue/vendors/google-pubsub/" data-link-title="Google Cloud Pub/Sub" data-link-desc="GCP managed pub/sub、global routing、push/pull">Google Pub/Sub</a> / <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a></li>
<li>平行 migration playbook：<a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a></li>
<li>引用案例：<a href="/blog/backend/03-message-queue/cases/sqs-airbnb-dynein-delayed-jobs/" data-link-title="3.C48 Airbnb Dynein：SQS 分散式延遲任務排程" data-link-desc="Airbnb 用 SQS at-least-once &#43; DLQ 取代 Resque 單 Redis 限制、每 scheduler 1000 QPS、SQS wrap DynamoDB 處理 &gt; 15 分鐘 delay。">3.C48 Airbnb Dynein</a> / <a href="/blog/backend/03-message-queue/cases/sqs-capital-one-visibility-timeout/" data-link-title="3.C50 Capital One：Visibility timeout 設計與 Lambda event source" data-link-desc="Capital One tech blog 講 SQS &#43; Lambda：visibility timeout 應略高於最大處理時間、Lambda 初 5 個 long polling、可擴 60/min。">3.C50 Capital One</a> / <a href="/blog/backend/03-message-queue/cases/sqs-twitch-eventsub-fanout/" data-link-title="3.C54 Twitch EventSub：SNS&#43;SQS fan-out 給第三方" data-link-desc="Twitch Event Bus ~1660 events/sec 進 SNS、EventSub 用 SQS 接收 &#43; Dispatcher fan-out 給訂閱者。">3.C54 Twitch EventSub</a> / <a href="/blog/backend/03-message-queue/cases/sqs-twilio-webhook-buffer/" data-link-title="3.C58 Twilio：SQS 緩衝高流量 webhook" data-link-desc="Twilio 教用 SQS 緩衝 SMS / status callback webhook、分 queue（SMS vs callback）、long polling 減 cost、FIFO 300 TPS 上限要分片。">3.C58 Twilio webhook</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>3.C48 Airbnb Dynein：SQS 分散式延遲任務排程</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-airbnb-dynein-delayed-jobs/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-airbnb-dynein-delayed-jobs/</guid><description>&lt;p>這個案例的核心責任是說明 SQS at-least-once + DLQ 模型在工作排程的對齊邏輯。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Airbnb 構建 Dynein 分散式延遲任務排程系統取代 Resque（受限於單 Redis 實例）。明確選 SQS、利用 at-least-once delivery、dead letter queue、individual message acknowledgment、access control 與 encryption-at-rest。每個 scheduler instance 達 ~1000 QPS、可水平擴展。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>at-least-once 對工作排程「不丟資料」假設足夠、SQS wrap DynamoDB 處理 &amp;gt; 15 分鐘 delay、DLQ 分離「短暫失敗」與「永久毒訊息」。揭露 managed queue 在工作排程的取捨：trade ordering 換 scaling。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：Standard vs FIFO / DLQ 設計。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/airbnb-engineering/dynein-building-a-distributed-delayed-job-queueing-system-93ab10f05f99">Dynein: Building a Distributed Delayed Job Queueing System&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 SQS at-least-once + DLQ 模型在工作排程的對齊邏輯。</p>
<h2 id="觀察">觀察</h2>
<p>Airbnb 構建 Dynein 分散式延遲任務排程系統取代 Resque（受限於單 Redis 實例）。明確選 SQS、利用 at-least-once delivery、dead letter queue、individual message acknowledgment、access control 與 encryption-at-rest。每個 scheduler instance 達 ~1000 QPS、可水平擴展。</p>
<h2 id="判讀">判讀</h2>
<p>at-least-once 對工作排程「不丟資料」假設足夠、SQS wrap DynamoDB 處理 &gt; 15 分鐘 delay、DLQ 分離「短暫失敗」與「永久毒訊息」。揭露 managed queue 在工作排程的取捨：trade ordering 換 scaling。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：Standard vs FIFO / DLQ 設計。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</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>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://medium.com/airbnb-engineering/dynein-building-a-distributed-delayed-job-queueing-system-93ab10f05f99">Dynein: Building a Distributed Delayed Job Queueing System</a></li>
</ul>
]]></content:encoded></item><item><title>3.C49 Airbnb Inspekt：Visibility timeout 當 retry budget</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-airbnb-inspekt-data-protection/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-airbnb-inspekt-data-protection/</guid><description>&lt;p>這個案例的核心責任是說明 visibility timeout 不只是「處理時間」、可當隱式的 retry 機制。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Airbnb 的 Inspekt 隱私資料掃描系統用 SQS task queue 派發 scan task（每 table/object/app 一個 message）、Scanner nodes 水平 pull。&amp;ldquo;each message reappears N times back into the queue until a scanner node deletes it&amp;rdquo; 是 visibility timeout 在實戰的應用。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>用 message 重現次數做 retry budget、scanner 失敗時不用自管 retry table。揭露 SQS 的「不刪除即重現」是設計、不是 bug、可以當隱式 retry 機制用。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：Visibility timeout + in-flight messages。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/airbnb-engineering/automating-data-protection-at-scale-part-2-c2b8d2068216">Automating Data Protection at Scale Part 2&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 visibility timeout 不只是「處理時間」、可當隱式的 retry 機制。</p>
<h2 id="觀察">觀察</h2>
<p>Airbnb 的 Inspekt 隱私資料掃描系統用 SQS task queue 派發 scan task（每 table/object/app 一個 message）、Scanner nodes 水平 pull。&ldquo;each message reappears N times back into the queue until a scanner node deletes it&rdquo; 是 visibility timeout 在實戰的應用。</p>
<h2 id="判讀">判讀</h2>
<p>用 message 重現次數做 retry budget、scanner 失敗時不用自管 retry table。揭露 SQS 的「不刪除即重現」是設計、不是 bug、可以當隱式 retry 機制用。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：Visibility timeout + in-flight messages。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a> 與 <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>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://medium.com/airbnb-engineering/automating-data-protection-at-scale-part-2-c2b8d2068216">Automating Data Protection at Scale Part 2</a></li>
</ul>
]]></content:encoded></item><item><title>3.C50 Capital One：Visibility timeout 設計與 Lambda event source</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-capital-one-visibility-timeout/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-capital-one-visibility-timeout/</guid><description>&lt;p>Capital One 的 SQS + Lambda 實務揭露了 visibility timeout 的雙邊風險 — 太短導致重複處理、太長延遲 retry — 以及 Lambda event source mapping 的 scaling 行為跟直覺不同的地方。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>Capital One 是美國大型金融機構，tech blog 公開分享了 SQS + Lambda 的 event-driven 架構實踐。金融場景的 message 處理對正確性要求極高 — 重複處理一筆交易跟遺失一筆交易的代價都是具體的金錢損失。&lt;/p>
&lt;p>SQS 是 AWS 原生的 managed queue，Lambda 是 serverless compute。兩者搭配的 event source mapping 是 AWS 上最常見的 event-driven 入門架構 — 看起來簡單（SQS → Lambda 自動觸發），但 visibility timeout 跟 Lambda scaling 的互動有不少實務細節。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="visibility-timeout-的雙邊風險">Visibility timeout 的雙邊風險&lt;/h3>
&lt;p>SQS 的 visibility timeout 定義了「consumer 取走訊息後，其他 consumer 多久之後才能再看到這筆訊息」。它是 SQS 的核心容錯機制 — consumer 處理失敗（crash、timeout）時，visibility timeout 到期後訊息重新出現在 queue 裡，讓其他 consumer 接手。&lt;/p>
&lt;p>&lt;strong>Timeout 太短&lt;/strong>：consumer 還在處理中、visibility timeout 已到期、另一個 consumer 取走同一筆訊息開始處理 — 重複處理。金融場景的重複處理可能導致重複扣款或重複退款。&lt;/p>
&lt;p>&lt;strong>Timeout 太長&lt;/strong>：consumer 處理失敗、需要等很久 visibility timeout 才到期、訊息才重新出現 — retry 延遲。原本幾秒就能被其他 consumer 接手的訊息，要等 15 分鐘才 retry。&lt;/p>
&lt;p>Capital One 的實務建議是 visibility timeout 設為「最大預期處理時間 + 少量緩衝」。例如：最大處理時間 30 秒 → visibility timeout 設 45 秒。&lt;/p>
&lt;h3 id="lambda-event-source-mapping-的-scaling-行為">Lambda event source mapping 的 scaling 行為&lt;/h3>
&lt;p>Lambda 跟 SQS 的整合透過 event source mapping — Lambda 服務自動從 SQS long polling 取訊息、觸發 Lambda function。使用者不需要自己寫 polling 邏輯。&lt;/p>
&lt;p>Capital One 揭露的 scaling 行為跟「Lambda 自動擴展」的直覺不同：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>初始狀態&lt;/strong>：Lambda 啟動 5 個 long polling connection（poller）&lt;/li>
&lt;li>&lt;strong>Scale up&lt;/strong>：每分鐘最多新增 60 個 poller instance（每個 instance 處理一批 message）&lt;/li>
&lt;li>&lt;strong>上限&lt;/strong>：最多 1000 個並行 batch&lt;/li>
&lt;/ul>
&lt;p>這意味著突發流量（queue 瞬間湧入大量訊息）的消化速度不是即時的 — Lambda 需要數分鐘才能 scale 到足夠的並行度。在這段 ramp-up 期間，queue depth 會持續增長。&lt;/p>
&lt;h3 id="batch-size-跟-visibility-timeout-的互動">Batch size 跟 visibility timeout 的互動&lt;/h3>
&lt;p>Lambda event source mapping 預設 batch size = 10 — 一次取 10 筆訊息、用一個 Lambda invocation 處理。如果 batch 中的某一筆處理特別慢，整個 batch 的處理時間會被拉長。&lt;/p></description><content:encoded><![CDATA[<p>Capital One 的 SQS + Lambda 實務揭露了 visibility timeout 的雙邊風險 — 太短導致重複處理、太長延遲 retry — 以及 Lambda event source mapping 的 scaling 行為跟直覺不同的地方。</p>
<h2 id="業務背景">業務背景</h2>
<p>Capital One 是美國大型金融機構，tech blog 公開分享了 SQS + Lambda 的 event-driven 架構實踐。金融場景的 message 處理對正確性要求極高 — 重複處理一筆交易跟遺失一筆交易的代價都是具體的金錢損失。</p>
<p>SQS 是 AWS 原生的 managed queue，Lambda 是 serverless compute。兩者搭配的 event source mapping 是 AWS 上最常見的 event-driven 入門架構 — 看起來簡單（SQS → Lambda 自動觸發），但 visibility timeout 跟 Lambda scaling 的互動有不少實務細節。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="visibility-timeout-的雙邊風險">Visibility timeout 的雙邊風險</h3>
<p>SQS 的 visibility timeout 定義了「consumer 取走訊息後，其他 consumer 多久之後才能再看到這筆訊息」。它是 SQS 的核心容錯機制 — consumer 處理失敗（crash、timeout）時，visibility timeout 到期後訊息重新出現在 queue 裡，讓其他 consumer 接手。</p>
<p><strong>Timeout 太短</strong>：consumer 還在處理中、visibility timeout 已到期、另一個 consumer 取走同一筆訊息開始處理 — 重複處理。金融場景的重複處理可能導致重複扣款或重複退款。</p>
<p><strong>Timeout 太長</strong>：consumer 處理失敗、需要等很久 visibility timeout 才到期、訊息才重新出現 — retry 延遲。原本幾秒就能被其他 consumer 接手的訊息，要等 15 分鐘才 retry。</p>
<p>Capital One 的實務建議是 visibility timeout 設為「最大預期處理時間 + 少量緩衝」。例如：最大處理時間 30 秒 → visibility timeout 設 45 秒。</p>
<h3 id="lambda-event-source-mapping-的-scaling-行為">Lambda event source mapping 的 scaling 行為</h3>
<p>Lambda 跟 SQS 的整合透過 event source mapping — Lambda 服務自動從 SQS long polling 取訊息、觸發 Lambda function。使用者不需要自己寫 polling 邏輯。</p>
<p>Capital One 揭露的 scaling 行為跟「Lambda 自動擴展」的直覺不同：</p>
<ul>
<li><strong>初始狀態</strong>：Lambda 啟動 5 個 long polling connection（poller）</li>
<li><strong>Scale up</strong>：每分鐘最多新增 60 個 poller instance（每個 instance 處理一批 message）</li>
<li><strong>上限</strong>：最多 1000 個並行 batch</li>
</ul>
<p>這意味著突發流量（queue 瞬間湧入大量訊息）的消化速度不是即時的 — Lambda 需要數分鐘才能 scale 到足夠的並行度。在這段 ramp-up 期間，queue depth 會持續增長。</p>
<h3 id="batch-size-跟-visibility-timeout-的互動">Batch size 跟 visibility timeout 的互動</h3>
<p>Lambda event source mapping 預設 batch size = 10 — 一次取 10 筆訊息、用一個 Lambda invocation 處理。如果 batch 中的某一筆處理特別慢，整個 batch 的處理時間會被拉長。</p>
<p>Visibility timeout 要覆蓋整個 batch 的處理時間（包含最慢的那一筆），否則 batch 還在處理中、早期取走的訊息 visibility timeout 到期、被其他 poller 重新取走 — 導致重複處理。</p>
<h2 id="解法與取捨">解法與取捨</h2>
<table>
  <thead>
      <tr>
          <th>設計參數</th>
          <th>建議值</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Visibility timeout</td>
          <td>最大處理時間 + 緩衝（例 45 秒）</td>
          <td>太短重複、太長延遲 retry</td>
      </tr>
      <tr>
          <td>Batch size</td>
          <td>依處理時間變異度調整</td>
          <td>Batch 大省 invocation 費用、但延長 visibility 需求</td>
      </tr>
      <tr>
          <td>DLQ</td>
          <td>設定 maxReceiveCount（例 3 次）</td>
          <td>避免 poison message 無限 retry</td>
      </tr>
      <tr>
          <td>Concurrency limit</td>
          <td>依下游承受能力設定</td>
          <td>避免 Lambda 爆量壓垮下游 DB</td>
      </tr>
  </tbody>
</table>
<h3 id="idempotency-作為安全網">Idempotency 作為安全網</h3>
<p>Visibility timeout 無法完全避免重複處理（網路分區、Lambda timeout 等邊界條件）。Capital One 的做法是在 Lambda function 內實作 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> — 用 message ID 做去重，確保同一筆訊息被多次處理時結果相同。</p>
<p>Idempotency 把 visibility timeout 的精確度要求降低 — 即使偶爾重複處理，業務結果仍然正確。Visibility timeout 仍然需要合理設定（降低不必要的重複 invocation 成本），但 idempotency 是「即使設錯也不會造成業務錯誤」的安全網。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a>：visibility timeout、in-flight limit、Lambda event source 的進階主題</li>
<li><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>：at-least-once 語意下的 consumer 端 idempotency</li>
<li><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>：visibility timeout 是 SQS 的 delivery guarantee 機制</li>
<li><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>：DLQ + maxReceiveCount 的 retry 升級策略</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>SQS + Lambda 架構中出現訊息重複處理（CloudWatch 的 <code>ApproximateNumberOfMessagesNotVisible</code> 跟 <code>NumberOfMessagesReceived</code> 比例異常）</li>
<li>Lambda function 的 timeout 跟 SQS visibility timeout 的關係沒有明確設計</li>
<li>突發流量時 queue depth 持續增長、Lambda 的 concurrent execution 沒有立刻跟上</li>
<li>Batch processing 中的慢訊息拖慢整個 batch、造成 visibility timeout 到期</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.capitalone.com/tech/cloud/using-aws-solutions-for-event-driven-serverless-architectures/">Using AWS Solutions for Event-Driven Serverless Architectures</a></li>
</ul>
]]></content:encoded></item><item><title>3.C51 Atlassian JiRT：Kinesis + SQS subscription</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-atlassian-jirt-kinesis-sqs/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-atlassian-jirt-kinesis-sqs/</guid><description>&lt;p>這個案例的核心責任是說明 SQS 作為 streaming source 的 per-consumer subscription 模式。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Atlassian 內部 event bus StreamHub 底層用 Kinesis、但「每個 consumer 自己準備 SQS queue 接收 event」。JiRT 即時服務透過此模式把輪詢式（~1 min）改成 event-driven（秒級）。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>在 Kinesis 上面疊 SQS 讓 consumer 各自設定 retention、各自獨立 visibility timeout。揭露「stream + per-consumer queue」是 fan-out 場景的常見複合 pattern、不是 streaming vs queue 二選一。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：Standard vs FIFO / SQS 作為 fan-out subscriber。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁&lt;/a>（streaming + queue 對照）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.atlassian.com/blog/atlassian-engineering/using-an-event-driven-architecture-to-improve-jira-software-responsiveness">Using an Event-Driven Architecture to Improve Jira Software Responsiveness&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 SQS 作為 streaming source 的 per-consumer subscription 模式。</p>
<h2 id="觀察">觀察</h2>
<p>Atlassian 內部 event bus StreamHub 底層用 Kinesis、但「每個 consumer 自己準備 SQS queue 接收 event」。JiRT 即時服務透過此模式把輪詢式（~1 min）改成 event-driven（秒級）。</p>
<h2 id="判讀">判讀</h2>
<p>在 Kinesis 上面疊 SQS 讓 consumer 各自設定 retention、各自獨立 visibility timeout。揭露「stream + per-consumer queue」是 fan-out 場景的常見複合 pattern、不是 streaming vs queue 二選一。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：Standard vs FIFO / SQS 作為 fan-out subscriber。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</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 vendor 頁</a>（streaming + queue 對照）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.atlassian.com/blog/atlassian-engineering/using-an-event-driven-architecture-to-improve-jira-software-responsiveness">Using an Event-Driven Architecture to Improve Jira Software Responsiveness</a></li>
</ul>
]]></content:encoded></item><item><title>3.C52 Nielsen：Spark on EKS 雙 SQS 工作流</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-nielsen-spark-eks-dual-queue/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-nielsen-spark-eks-dual-queue/</guid><description>&lt;p>這個案例的核心責任是說明 SQS queue depth 作為 autoscale 訊號的真實案例。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Nielsen 每日處理 25 TB / 30 billion event。架構用兩個 SQS queue：work queue（待處理工作項）+ completion queue（回報完成）。Lambda 從 DB 拉檔案、組成 work item 推進 work queue、EKS pod 拉取處理、處理完寫 completion queue。基於 queue depth 自動擴 pod。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>不用直接 Lambda invoke（pod 上跑長時間 Spark workload）、queue depth 當 backlog signal driving autoscale。揭露長 workload 場景該用 pod + queue depth、不是 Lambda function。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：CloudWatch metric + alarm / Standard queue / 長 workload autoscaling。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-trivago-keda-scale-to-zero/" data-link-title="3.C22 Trivago：KEDA scale-to-zero by Kafka lag" data-link-desc="Trivago 50&amp;#43; Kafka sink、CPU/mem autoscaling 無效（I/O bottleneck）、KEDA 以 consumer lag 為訊號達到 scale-to-zero。">3.C22 Trivago KEDA&lt;/a>（lag-based autoscale 對照）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/blogs/architecture/how-nielsen-uses-serverless-concepts-on-amazon-eks-for-big-data-processing-with-spark-workloads/">How Nielsen Uses Serverless Concepts on Amazon EKS for Big Data Spark Workloads&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 SQS queue depth 作為 autoscale 訊號的真實案例。</p>
<h2 id="觀察">觀察</h2>
<p>Nielsen 每日處理 25 TB / 30 billion event。架構用兩個 SQS queue：work queue（待處理工作項）+ completion queue（回報完成）。Lambda 從 DB 拉檔案、組成 work item 推進 work queue、EKS pod 拉取處理、處理完寫 completion queue。基於 queue depth 自動擴 pod。</p>
<h2 id="判讀">判讀</h2>
<p>不用直接 Lambda invoke（pod 上跑長時間 Spark workload）、queue depth 當 backlog signal driving autoscale。揭露長 workload 場景該用 pod + queue depth、不是 Lambda function。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：CloudWatch metric + alarm / Standard queue / 長 workload autoscaling。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/kafka-trivago-keda-scale-to-zero/" data-link-title="3.C22 Trivago：KEDA scale-to-zero by Kafka lag" data-link-desc="Trivago 50&#43; Kafka sink、CPU/mem autoscaling 無效（I/O bottleneck）、KEDA 以 consumer lag 為訊號達到 scale-to-zero。">3.C22 Trivago KEDA</a>（lag-based autoscale 對照）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/blogs/architecture/how-nielsen-uses-serverless-concepts-on-amazon-eks-for-big-data-processing-with-spark-workloads/">How Nielsen Uses Serverless Concepts on Amazon EKS for Big Data Spark Workloads</a></li>
</ul>
]]></content:encoded></item><item><title>3.C53 FINRA：S3 → SQS notification 大檔上傳</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-finra-large-file-service/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-finra-large-file-service/</guid><description>&lt;p>這個案例的核心責任是說明 S3 event notification 是 SQS 最經典 trigger、合規場景的 IAM 多層設定。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>FINRA 金融監管機構、處理 broker-dealer 上傳大檔。Large File Service 用 S3 → SQS 通知模式：使用者上傳完 loading dock bucket、S3 推 SQS message 給 LFS、移檔後再推 &amp;ldquo;file available&amp;rdquo; SQS message 給下游。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>S3 通知是 SQS 最經典 trigger、KMS + bucket policy + queue 權限的合規場景（金融業要保留稽核軌跡）。揭露金融場景的 IAM 設計不是一道權限、是多層稽核軌跡。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：SQS + Lambda event source / IAM + Cross-account。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">7 security 模組&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.finra.org/about/how-we-operate/technology/blog/large-file-service-securely-uploading-large-files-to-s3">FINRA Large File Service&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 S3 event notification 是 SQS 最經典 trigger、合規場景的 IAM 多層設定。</p>
<h2 id="觀察">觀察</h2>
<p>FINRA 金融監管機構、處理 broker-dealer 上傳大檔。Large File Service 用 S3 → SQS 通知模式：使用者上傳完 loading dock bucket、S3 推 SQS message 給 LFS、移檔後再推 &ldquo;file available&rdquo; SQS message 給下游。</p>
<h2 id="判讀">判讀</h2>
<p>S3 通知是 SQS 最經典 trigger、KMS + bucket policy + queue 權限的合規場景（金融業要保留稽核軌跡）。揭露金融場景的 IAM 設計不是一道權限、是多層稽核軌跡。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：SQS + Lambda event source / IAM + Cross-account。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a> 與 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">7 security 模組</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.finra.org/about/how-we-operate/technology/blog/large-file-service-securely-uploading-large-files-to-s3">FINRA Large File Service</a></li>
</ul>
]]></content:encoded></item><item><title>3.C54 Twitch EventSub：SNS+SQS fan-out 給第三方</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-twitch-eventsub-fanout/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-twitch-eventsub-fanout/</guid><description>&lt;p>這個案例的核心責任是說明 SNS-SQS fan-out + dispatcher pattern 的實戰。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Twitch 內部 Event Bus 發佈 ~1660 events/sec 到 SNS。EventSub（給第三方應用訂閱 Twitch 事件）用 SQS 接收 async notification、再由 Dispatcher fan-out 給各訂閱者。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>fan-out 後每個 consumer 要自己一個 queue。揭露 SNS → SQS 是 AWS 生態的 fan-out 標配、SQS 是第三方訂閱的 buffer 層、Dispatcher 是 application 級別的分發責任。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：Standard queue + SQS + Lambda / SNS-SQS fan-out。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-atlassian-jirt-kinesis-sqs/" data-link-title="3.C51 Atlassian JiRT：Kinesis &amp;#43; SQS subscription" data-link-desc="Atlassian StreamHub Kinesis 底層、每 consumer 自己一個 SQS queue、JiRT 把輪詢 1 min 改成秒級 event-driven。">3.C51 Atlassian JiRT&lt;/a>（subscription 對照）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.twitch.tv/en/2023/09/28/twitch-state-of-engineering-2023/">Twitch State of Engineering 2023&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 SNS-SQS fan-out + dispatcher pattern 的實戰。</p>
<h2 id="觀察">觀察</h2>
<p>Twitch 內部 Event Bus 發佈 ~1660 events/sec 到 SNS。EventSub（給第三方應用訂閱 Twitch 事件）用 SQS 接收 async notification、再由 Dispatcher fan-out 給各訂閱者。</p>
<h2 id="判讀">判讀</h2>
<p>fan-out 後每個 consumer 要自己一個 queue。揭露 SNS → SQS 是 AWS 生態的 fan-out 標配、SQS 是第三方訂閱的 buffer 層、Dispatcher 是 application 級別的分發責任。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：Standard queue + SQS + Lambda / SNS-SQS fan-out。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/sqs-atlassian-jirt-kinesis-sqs/" data-link-title="3.C51 Atlassian JiRT：Kinesis &#43; SQS subscription" data-link-desc="Atlassian StreamHub Kinesis 底層、每 consumer 自己一個 SQS queue、JiRT 把輪詢 1 min 改成秒級 event-driven。">3.C51 Atlassian JiRT</a>（subscription 對照）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://blog.twitch.tv/en/2023/09/28/twitch-state-of-engineering-2023/">Twitch State of Engineering 2023</a></li>
</ul>
]]></content:encoded></item><item><title>3.C55 SmugMug：SQS 驅動可重放搜尋管線</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-smugmug-search-pipeline/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-smugmug-search-pipeline/</guid><description>&lt;p>這個案例的核心責任是說明 SQS 作為「workload generator」的分散式平行化角色。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>SmugMug 用 SQS 兩種模式：(1) backfill — script 推 DynamoDB scan-segment 指令進 SQS、Lambda 拉取做平行掃描寫 OpenSearch、(2) 鏡像查詢 — production query 推副本 SQS、Lambda replay 到 replica domain。每小時可 index &amp;gt; 1 billion document、不影響 production。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>SQS 作為「workload generator」分散式平行化、不需協調 worker 數量。揭露 SQS 不只是「事件 queue」、也是「並行任務分散」的協調基礎。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：Standard queue / Long polling / SQS + Lambda event source。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/blogs/big-data/smugmugs-durable-search-pipelines-for-amazon-opensearch-service/">SmugMug&amp;rsquo;s Durable Search Pipelines for Amazon OpenSearch Service&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 SQS 作為「workload generator」的分散式平行化角色。</p>
<h2 id="觀察">觀察</h2>
<p>SmugMug 用 SQS 兩種模式：(1) backfill — script 推 DynamoDB scan-segment 指令進 SQS、Lambda 拉取做平行掃描寫 OpenSearch、(2) 鏡像查詢 — production query 推副本 SQS、Lambda replay 到 replica domain。每小時可 index &gt; 1 billion document、不影響 production。</p>
<h2 id="判讀">判讀</h2>
<p>SQS 作為「workload generator」分散式平行化、不需協調 worker 數量。揭露 SQS 不只是「事件 queue」、也是「並行任務分散」的協調基礎。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：Standard queue / Long polling / SQS + Lambda event source。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a> 與 <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>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/blogs/big-data/smugmugs-durable-search-pipelines-for-amazon-opensearch-service/">SmugMug&rsquo;s Durable Search Pipelines for Amazon OpenSearch Service</a></li>
</ul>
]]></content:encoded></item><item><title>3.C56 PostNL EBE：完整 DLQ + retention + redrive 設計</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-postnl-mission-critical-ebe/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-postnl-mission-critical-ebe/</guid><description>&lt;p>這個案例的核心責任是業內真正完整的 DLQ + redrive + retention 設計案例、不是 demo 規模。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>PostNL（荷蘭最大物流商、每天 6.9M 信件 + 1.1M 包裹）的 Event Broker E-commerce 系統每天處理 ~10M message。完整列出 SQS 配置：每 producer/consumer 隔離 stack（最小爆炸半徑）、3 天 replay via EventBridge、exponential backoff with jitter、24 小時內最多 retry 100 次、final DLQ 允許 consumer 自己 redrive。max receive count 設 1 觸發 DLQ 告警。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>「每 producer/consumer 隔離 stack」是 mission-critical 系統的 blast radius 設計、不只是 queue 配置。揭露 production-grade SQS 設計含三件事：隔離 + retry 政策 + redrive 流程。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：DLQ 設計 / CloudWatch alarm / Cost 模型。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁&lt;/a> 與 &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 反例：語義誤配&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/postnl-engineering/design-a-mission-critical-serverless-application-for-high-resilience-2858bf11360a">Designing a Mission-Critical Serverless Application for High Resilience&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是業內真正完整的 DLQ + redrive + retention 設計案例、不是 demo 規模。</p>
<h2 id="觀察">觀察</h2>
<p>PostNL（荷蘭最大物流商、每天 6.9M 信件 + 1.1M 包裹）的 Event Broker E-commerce 系統每天處理 ~10M message。完整列出 SQS 配置：每 producer/consumer 隔離 stack（最小爆炸半徑）、3 天 replay via EventBridge、exponential backoff with jitter、24 小時內最多 retry 100 次、final DLQ 允許 consumer 自己 redrive。max receive count 設 1 觸發 DLQ 告警。</p>
<h2 id="判讀">判讀</h2>
<p>「每 producer/consumer 隔離 stack」是 mission-critical 系統的 blast radius 設計、不只是 queue 配置。揭露 production-grade SQS 設計含三件事：隔離 + retry 政策 + redrive 流程。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：DLQ 設計 / CloudWatch alarm / Cost 模型。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</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>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://medium.com/postnl-engineering/design-a-mission-critical-serverless-application-for-high-resilience-2858bf11360a">Designing a Mission-Critical Serverless Application for High Resilience</a></li>
</ul>
]]></content:encoded></item><item><title>3.C57 Lob：自家 fork @lob/sqs-consumer 修 FIFO bug</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-lob-sqs-consumer-library/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-lob-sqs-consumer-library/</guid><description>&lt;p>這個案例的核心責任是說明真實 production library 維護成本、FIFO consumer 的隱性 bug。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Lob（programmatic mail API）原本用 bbc/sqs-consumer 但被鎖在 AWS SDK v2。他們 fork 出 @lob/sqs-consumer：支援 SDK v3（模組化 import 縮 bundle、TypeScript 一級支援、async/await）、修正原 library 對 FIFO queue 的 bug。SQS 用在 Lob API 跟其他內部 service。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>不能只靠 SDK 原生 API、SDK 升級會逼出 library 維護議題。揭露「FIFO queue 跟 standard queue 的 client 行為差異」是 library 層的隱性 bug 來源。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：Standard vs FIFO / Long polling / Client library 維護。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.lob.com/blog/lob-sqs-consumer">@lob/sqs-consumer&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明真實 production library 維護成本、FIFO consumer 的隱性 bug。</p>
<h2 id="觀察">觀察</h2>
<p>Lob（programmatic mail API）原本用 bbc/sqs-consumer 但被鎖在 AWS SDK v2。他們 fork 出 @lob/sqs-consumer：支援 SDK v3（模組化 import 縮 bundle、TypeScript 一級支援、async/await）、修正原 library 對 FIFO queue 的 bug。SQS 用在 Lob API 跟其他內部 service。</p>
<h2 id="判讀">判讀</h2>
<p>不能只靠 SDK 原生 API、SDK 升級會逼出 library 維護議題。揭露「FIFO queue 跟 standard queue 的 client 行為差異」是 library 層的隱性 bug 來源。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：Standard vs FIFO / Long polling / Client library 維護。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a> 與 <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>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.lob.com/blog/lob-sqs-consumer">@lob/sqs-consumer</a></li>
</ul>
]]></content:encoded></item><item><title>3.C58 Twilio：SQS 緩衝高流量 webhook</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-twilio-webhook-buffer/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-twilio-webhook-buffer/</guid><description>&lt;p>這個案例的核心責任是說明 webhook → SQS buffer 是 Twilio 推薦的 pattern、FIFO TPS 上限的分片實務。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Twilio 自己 engineering blog 教使用者用 SQS 緩衝來自 Twilio 的高流量 SMS / status callback webhook（避免下游 app 來不及處理）。用 separate queue 區分 SMS vs status callback、long polling 減少空 API call、特別點出 FIFO 300 TPS 上限要分 queue。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Webhook 是 push、下游可能來不及、SQS 當 buffer 是常見 pattern。揭露 FIFO 的 300 TPS 上限是 hard limit、要設計分片才能擴張。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：Long polling / Standard vs FIFO。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.twilio.com/en-us/blog/handling-high-volume-inbound-sms-and-webhooks-with-twilio-functions-and-amazon-sqs-html">Handling High Volume Inbound SMS and Webhooks with Twilio Functions and Amazon SQS&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 webhook → SQS buffer 是 Twilio 推薦的 pattern、FIFO TPS 上限的分片實務。</p>
<h2 id="觀察">觀察</h2>
<p>Twilio 自己 engineering blog 教使用者用 SQS 緩衝來自 Twilio 的高流量 SMS / status callback webhook（避免下游 app 來不及處理）。用 separate queue 區分 SMS vs status callback、long polling 減少空 API call、特別點出 FIFO 300 TPS 上限要分 queue。</p>
<h2 id="判讀">判讀</h2>
<p>Webhook 是 push、下游可能來不及、SQS 當 buffer 是常見 pattern。揭露 FIFO 的 300 TPS 上限是 hard limit、要設計分片才能擴張。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：Long polling / Standard vs FIFO。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</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>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.twilio.com/en-us/blog/handling-high-volume-inbound-sms-and-webhooks-with-twilio-functions-and-amazon-sqs-html">Handling High Volume Inbound SMS and Webhooks with Twilio Functions and Amazon SQS</a></li>
</ul>
]]></content:encoded></item><item><title>3.C59 Rapid7：SQS 100 億 message/day 規模</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-rapid7-scale-billion-messages/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/sqs-rapid7-scale-billion-messages/</guid><description>&lt;p>這個案例的核心責任是建立 SQS 在 10 billion+/day 規模下的成本結構與量級參考點。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Rapid7 Platform Software Architect 公開引述：「SQS 是我們架構的關鍵元件、讓我們 scale 到處理 10s of billions of messages per day。」是 AWS 官方文中具名客戶 quote、非 marketing 概括。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>SQS 在百億訊息/日規模下仍可用、是 scale 的具體量級參考點。揭露 SQS request-based 計費在這個規模下、cost 模型該被認真評估。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>SQS 進階主題：Cost 模型 / Standard queue。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本取捨&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/blogs/aws/amazon-sqs-15-years-and-still-queueing/">Amazon SQS — 15 Years and Still Queueing&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是建立 SQS 在 10 billion+/day 規模下的成本結構與量級參考點。</p>
<h2 id="觀察">觀察</h2>
<p>Rapid7 Platform Software Architect 公開引述：「SQS 是我們架構的關鍵元件、讓我們 scale 到處理 10s of billions of messages per day。」是 AWS 官方文中具名客戶 quote、非 marketing 概括。</p>
<h2 id="判讀">判讀</h2>
<p>SQS 在百億訊息/日規模下仍可用、是 scale 的具體量級參考點。揭露 SQS request-based 計費在這個規模下、cost 模型該被認真評估。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>SQS 進階主題：Cost 模型 / Standard queue。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS vendor 頁</a> 與 <a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本取捨</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/blogs/aws/amazon-sqs-15-years-and-still-queueing/">Amazon SQS — 15 Years and Still Queueing</a></li>
</ul>
]]></content:encoded></item></channel></rss>