<?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>Buffering on Tarragon</title><link>https://tarrragon.github.io/blog/tags/buffering/</link><description>Recent content in Buffering on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sat, 20 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/buffering/index.xml" rel="self" type="application/rss+xml"/><item><title>Queue 緩衝</title><link>https://tarrragon.github.io/blog/devops/07-burst-traffic/queue-buffering/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/devops/07-burst-traffic/queue-buffering/</guid><description>&lt;p>Message queue 放在 ingestion（接收事件）和 processing（寫入 storage）之間，把兩者解耦。Ingestion 只負責驗證和寫入 queue，processing 按自己的速度從 queue 消費。Queue 做 burst 的時間緩衝 — 高峰時 queue 積壓、低峰時 worker 追上。&lt;/p>
&lt;h2 id="為什麼不直接寫-db">為什麼不直接寫 DB&lt;/h2>
&lt;p>直接寫 DB（SQLite / PostgreSQL）的問題是 ingestion 速度被 DB 寫入速度限制。DB 寫入慢（鎖定、WAL flush、索引更新）時，HTTP handler 的 goroutine 等在 &lt;code>Storage.Store()&lt;/code> 上 — goroutine 積壓 → 記憶體上升 → 最終 OOM 或 response timeout。&lt;/p>
&lt;p>Queue 的解決方式是把「接收」和「寫入」分開：接收端只做 JSON 驗證 + 寫入 queue（微秒級），處理端從 queue 讀取 + 寫入 DB（毫秒級）。接收端的吞吐量不再受 DB 限制。&lt;/p>
&lt;h3 id="取捨">取捨&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>直接寫 DB&lt;/th>
 &lt;th>經過 Queue&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>延遲&lt;/td>
 &lt;td>事件寫完 DB 即可查詢&lt;/td>
 &lt;td>事件要等 worker 消費後才可查詢&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>吞吐&lt;/td>
 &lt;td>受 DB 寫入速度限制&lt;/td>
 &lt;td>受 queue 寫入速度限制（通常遠高於 DB）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>複雜度&lt;/td>
 &lt;td>一個元件&lt;/td>
 &lt;td>三個元件（collector + queue + worker）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>故障模式&lt;/td>
 &lt;td>DB 掛了事件丟失（除非有背壓）&lt;/td>
 &lt;td>Queue 做持久化，DB 掛了事件在 queue 等待&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>自用工具場景不需要 queue — 單 collector + SQLite 的直接寫入足夠。Queue 的引入條件是「直接寫 DB 的背壓開始頻繁觸發」。&lt;/p>
&lt;h2 id="候選類型">候選類型&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Queue&lt;/th>
 &lt;th>特點&lt;/th>
 &lt;th>適用場景&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Kafka&lt;/strong>&lt;/td>
 &lt;td>高吞吐、持久化、消費者群組&lt;/td>
 &lt;td>大規模（&amp;gt; 10 萬 events/sec）、多消費者&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>NATS JetStream&lt;/strong>&lt;/td>
 &lt;td>輕量、低延遲、Go 原生&lt;/td>
 &lt;td>中型（千 ~ 萬 events/sec）、Go 生態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Redis Streams&lt;/strong>&lt;/td>
 &lt;td>用既有 Redis、XADD/XREAD API&lt;/td>
 &lt;td>中型、已有 Redis 基礎設施&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="選型判斷">選型判斷&lt;/h3>
&lt;p>已有 Redis → 先用 Redis Streams（零新增元件）。Go 為主的技術棧 → NATS JetStream（Go 原生 client、單 binary 部署）。需要跨消費者群組或日誌級持久化 → Kafka。&lt;/p>
&lt;h3 id="引入條件">引入條件&lt;/h3>
&lt;p>Queue 的引入是架構複雜度的顯著上升（一個元件變三個）。明確的觸發條件：&lt;/p>
&lt;ul>
&lt;li>背壓（429 回應）頻繁觸發（每天 &amp;gt; 100 次）且持續（不只是瞬間 burst）&lt;/li>
&lt;li>寫入延遲的 P95 超過 500ms（DB 成為瓶頸）&lt;/li>
&lt;li>需要多個 consumer（同一批事件要送到不同的下游 — analytics DB、alert engine、archive）&lt;/li>
&lt;/ul>
&lt;h2 id="監控系統的-queue-架構">監控系統的 Queue 架構&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">SDK ──→ Collector (ingestion only)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> ├─ 驗證 JSON Schema
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> ├─ Redaction
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> └─ 寫入 Queue
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> ├── Worker A → PostgreSQL（主 storage）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> ├── Worker B → 降採樣 → Summary tables
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl"> └── Worker C → Rule engine → Alert&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Collector 瘦身為 ingestion-only — 只做接收、驗證、redaction 和寫入 queue。Storage 寫入、降採樣、rule engine 都移到 worker 群。Collector 的吞吐瓶頸從 DB 寫入變成 queue 寫入（queue 的寫入吞吐通常是 DB 的 10-100 倍）。&lt;/p></description><content:encoded><![CDATA[<p>Message queue 放在 ingestion（接收事件）和 processing（寫入 storage）之間，把兩者解耦。Ingestion 只負責驗證和寫入 queue，processing 按自己的速度從 queue 消費。Queue 做 burst 的時間緩衝 — 高峰時 queue 積壓、低峰時 worker 追上。</p>
<h2 id="為什麼不直接寫-db">為什麼不直接寫 DB</h2>
<p>直接寫 DB（SQLite / PostgreSQL）的問題是 ingestion 速度被 DB 寫入速度限制。DB 寫入慢（鎖定、WAL flush、索引更新）時，HTTP handler 的 goroutine 等在 <code>Storage.Store()</code> 上 — goroutine 積壓 → 記憶體上升 → 最終 OOM 或 response timeout。</p>
<p>Queue 的解決方式是把「接收」和「寫入」分開：接收端只做 JSON 驗證 + 寫入 queue（微秒級），處理端從 queue 讀取 + 寫入 DB（毫秒級）。接收端的吞吐量不再受 DB 限制。</p>
<h3 id="取捨">取捨</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>直接寫 DB</th>
          <th>經過 Queue</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>延遲</td>
          <td>事件寫完 DB 即可查詢</td>
          <td>事件要等 worker 消費後才可查詢</td>
      </tr>
      <tr>
          <td>吞吐</td>
          <td>受 DB 寫入速度限制</td>
          <td>受 queue 寫入速度限制（通常遠高於 DB）</td>
      </tr>
      <tr>
          <td>複雜度</td>
          <td>一個元件</td>
          <td>三個元件（collector + queue + worker）</td>
      </tr>
      <tr>
          <td>故障模式</td>
          <td>DB 掛了事件丟失（除非有背壓）</td>
          <td>Queue 做持久化，DB 掛了事件在 queue 等待</td>
      </tr>
  </tbody>
</table>
<p>自用工具場景不需要 queue — 單 collector + SQLite 的直接寫入足夠。Queue 的引入條件是「直接寫 DB 的背壓開始頻繁觸發」。</p>
<h2 id="候選類型">候選類型</h2>
<table>
  <thead>
      <tr>
          <th>Queue</th>
          <th>特點</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Kafka</strong></td>
          <td>高吞吐、持久化、消費者群組</td>
          <td>大規模（&gt; 10 萬 events/sec）、多消費者</td>
      </tr>
      <tr>
          <td><strong>NATS JetStream</strong></td>
          <td>輕量、低延遲、Go 原生</td>
          <td>中型（千 ~ 萬 events/sec）、Go 生態</td>
      </tr>
      <tr>
          <td><strong>Redis Streams</strong></td>
          <td>用既有 Redis、XADD/XREAD API</td>
          <td>中型、已有 Redis 基礎設施</td>
      </tr>
  </tbody>
</table>
<h3 id="選型判斷">選型判斷</h3>
<p>已有 Redis → 先用 Redis Streams（零新增元件）。Go 為主的技術棧 → NATS JetStream（Go 原生 client、單 binary 部署）。需要跨消費者群組或日誌級持久化 → Kafka。</p>
<h3 id="引入條件">引入條件</h3>
<p>Queue 的引入是架構複雜度的顯著上升（一個元件變三個）。明確的觸發條件：</p>
<ul>
<li>背壓（429 回應）頻繁觸發（每天 &gt; 100 次）且持續（不只是瞬間 burst）</li>
<li>寫入延遲的 P95 超過 500ms（DB 成為瓶頸）</li>
<li>需要多個 consumer（同一批事件要送到不同的下游 — analytics DB、alert engine、archive）</li>
</ul>
<h2 id="監控系統的-queue-架構">監控系統的 Queue 架構</h2>





<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">SDK ──→ Collector (ingestion only)
</span></span><span class="line"><span class="ln">2</span><span class="cl">           │
</span></span><span class="line"><span class="ln">3</span><span class="cl">           ├─ 驗證 JSON Schema
</span></span><span class="line"><span class="ln">4</span><span class="cl">           ├─ Redaction
</span></span><span class="line"><span class="ln">5</span><span class="cl">           └─ 寫入 Queue
</span></span><span class="line"><span class="ln">6</span><span class="cl">                 │
</span></span><span class="line"><span class="ln">7</span><span class="cl">                 ├── Worker A → PostgreSQL（主 storage）
</span></span><span class="line"><span class="ln">8</span><span class="cl">                 ├── Worker B → 降採樣 → Summary tables
</span></span><span class="line"><span class="ln">9</span><span class="cl">                 └── Worker C → Rule engine → Alert</span></span></code></pre></div><p>Collector 瘦身為 ingestion-only — 只做接收、驗證、redaction 和寫入 queue。Storage 寫入、降採樣、rule engine 都移到 worker 群。Collector 的吞吐瓶頸從 DB 寫入變成 queue 寫入（queue 的寫入吞吐通常是 DB 的 10-100 倍）。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>突發流量的分類 → <a href="/blog/devops/07-burst-traffic/burst-classification/" data-link-title="突發流量的分類" data-link-desc="可預期 vs 不可預期的突發流量 — 不同來源、持續時間和倍率決定不同的應對策略">突發流量的分類</a></li>
<li>降級策略 → <a href="/blog/devops/07-burst-traffic/degradation-strategy/" data-link-title="降級策略" data-link-desc="系統超載時犧牲什麼保住什麼 — 動態取樣、事件優先級、功能降級、聚合前移四種策略">降級策略</a></li>
<li>規模分級的完整應對 → <a href="/blog/devops/07-burst-traffic/scale-tier-response/" data-link-title="規模分級應對表" data-link-desc="自用級 → 中型 → 大型 → 商業網站級的四級應對方案 — 每級的觸發條件、架構組成和成本">規模分級應對表</a></li>
<li>Queue 的選型和操作實務 → <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">backend 非同步佇列</a></li>
</ul>
]]></content:encoded></item></channel></rss>