<?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>Backpressure on Tarragon</title><link>https://tarrragon.github.io/blog/tags/backpressure/</link><description>Recent content in Backpressure on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 24 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/backpressure/index.xml" rel="self" type="application/rss+xml"/><item><title>背壓機制</title><link>https://tarrragon.github.io/blog/devops/03-traffic-management/backpressure/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/devops/03-traffic-management/backpressure/</guid><description>&lt;p>背壓是一種被動的流量控制機制 — 當下游（處理端）的速度跟不上上游（請求端）時，下游透過訊號讓上游知道「慢一點」。背壓不拒絕請求，而是讓請求的發送者自己決定要等待、重試還是放棄。&lt;/p>
&lt;h2 id="背壓-vs-rate-limit">背壓 vs Rate Limit&lt;/h2>
&lt;p>背壓和 rate limit 都是流量控制，但觸發邏輯不同：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>背壓&lt;/th>
 &lt;th>Rate Limit&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>觸發條件&lt;/td>
 &lt;td>下游實際變慢了（buffer 滿）&lt;/td>
 &lt;td>請求速率超過預設上限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>性質&lt;/td>
 &lt;td>被動（根據實際負載）&lt;/td>
 &lt;td>主動（根據預設規則）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>訊號&lt;/td>
 &lt;td>HTTP 429 + Retry-After / TCP 窗口縮小 / channel 阻塞&lt;/td>
 &lt;td>HTTP 429 + 固定的 rate limit header&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>發送者行為&lt;/td>
 &lt;td>根據 Retry-After 動態調整&lt;/td>
 &lt;td>等待限速窗口重設&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>背壓在系統承載達到上限時才觸發，rate limit 在到達預設上限時就觸發（即使系統還有餘裕）。兩者互補：rate limit 防止單一來源打爆系統，背壓防止所有來源加起來打爆系統。&lt;/p>
&lt;h2 id="實作模式">實作模式&lt;/h2>
&lt;h3 id="有限-buffer--回壓訊號">有限 buffer + 回壓訊號&lt;/h3>
&lt;p>最常見的背壓實作是在處理管線中加一個有限容量的 buffer。Buffer 滿了代表下游處理不完，這時對新請求回傳「忙碌」訊號。&lt;/p>
&lt;p>在 Go 的 HTTP server 中，buffer 可以是一個有限容量的 channel：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">ingestCh&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="nx">Event&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10000&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 有限 buffer&lt;/span>
&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">&lt;span class="kd">func&lt;/span> &lt;span class="nf">handleIngest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ResponseWriter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Request&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">event&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nf">parseEvent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">select&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="nx">ingestCh&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteHeader&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">StatusAccepted&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 202&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">default&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Header&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nf">Set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Retry-After&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;5&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteHeader&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">StatusTooManyRequests&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 429&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Buffer 容量的選擇取決於下游的處理速度和可接受的記憶體用量。每個 event 約 1KB 時，10000 容量的 buffer 佔 ~10MB — 對多數服務來說可以接受。&lt;/p>
&lt;h3 id="http-429--retry-after">HTTP 429 + Retry-After&lt;/h3>
&lt;p>HTTP 429 Too Many Requests 是標準的回壓訊號。&lt;code>Retry-After&lt;/code> header 告訴 client 多少秒後重試。&lt;/p>
&lt;p>&lt;code>Retry-After&lt;/code> 的值可以是固定的（如 5 秒），也可以根據 buffer 的填充程度動態計算 — buffer 越滿、Retry-After 越長。&lt;/p>
&lt;h3 id="tcp-層的背壓">TCP 層的背壓&lt;/h3>
&lt;p>TCP 協議本身有背壓機制 — 接收端的 receive window 縮小時，發送端自動減速。但 HTTP 層的背壓比 TCP 層更精確，因為 HTTP 可以回傳語意化的狀態碼和 header，client 可以根據語意做出更智慧的回應（如優先重試 error 事件、放棄 event 事件）。&lt;/p>
&lt;h2 id="監控系統的應用">監控系統的應用&lt;/h2>
&lt;p>監控系統的 &lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">collector&lt;/a> 是背壓的典型場景：多個 SDK 同時 flush 事件到 collector，collector 的寫入速度（SQLite / PostgreSQL）是瓶頸。&lt;/p>
&lt;p>背壓鏈路：SDK flush → collector HTTP endpoint → 寫入 channel（有限容量）→ 寫入 goroutine → storage。Channel 滿時回 429，SDK 的離線 buffer 機制接手 — 事件暫存本地，等 collector 恢復後補發。&lt;/p>
&lt;p>這個設計讓 collector 在高峰時不崩潰（有限 buffer 控制記憶體）、SDK 端不丟事件（離線 buffer 暫存）。代價是事件的到達有延遲（Retry-After 時間 + 補發時間）。&lt;/p></description><content:encoded><![CDATA[<p>背壓是一種被動的流量控制機制 — 當下游（處理端）的速度跟不上上游（請求端）時，下游透過訊號讓上游知道「慢一點」。背壓不拒絕請求，而是讓請求的發送者自己決定要等待、重試還是放棄。</p>
<h2 id="背壓-vs-rate-limit">背壓 vs Rate Limit</h2>
<p>背壓和 rate limit 都是流量控制，但觸發邏輯不同：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>背壓</th>
          <th>Rate Limit</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>觸發條件</td>
          <td>下游實際變慢了（buffer 滿）</td>
          <td>請求速率超過預設上限</td>
      </tr>
      <tr>
          <td>性質</td>
          <td>被動（根據實際負載）</td>
          <td>主動（根據預設規則）</td>
      </tr>
      <tr>
          <td>訊號</td>
          <td>HTTP 429 + Retry-After / TCP 窗口縮小 / channel 阻塞</td>
          <td>HTTP 429 + 固定的 rate limit header</td>
      </tr>
      <tr>
          <td>發送者行為</td>
          <td>根據 Retry-After 動態調整</td>
          <td>等待限速窗口重設</td>
      </tr>
  </tbody>
</table>
<p>背壓在系統承載達到上限時才觸發，rate limit 在到達預設上限時就觸發（即使系統還有餘裕）。兩者互補：rate limit 防止單一來源打爆系統，背壓防止所有來源加起來打爆系統。</p>
<h2 id="實作模式">實作模式</h2>
<h3 id="有限-buffer--回壓訊號">有限 buffer + 回壓訊號</h3>
<p>最常見的背壓實作是在處理管線中加一個有限容量的 buffer。Buffer 滿了代表下游處理不完，這時對新請求回傳「忙碌」訊號。</p>
<p>在 Go 的 HTTP server 中，buffer 可以是一個有限容量的 channel：</p>





<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="kd">var</span> <span class="nx">ingestCh</span> <span class="p">=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="nx">Event</span><span class="p">,</span> <span class="mi">10000</span><span class="p">)</span> <span class="c1">// 有限 buffer</span>
</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"><span class="kd">func</span> <span class="nf">handleIngest</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">event</span> <span class="o">:=</span> <span class="nf">parseEvent</span><span class="p">(</span><span class="nx">r</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">case</span> <span class="nx">ingestCh</span> <span class="o">&lt;-</span> <span class="nx">event</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusAccepted</span><span class="p">)</span> <span class="c1">// 202</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nx">w</span><span class="p">.</span><span class="nf">Header</span><span class="p">().</span><span class="nf">Set</span><span class="p">(</span><span class="s">&#34;Retry-After&#34;</span><span class="p">,</span> <span class="s">&#34;5&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusTooManyRequests</span><span class="p">)</span> <span class="c1">// 429</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Buffer 容量的選擇取決於下游的處理速度和可接受的記憶體用量。每個 event 約 1KB 時，10000 容量的 buffer 佔 ~10MB — 對多數服務來說可以接受。</p>
<h3 id="http-429--retry-after">HTTP 429 + Retry-After</h3>
<p>HTTP 429 Too Many Requests 是標準的回壓訊號。<code>Retry-After</code> header 告訴 client 多少秒後重試。</p>
<p><code>Retry-After</code> 的值可以是固定的（如 5 秒），也可以根據 buffer 的填充程度動態計算 — buffer 越滿、Retry-After 越長。</p>
<h3 id="tcp-層的背壓">TCP 層的背壓</h3>
<p>TCP 協議本身有背壓機制 — 接收端的 receive window 縮小時，發送端自動減速。但 HTTP 層的背壓比 TCP 層更精確，因為 HTTP 可以回傳語意化的狀態碼和 header，client 可以根據語意做出更智慧的回應（如優先重試 error 事件、放棄 event 事件）。</p>
<h2 id="監控系統的應用">監控系統的應用</h2>
<p>監控系統的 <a href="/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">collector</a> 是背壓的典型場景：多個 SDK 同時 flush 事件到 collector，collector 的寫入速度（SQLite / PostgreSQL）是瓶頸。</p>
<p>背壓鏈路：SDK flush → collector HTTP endpoint → 寫入 channel（有限容量）→ 寫入 goroutine → storage。Channel 滿時回 429，SDK 的離線 buffer 機制接手 — 事件暫存本地，等 collector 恢復後補發。</p>
<p>這個設計讓 collector 在高峰時不崩潰（有限 buffer 控制記憶體）、SDK 端不丟事件（離線 buffer 暫存）。代價是事件的到達有延遲（Retry-After 時間 + 補發時間）。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>主動的流量限制 → <a href="/blog/devops/03-traffic-management/rate-limiting/" data-link-title="Rate Limiting" data-link-desc="主動限制每個來源的請求速率 — per-client vs global、token bucket vs sliding window、優先級豁免">Rate Limiting</a></li>
<li>依賴服務失敗時的防護 → <a href="/blog/devops/03-traffic-management/circuit-breaker/" data-link-title="熔斷器" data-link-desc="依賴服務失敗時怎麼快速失敗而非拖慢自己 — 三狀態模型（closed → open → half-open）和熔斷判斷條件">熔斷器</a></li>
<li>突發流量時的組合策略 → <a href="/blog/devops/07-burst-traffic/" data-link-title="模組七：突發流量應對" data-link-desc="行銷活動或新聞曝光帶來 10x-100x 流量時怎麼撐 — 突發分類、降級策略、queue 緩衝、規模分級應對">模組七 突發流量</a></li>
</ul>
]]></content:encoded></item><item><title>模組三：流量管控</title><link>https://tarrragon.github.io/blog/devops/03-traffic-management/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/devops/03-traffic-management/</guid><description>&lt;p>回答「收到的流量超過處理能力時怎麼辦」。四種防護機制各自處理不同層面的過載問題。&lt;/p>
&lt;h2 id="待寫章節">待寫章節&lt;/h2>
&lt;ul>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 背壓機制（下游慢時上游怎麼減速）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Rate Limiting（主動限制每個來源的請求速率）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 熔斷器（依賴服務失敗時怎麼快速失敗而非拖慢自己）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Bulkhead 隔離（不同工作負載的資源池隔離）&lt;/li>
&lt;/ul>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">monitoring 模組四 Collector&lt;/a>：Collector 的 ingestion 防護是本模組的應用場景&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/devops/07-burst-traffic/" data-link-title="模組七：突發流量應對" data-link-desc="行銷活動或新聞曝光帶來 10x-100x 流量時怎麼撐 — 突發分類、降級策略、queue 緩衝、規模分級應對">devops 模組七 突發流量&lt;/a>：突發流量時這四種機制怎麼組合使用&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">backend 可靠性&lt;/a>：熔斷和 bulkhead 也是 backend 的可靠性設計元件&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>回答「收到的流量超過處理能力時怎麼辦」。四種防護機制各自處理不同層面的過載問題。</p>
<h2 id="待寫章節">待寫章節</h2>
<ul>
<li><input checked="" disabled="" type="checkbox"> 背壓機制（下游慢時上游怎麼減速）</li>
<li><input checked="" disabled="" type="checkbox"> Rate Limiting（主動限制每個來源的請求速率）</li>
<li><input checked="" disabled="" type="checkbox"> 熔斷器（依賴服務失敗時怎麼快速失敗而非拖慢自己）</li>
<li><input checked="" disabled="" type="checkbox"> Bulkhead 隔離（不同工作負載的資源池隔離）</li>
</ul>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">monitoring 模組四 Collector</a>：Collector 的 ingestion 防護是本模組的應用場景</li>
<li>→ <a href="/blog/devops/07-burst-traffic/" data-link-title="模組七：突發流量應對" data-link-desc="行銷活動或新聞曝光帶來 10x-100x 流量時怎麼撐 — 突發分類、降級策略、queue 緩衝、規模分級應對">devops 模組七 突發流量</a>：突發流量時這四種機制怎麼組合使用</li>
<li>→ <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">backend 可靠性</a>：熔斷和 bulkhead 也是 backend 的可靠性設計元件</li>
</ul>
]]></content:encoded></item><item><title>Backpressure</title><link>https://tarrragon.github.io/blog/monitoring/knowledge-cards/backpressure/</link><pubDate>Wed, 24 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/knowledge-cards/backpressure/</guid><description>&lt;p>背壓（backpressure）的通用概念見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">Backend 知識卡：Backpressure&lt;/a> — 下游處理能力不足時向上游回傳「慢下來」訊號。本卡聚焦監控系統中的具體實作：collector 是下游、SDK 是上游，collector 的寫入 channel 滿時回 HTTP 429（Too Many Requests），SDK 收到 429 後自動降低&lt;a href="https://tarrragon.github.io/blog/monitoring/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="在事件產生階段按比例丟棄部分事件降低管線負載 — 分靜態取樣（config 固定比例）和動態取樣（背壓觸發自動降低）">取樣&lt;/a>率。可先對照 &lt;a href="https://tarrragon.github.io/blog/monitoring/knowledge-cards/rate-limiting/" data-link-title="Rate Limiting" data-link-desc="限制每個 client 在單位時間內可送出的事件數量 — 防止單一 SDK bug 或偽造流量消耗整個 collector 的處理能力">rate limiting&lt;/a>（per-client 的配額限制）。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>背壓位在 SDK 和 collector 之間的 HTTP 通訊層。觸發順序：collector 的寫入 channel 容量耗盡 → HTTP handler 無法送入事件 → 回 429 → SDK 收到 429 → SDK 降低取樣率（從 1.0 → 0.5 → 0.1）。背壓是全域的容量訊號 — 所有 SDK 同時收到，所有 SDK 同時降速。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>需要關注背壓的訊號是 collector 端的 &lt;code>collector.events.backpressure&lt;/code> 計數器持續上升、或 SDK 端的 &lt;code>sdk.sampling.rate&lt;/code> 低於 1.0。典型場景：行銷活動導致同時在線使用者暴增 → 所有 SDK 同時 flush → collector channel 瞬間填滿 → 全域 429 → 所有 SDK 動態降採樣。&lt;/p>
&lt;h2 id="和-devops-背壓的關係">和 DevOps 背壓的關係&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/devops/03-traffic-management/" data-link-title="模組三：流量管控" data-link-desc="收到的流量超過處理能力時怎麼辦 — 背壓、rate limit、熔斷、bulkhead 四種防護機制">DevOps 流量管控&lt;/a>討論通用的背壓概念（TCP flow control、message queue consumer lag、circuit breaker）。本系列聚焦 SDK ↔ collector 之間的具體實作 — HTTP 429 是訊號、動態取樣是回應、Go channel 容量是觸發條件。通用概念在 DevOps 模組，監控場景的具體機制在本系列。&lt;/p>
&lt;h2 id="完整章節">完整章節&lt;/h2>
&lt;p>背壓在四層防線中的位置（第二層 collector 單機防護）→ &lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/ingestion-scaling/" data-link-title="Ingestion Scaling" data-link-desc="四層防線應對 ingestion 端的流量擴展 — SDK 取樣、Collector 背壓、水平擴展、Queue 解耦">Ingestion Scaling&lt;/a>。背壓造成的資料損失和控制策略 → &lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/data-integrity/" data-link-title="端到端資料完整性" data-link-desc="從 SDK 到 storage 的資料損失地圖 — 每個環節的損失類型、控制策略、完整性指標、被自己 SDK DDoS 的防護">端到端資料完整性&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>背壓（backpressure）的通用概念見 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">Backend 知識卡：Backpressure</a> — 下游處理能力不足時向上游回傳「慢下來」訊號。本卡聚焦監控系統中的具體實作：collector 是下游、SDK 是上游，collector 的寫入 channel 滿時回 HTTP 429（Too Many Requests），SDK 收到 429 後自動降低<a href="/blog/monitoring/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="在事件產生階段按比例丟棄部分事件降低管線負載 — 分靜態取樣（config 固定比例）和動態取樣（背壓觸發自動降低）">取樣</a>率。可先對照 <a href="/blog/monitoring/knowledge-cards/rate-limiting/" data-link-title="Rate Limiting" data-link-desc="限制每個 client 在單位時間內可送出的事件數量 — 防止單一 SDK bug 或偽造流量消耗整個 collector 的處理能力">rate limiting</a>（per-client 的配額限制）。</p>
<h2 id="概念位置">概念位置</h2>
<p>背壓位在 SDK 和 collector 之間的 HTTP 通訊層。觸發順序：collector 的寫入 channel 容量耗盡 → HTTP handler 無法送入事件 → 回 429 → SDK 收到 429 → SDK 降低取樣率（從 1.0 → 0.5 → 0.1）。背壓是全域的容量訊號 — 所有 SDK 同時收到，所有 SDK 同時降速。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>需要關注背壓的訊號是 collector 端的 <code>collector.events.backpressure</code> 計數器持續上升、或 SDK 端的 <code>sdk.sampling.rate</code> 低於 1.0。典型場景：行銷活動導致同時在線使用者暴增 → 所有 SDK 同時 flush → collector channel 瞬間填滿 → 全域 429 → 所有 SDK 動態降採樣。</p>
<h2 id="和-devops-背壓的關係">和 DevOps 背壓的關係</h2>
<p><a href="/blog/devops/03-traffic-management/" data-link-title="模組三：流量管控" data-link-desc="收到的流量超過處理能力時怎麼辦 — 背壓、rate limit、熔斷、bulkhead 四種防護機制">DevOps 流量管控</a>討論通用的背壓概念（TCP flow control、message queue consumer lag、circuit breaker）。本系列聚焦 SDK ↔ collector 之間的具體實作 — HTTP 429 是訊號、動態取樣是回應、Go channel 容量是觸發條件。通用概念在 DevOps 模組，監控場景的具體機制在本系列。</p>
<h2 id="完整章節">完整章節</h2>
<p>背壓在四層防線中的位置（第二層 collector 單機防護）→ <a href="/blog/monitoring/04-collector/ingestion-scaling/" data-link-title="Ingestion Scaling" data-link-desc="四層防線應對 ingestion 端的流量擴展 — SDK 取樣、Collector 背壓、水平擴展、Queue 解耦">Ingestion Scaling</a>。背壓造成的資料損失和控制策略 → <a href="/blog/monitoring/04-collector/data-integrity/" data-link-title="端到端資料完整性" data-link-desc="從 SDK 到 storage 的資料損失地圖 — 每個環節的損失類型、控制策略、完整性指標、被自己 SDK DDoS 的防護">端到端資料完整性</a>。</p>
]]></content:encoded></item><item><title>Ingestion Scaling</title><link>https://tarrragon.github.io/blog/monitoring/04-collector/ingestion-scaling/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/04-collector/ingestion-scaling/</guid><description>&lt;p>Ingestion scaling 處理的是「大量事件同時湧入 collector 時怎麼辦」。這和 storage scaling（&lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">SQLite → PostgreSQL 的可插拔 backend&lt;/a>）是兩個獨立的擴展軸 — storage scaling 解決「查得動嗎」，ingestion scaling 解決「收得下嗎」。一個 collector 可能 storage 用 PostgreSQL（查詢能力足夠）但 ingestion 撐不住（HTTP 請求太多），反之亦然。&lt;/p>
&lt;h2 id="四層防線">四層防線&lt;/h2>
&lt;p>每一層在不同規模觸發，由近到遠依序啟用。前一層能擋住的流量不需要啟用後一層。本章的四層按防線位置劃分（SDK / Collector / 基礎設施兩層）。DevOps 的&lt;a href="https://tarrragon.github.io/blog/devops/07-burst-traffic/scale-tier-response/" data-link-title="規模分級應對表" data-link-desc="自用級 → 中型 → 大型 → 商業網站級的四級應對方案 — 每級的觸發條件、架構組成和成本">規模分級應對表&lt;/a>按 events/sec 量級劃分（Tier 1-4），兩者視角不同但覆蓋相同的擴展路徑。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層&lt;/th>
 &lt;th>機制&lt;/th>
 &lt;th>在哪裡做&lt;/th>
 &lt;th>觸發條件&lt;/th>
 &lt;th>適用規模&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>一&lt;/td>
 &lt;td>SDK 端取樣 + 聚合前移&lt;/td>
 &lt;td>SDK&lt;/td>
 &lt;td>高頻事件超過合理粒度&lt;/td>
 &lt;td>所有規模&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>二&lt;/td>
 &lt;td>Collector 單機背壓 + rate limit&lt;/td>
 &lt;td>Collector&lt;/td>
 &lt;td>寫入 channel 接近滿載&lt;/td>
 &lt;td>自用 ~ 小型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>三&lt;/td>
 &lt;td>水平擴展（多 collector + LB）&lt;/td>
 &lt;td>基礎設施&lt;/td>
 &lt;td>單機 CPU / 連線數飽和&lt;/td>
 &lt;td>中型 ~ 大型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>四&lt;/td>
 &lt;td>Queue 解耦（Kafka / NATS）&lt;/td>
 &lt;td>基礎設施&lt;/td>
 &lt;td>突發流量超過 collector 群的即時處理能力&lt;/td>
 &lt;td>商業網站級&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="第一層sdk-端的流量控制">第一層：SDK 端的流量控制&lt;/h2>
&lt;p>流量控制的最有效位置是事件產生的源頭。SDK 端減少的事件量，後面每一層都不需要處理。&lt;/p>
&lt;h3 id="動態取樣">動態取樣&lt;/h3>
&lt;p>SDK 在收到 collector 的 HTTP 429（Too Many Requests）回應時，自動降低取樣率。恢復正常後逐步回升。&lt;/p>





&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">正常 → sampling 1.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">收到 429 → sampling 降到 0.5
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">持續 429 → sampling 降到 0.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">連續 10 次成功 → sampling 回升到 0.5
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">連續 30 次成功 → sampling 回到 1.0&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>動態取樣的控制邏輯在 SDK 端實作，不需要 collector 端額外支援 — 429 回應碼就是觸發訊號。和&lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/sensor-lifecycle-management/" data-link-title="感測器生命週期管理" data-link-desc="產品生命週期的五個階段各啟用什麼感測器 — feature flag 整合、取樣率動態調整、感測器開關的可觀察性">感測器生命週期管理&lt;/a>的靜態取樣率互補 — 靜態取樣在 config 中設定、動態取樣在執行期自動調整。&lt;/p>
&lt;h3 id="聚合前移">聚合前移&lt;/h3>
&lt;p>SDK 端累積一段時間的同名事件，送出摘要而非逐筆。適合 metric 類的高頻取樣。&lt;/p>
&lt;p>例：原本每 100ms 送一筆 &lt;code>render.frame_drop&lt;/code>，改成每 5 秒送一筆 &lt;code>render.frame_drop_summary&lt;/code>（帶 count + min + max + avg）。事件數從 50 筆/5s 降到 1 筆/5s。&lt;/p>
&lt;p>聚合前移犧牲事件粒度換取吞吐量。只適合「趨勢比每筆細節重要」的 metric 類事件。Error 和 lifecycle 事件不做聚合 — 每筆的 stack trace 和狀態轉換都有 debug 價值。&lt;/p></description><content:encoded><![CDATA[<p>Ingestion scaling 處理的是「大量事件同時湧入 collector 時怎麼辦」。這和 storage scaling（<a href="/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">SQLite → PostgreSQL 的可插拔 backend</a>）是兩個獨立的擴展軸 — storage scaling 解決「查得動嗎」，ingestion scaling 解決「收得下嗎」。一個 collector 可能 storage 用 PostgreSQL（查詢能力足夠）但 ingestion 撐不住（HTTP 請求太多），反之亦然。</p>
<h2 id="四層防線">四層防線</h2>
<p>每一層在不同規模觸發，由近到遠依序啟用。前一層能擋住的流量不需要啟用後一層。本章的四層按防線位置劃分（SDK / Collector / 基礎設施兩層）。DevOps 的<a href="/blog/devops/07-burst-traffic/scale-tier-response/" data-link-title="規模分級應對表" data-link-desc="自用級 → 中型 → 大型 → 商業網站級的四級應對方案 — 每級的觸發條件、架構組成和成本">規模分級應對表</a>按 events/sec 量級劃分（Tier 1-4），兩者視角不同但覆蓋相同的擴展路徑。</p>
<table>
  <thead>
      <tr>
          <th>層</th>
          <th>機制</th>
          <th>在哪裡做</th>
          <th>觸發條件</th>
          <th>適用規模</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一</td>
          <td>SDK 端取樣 + 聚合前移</td>
          <td>SDK</td>
          <td>高頻事件超過合理粒度</td>
          <td>所有規模</td>
      </tr>
      <tr>
          <td>二</td>
          <td>Collector 單機背壓 + rate limit</td>
          <td>Collector</td>
          <td>寫入 channel 接近滿載</td>
          <td>自用 ~ 小型</td>
      </tr>
      <tr>
          <td>三</td>
          <td>水平擴展（多 collector + LB）</td>
          <td>基礎設施</td>
          <td>單機 CPU / 連線數飽和</td>
          <td>中型 ~ 大型</td>
      </tr>
      <tr>
          <td>四</td>
          <td>Queue 解耦（Kafka / NATS）</td>
          <td>基礎設施</td>
          <td>突發流量超過 collector 群的即時處理能力</td>
          <td>商業網站級</td>
      </tr>
  </tbody>
</table>
<h2 id="第一層sdk-端的流量控制">第一層：SDK 端的流量控制</h2>
<p>流量控制的最有效位置是事件產生的源頭。SDK 端減少的事件量，後面每一層都不需要處理。</p>
<h3 id="動態取樣">動態取樣</h3>
<p>SDK 在收到 collector 的 HTTP 429（Too Many Requests）回應時，自動降低取樣率。恢復正常後逐步回升。</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">正常 → sampling 1.0
</span></span><span class="line"><span class="ln">2</span><span class="cl">收到 429 → sampling 降到 0.5
</span></span><span class="line"><span class="ln">3</span><span class="cl">持續 429 → sampling 降到 0.1
</span></span><span class="line"><span class="ln">4</span><span class="cl">連續 10 次成功 → sampling 回升到 0.5
</span></span><span class="line"><span class="ln">5</span><span class="cl">連續 30 次成功 → sampling 回到 1.0</span></span></code></pre></div><p>動態取樣的控制邏輯在 SDK 端實作，不需要 collector 端額外支援 — 429 回應碼就是觸發訊號。和<a href="/blog/monitoring/03-sdk-design/sensor-lifecycle-management/" data-link-title="感測器生命週期管理" data-link-desc="產品生命週期的五個階段各啟用什麼感測器 — feature flag 整合、取樣率動態調整、感測器開關的可觀察性">感測器生命週期管理</a>的靜態取樣率互補 — 靜態取樣在 config 中設定、動態取樣在執行期自動調整。</p>
<h3 id="聚合前移">聚合前移</h3>
<p>SDK 端累積一段時間的同名事件，送出摘要而非逐筆。適合 metric 類的高頻取樣。</p>
<p>例：原本每 100ms 送一筆 <code>render.frame_drop</code>，改成每 5 秒送一筆 <code>render.frame_drop_summary</code>（帶 count + min + max + avg）。事件數從 50 筆/5s 降到 1 筆/5s。</p>
<p>聚合前移犧牲事件粒度換取吞吐量。只適合「趨勢比每筆細節重要」的 metric 類事件。Error 和 lifecycle 事件不做聚合 — 每筆的 stack trace 和狀態轉換都有 debug 價值。</p>
<h3 id="優先級丟棄">優先級丟棄</h3>
<p>SDK 的離線 buffer 滿時，按優先級丟棄。Error 的 debug 價值最高，最後丟。</p>
<table>
  <thead>
      <tr>
          <th>優先級</th>
          <th>事件類型</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>高</td>
          <td>error</td>
          <td>每筆都可能是需要修的 bug</td>
      </tr>
      <tr>
          <td>高</td>
          <td>lifecycle</td>
          <td>session 邊界和狀態轉換、影響 debug 和 cohort</td>
      </tr>
      <tr>
          <td>中</td>
          <td>metric</td>
          <td>丟幾筆不影響趨勢（聚合摘要仍然有效）</td>
      </tr>
      <tr>
          <td>低</td>
          <td>event</td>
          <td>行為事件在取樣後丟幾筆對 funnel 影響有限</td>
      </tr>
  </tbody>
</table>
<h2 id="第二層collector-單機的防護">第二層：Collector 單機的防護</h2>
<p>Collector 在自身能力範圍內保護自己不被壓垮。和 <a href="/blog/monitoring/04-collector/architecture/" data-link-title="Collector 架構" data-link-desc="HTTP endpoint → JSON Schema 驗證 → 儲存 → 查詢 → rule engine 的五段式處理鏈路">architecture.md 的並發寫入策略</a>直接相關 — 寫入 channel 是背壓的實作基礎。背壓和流量管控的通用概念見 <a href="/blog/devops/03-traffic-management/" data-link-title="模組三：流量管控" data-link-desc="收到的流量超過處理能力時怎麼辦 — 背壓、rate limit、熔斷、bulkhead 四種防護機制">DevOps 流量管控</a>。</p>
<h3 id="寫入-channel-容量--背壓">寫入 channel 容量 + 背壓</h3>
<p>Single-writer goroutine pattern 的 Go channel 有固定容量（如 10,000）。Channel 滿時 HTTP handler 無法送入事件，此時回 429：</p>





<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="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">case</span> <span class="nx">writeCh</span> <span class="o">&lt;-</span> <span class="nx">event</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusAccepted</span><span class="p">)</span> <span class="c1">// 202</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nx">w</span><span class="p">.</span><span class="nf">Header</span><span class="p">().</span><span class="nf">Set</span><span class="p">(</span><span class="s">&#34;Retry-After&#34;</span><span class="p">,</span> <span class="s">&#34;5&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusTooManyRequests</span><span class="p">)</span> <span class="c1">// 429</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Channel 容量的設定依據：容量 × 每筆事件的記憶體大小 = 背壓 buffer 的記憶體上限。10,000 筆 × 每筆 ~1KB = ~10MB，對多數機器微不足道。</p>
<h3 id="per-sdk-rate-limiting">Per-SDK rate limiting</h3>
<p>按 source.app（或 API key，啟用認證後）限制每個 SDK 實例的請求速率。防止單一 SDK 的 bug（無限迴圈送事件）打爆 collector。</p>





<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">// 每個 source.app 一個 rate limiter</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">limiter</span> <span class="o">:=</span> <span class="nx">rateLimiters</span><span class="p">.</span><span class="nf">GetOrCreate</span><span class="p">(</span><span class="nx">sourceApp</span><span class="p">,</span> <span class="nx">rate</span><span class="p">.</span><span class="nf">Limit</span><span class="p">(</span><span class="mi">100</span><span class="p">))</span> <span class="c1">// 100 events/sec</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">if</span> <span class="p">!</span><span class="nx">limiter</span><span class="p">.</span><span class="nf">Allow</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusTooManyRequests</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">return</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="error-快通道">Error 快通道</h3>
<p>Error 事件不經 rate limit — 它們的 debug 價值最高，且在正常情況下數量遠少於其他類型。Error storm（app 出 bug 導致大量 error）時，error 的量可能暴增，但這正是最需要記錄的時刻。</p>
<p>Error 快通道用獨立的 channel 或跳過 rate limiter 的 check。如果 error 量也超出承載，用第一層的 SDK 端優先級丟棄處理。</p>
<h2 id="第三層水平擴展">第三層：水平擴展</h2>
<p>單機的 CPU、記憶體或網路頻寬飽和時，水平擴展 — 多個 collector 實例分攤流量。水平擴展的通用模式見 <a href="/blog/devops/02-horizontal-scaling/" data-link-title="模組二：水平擴展" data-link-desc="一個實例不夠時怎麼加第二個 — stateless 設計、shared storage、session 處理的工程約束">DevOps 水平擴展</a>。</p>
<h3 id="前提已切換到-postgresql">前提：已切換到 PostgreSQL</h3>
<p>SQLite backend 不支援水平擴展。每個 collector 實例有各自的 SQLite 檔案，無法合併查詢。水平擴展的前提是所有 collector 寫入同一個 PostgreSQL。</p>
<h3 id="架構">架構</h3>





<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 ──→ Load Balancer (nginx / HAProxy)
</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">        ┌────┴────┐
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        ▼         ▼
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">   Collector A  Collector B
</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">        └────┬────┘
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">             ▼
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        PostgreSQL
</span></span><span class="line"><span class="ln">10</span><span class="cl">             │
</span></span><span class="line"><span class="ln">11</span><span class="cl">             ▼
</span></span><span class="line"><span class="ln">12</span><span class="cl">         Dashboard</span></span></code></pre></div><p>Collector 實例是 stateless 的 — 不在記憶體保存查詢狀態，所有持久化資料在 PostgreSQL。任何 collector 接收的事件都能被任何 dashboard 查到。</p>
<p>Load balancer 用 round-robin 或 least-connections 分配。不需要 sticky session — collector 不保存 session 狀態。</p>
<h3 id="多機的-downsample-和-purge">多機的 Downsample 和 Purge</h3>
<p>Downsample 和 Purge job 只能由一個 collector 實例執行（避免重複處理）。用 PostgreSQL 的 advisory lock 或外部的 distributed lock 確保單一執行者。</p>
<h2 id="第四層queue-解耦">第四層：Queue 解耦</h2>
<p>突發流量超過 collector 群的即時處理能力時，在 collector 和 storage 之間插入 message queue 做緩衝。Queue 緩衝的通用概念見 <a href="/blog/devops/07-burst-traffic/" data-link-title="模組七：突發流量應對" data-link-desc="行銷活動或新聞曝光帶來 10x-100x 流量時怎麼撐 — 突發分類、降級策略、queue 緩衝、規模分級應對">DevOps 突發流量應對</a>，message queue 的選型見 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Backend 模組三 非同步與訊息佇列</a>。</p>
<h3 id="架構-1">架構</h3>





<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">             ▼
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        Queue (Kafka / NATS / Redis Streams)
</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></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">    Worker A   Worker B
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        │         │
</span></span><span class="line"><span class="ln">10</span><span class="cl">        └────┬────┘
</span></span><span class="line"><span class="ln">11</span><span class="cl">             ▼
</span></span><span class="line"><span class="ln">12</span><span class="cl">        PostgreSQL</span></span></code></pre></div><p>Collector 的職責簡化為「接收 → 驗證 → 寫入 queue → 回 202」。寫入 queue 比寫入 DB 快得多（append-only、不需要索引更新），collector 的吞吐上限大幅提升。</p>
<p>Worker 從 queue 消費、寫入 PostgreSQL。Worker 按自己的速度處理 — 高峰時 queue 積壓，高峰過後 worker 消化積壓。Queue 的持久化保證事件不遺失。</p>
<h3 id="queue-的選擇">Queue 的選擇</h3>
<table>
  <thead>
      <tr>
          <th>Queue</th>
          <th>適合場景</th>
          <th>代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Kafka</td>
          <td>高吞吐（百萬 events/sec）、需要 replay</td>
          <td>運維重（ZooKeeper / KRaft）</td>
      </tr>
      <tr>
          <td>NATS JetStream</td>
          <td>輕量、Go 原生、足夠的持久化</td>
          <td>生態較小</td>
      </tr>
      <tr>
          <td>Redis Streams</td>
          <td>簡單、如果已有 Redis</td>
          <td>不是專門的 queue、持久化設定需注意</td>
      </tr>
  </tbody>
</table>
<p>自架監控工具的 queue 層級推薦 NATS JetStream — Go 原生 client、單 binary 部署、JetStream 提供持久化和 replay。</p>
<h3 id="觸發條件">觸發條件</h3>
<p>Queue 解耦的引入時機是「collector 群已水平擴展但仍無法處理突發流量」。如果日常流量 collector 群能處理，只有行銷活動 / 新聞曝光的短暫高峰需要 queue 緩衝，queue 的維護成本可能高於收益 — 考慮用第一層的動態取樣在源頭降量。</p>
<h2 id="功能分層整合">功能分層整合</h2>
<p>擴展 <a href="/blog/monitoring/04-collector/feature-tier-boundary/" data-link-title="功能分層與 Backend 選擇" data-link-desc="SQLite 層和 PostgreSQL 層各自承載哪些功能 — 分界線是查詢模式而非資料量、觸發升級的是功能需求而非規模成長">功能分層與 Backend 選擇</a> 的分層表，加入 ingestion 維度：</p>
<table>
  <thead>
      <tr>
          <th>功能層級</th>
          <th>Storage</th>
          <th>Ingestion</th>
          <th>適用規模</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SQLite 層</td>
          <td>SQLite embedded</td>
          <td>單 collector + 背壓</td>
          <td>自用 ~ 小型團隊</td>
      </tr>
      <tr>
          <td>PostgreSQL 層</td>
          <td>PostgreSQL</td>
          <td>多 collector + LB</td>
          <td>中型 ~ 大型</td>
      </tr>
      <tr>
          <td>Queue 層</td>
          <td>PostgreSQL</td>
          <td>Collector + Queue + Worker</td>
          <td>商業網站級</td>
      </tr>
  </tbody>
</table>
<p>每一層是前一層的超集 — Queue 層包含 PostgreSQL 層的所有查詢能力，加上 ingestion 的 queue 緩衝。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Collector 的並發寫入策略 → <a href="/blog/monitoring/04-collector/architecture/" data-link-title="Collector 架構" data-link-desc="HTTP endpoint → JSON Schema 驗證 → 儲存 → 查詢 → rule engine 的五段式處理鏈路">Collector 架構</a></li>
<li>Storage 端的擴展設計 → <a href="/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進</a></li>
<li>功能分層的定義 → <a href="/blog/monitoring/04-collector/feature-tier-boundary/" data-link-title="功能分層與 Backend 選擇" data-link-desc="SQLite 層和 PostgreSQL 層各自承載哪些功能 — 分界線是查詢模式而非資料量、觸發升級的是功能需求而非規模成長">功能分層與 Backend 選擇</a></li>
<li>背壓和流量管控的通用概念 → <a href="/blog/devops/03-traffic-management/" data-link-title="模組三：流量管控" data-link-desc="收到的流量超過處理能力時怎麼辦 — 背壓、rate limit、熔斷、bulkhead 四種防護機制">DevOps 流量管控</a></li>
<li>水平擴展的通用模式 → <a href="/blog/devops/02-horizontal-scaling/" data-link-title="模組二：水平擴展" data-link-desc="一個實例不夠時怎麼加第二個 — stateless 設計、shared storage、session 處理的工程約束">DevOps 水平擴展</a></li>
<li>突發流量應對 → <a href="/blog/devops/07-burst-traffic/" data-link-title="模組七：突發流量應對" data-link-desc="行銷活動或新聞曝光帶來 10x-100x 流量時怎麼撐 — 突發分類、降級策略、queue 緩衝、規模分級應對">DevOps 突發流量</a></li>
<li>Message queue 選型 → <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Backend 模組三 非同步與訊息佇列</a></li>
<li>端到端資料完整性（資料損失地圖、完整性指標）→ <a href="/blog/monitoring/04-collector/data-integrity/" data-link-title="端到端資料完整性" data-link-desc="從 SDK 到 storage 的資料損失地圖 — 每個環節的損失類型、控制策略、完整性指標、被自己 SDK DDoS 的防護">端到端資料完整性</a></li>
</ul>
]]></content:encoded></item><item><title>端到端資料完整性</title><link>https://tarrragon.github.io/blog/monitoring/04-collector/data-integrity/</link><pubDate>Wed, 24 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/04-collector/data-integrity/</guid><description>&lt;p>監控資料從事件產生到寫入 storage，經過 SDK buffer、HTTP transport、collector pipeline、storage backend 四個環節。每個環節都有丟失事件的可能 — 記憶體 buffer 溢出、網路超時、背壓丟棄、磁碟寫入失敗。端到端資料完整性的目標是讓每個損失點都是有意識的設計取捨，而非靜默丟失。&lt;/p>
&lt;p>監控資料和交易資料的根本差異在這裡：交易資料的損失會直接造成商業損害（少了一筆訂單），監控資料的損失影響的是可觀測性的覆蓋率（少了幾筆 event 不影響趨勢判斷，但漏了 error 可能讓 bug 晚幾天被發現）。這個差異決定了完整性設計的方向 — 追求的是「損失可控且可觀測」，而非「零損失」。合規稽核 log、billing event 和安全事件不適用這個假設 — 它們的損失有法規或商業後果，需要 at-least-once delivery 和獨立的持久化保證，通常用 transaction log 而非監控管線處理。&lt;/p>
&lt;h2 id="資料損失地圖">資料損失地圖&lt;/h2>
&lt;p>一筆事件從產生到持久化，依序經過四個環節。每個環節的損失類型、發生條件和影響範圍各不同。&lt;/p>





&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 buffer] → HTTP POST → [Collector pipeline] → [Storage]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ① ② ③ ④ ⑤&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="環節一事件產生階段">環節一：事件產生階段&lt;/h3>
&lt;p>事件在 SDK 的 &lt;code>monitor.event()&lt;/code> / &lt;code>monitor.error()&lt;/code> 被呼叫時產生，進入記憶體 buffer。這個階段的損失來自取樣和 SDK 初始化時序。&lt;/p>
&lt;p>&lt;strong>靜態取樣&lt;/strong>：SDK config 中設定的取樣率（例如 metric 類 0.1 = 每 10 筆只收 1 筆）是設計內的損失。取樣後的事件量直接影響後續所有環節的負載。取樣率的設定依據見&lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/sensor-lifecycle-management/" data-link-title="感測器生命週期管理" data-link-desc="產品生命週期的五個階段各啟用什麼感測器 — feature flag 整合、取樣率動態調整、感測器開關的可觀察性">感測器生命週期管理&lt;/a>。&lt;/p>
&lt;p>&lt;strong>SDK 未初始化&lt;/strong>：app 啟動後到 &lt;code>monitor.init()&lt;/code> 完成之間的事件會被丟棄。如果 init 排在其他初始化邏輯之後，啟動階段的 crash 可能漏捕。商業 SDK（Sentry、Crashlytics）用 native crash handler 在 SDK 層之外攔截這類 crash，自架方案通常接受這個損失。&lt;/p>
&lt;h3 id="環節二sdk-buffer-階段">環節二：SDK buffer 階段&lt;/h3>
&lt;p>事件進入記憶體 buffer 後，等待 flush 觸發。Buffer 溢出和 app 強制終止是這段路徑上的兩個風險。&lt;/p>
&lt;p>&lt;strong>FIFO 丟棄&lt;/strong>：記憶體 buffer 有容量上限（典型值 200-500 筆）。離線時間過長或事件產生速率過高時，buffer 滿了會丟棄最舊的事件。丟棄策略見&lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/offline-buffer/" data-link-title="離線 buffer 與重試" data-link-desc="網路不可用時的事件保存策略 — FIFO 丟棄、本地 persistence、恢復後補發的取捨">離線 buffer 與重試&lt;/a>，優先級丟棄見 &lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/ingestion-scaling/" data-link-title="Ingestion Scaling" data-link-desc="四層防線應對 ingestion 端的流量擴展 — SDK 取樣、Collector 背壓、水平擴展、Queue 解耦">Ingestion Scaling 第一層&lt;/a>。&lt;/p>
&lt;p>&lt;strong>App 強制終止&lt;/strong>：iOS 的 &lt;code>kill&lt;/code>、Android 的 process death、Python 的 &lt;code>SIGKILL&lt;/code> — 記憶體 buffer 中未 flush 的事件全部遺失。&lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/batch-flush/" data-link-title="攢批送出策略" data-link-desc="flush interval / buffer size / flush on close 三個控制點決定事件何時離開 SDK — 平衡即時性和網路效率">攢批送出策略&lt;/a>的 close flush 嘗試在 app 正常退出時送出剩餘事件，但強制終止時連 close callback 都不會執行。&lt;/p>
&lt;p>&lt;strong>動態取樣&lt;/strong>：收到 collector 的 HTTP 429（Too Many Requests，表示 collector 過載）後，SDK 自動降低取樣率（從 1.0 降到 0.5 → 0.1）。這是對 collector 過載的回饋反應 — 損失的事件量隨背壓程度增加。和靜態取樣的差異是動態取樣在正常情況下不生效，只在過載時啟用。&lt;/p>
&lt;h3 id="環節三transport-階段">環節三：Transport 階段&lt;/h3>
&lt;p>SDK flush 時透過 HTTP POST 送出 batch。網路故障和重試耗盡構成 transport 層的主要損失。&lt;/p></description><content:encoded><![CDATA[<p>監控資料從事件產生到寫入 storage，經過 SDK buffer、HTTP transport、collector pipeline、storage backend 四個環節。每個環節都有丟失事件的可能 — 記憶體 buffer 溢出、網路超時、背壓丟棄、磁碟寫入失敗。端到端資料完整性的目標是讓每個損失點都是有意識的設計取捨，而非靜默丟失。</p>
<p>監控資料和交易資料的根本差異在這裡：交易資料的損失會直接造成商業損害（少了一筆訂單），監控資料的損失影響的是可觀測性的覆蓋率（少了幾筆 event 不影響趨勢判斷，但漏了 error 可能讓 bug 晚幾天被發現）。這個差異決定了完整性設計的方向 — 追求的是「損失可控且可觀測」，而非「零損失」。合規稽核 log、billing event 和安全事件不適用這個假設 — 它們的損失有法規或商業後果，需要 at-least-once delivery 和獨立的持久化保證，通常用 transaction log 而非監控管線處理。</p>
<h2 id="資料損失地圖">資料損失地圖</h2>
<p>一筆事件從產生到持久化，依序經過四個環節。每個環節的損失類型、發生條件和影響範圍各不同。</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">事件產生 → [SDK buffer] → HTTP POST → [Collector pipeline] → [Storage]
</span></span><span class="line"><span class="ln">2</span><span class="cl">     ①          ②            ③              ④                   ⑤</span></span></code></pre></div><h3 id="環節一事件產生階段">環節一：事件產生階段</h3>
<p>事件在 SDK 的 <code>monitor.event()</code> / <code>monitor.error()</code> 被呼叫時產生，進入記憶體 buffer。這個階段的損失來自取樣和 SDK 初始化時序。</p>
<p><strong>靜態取樣</strong>：SDK config 中設定的取樣率（例如 metric 類 0.1 = 每 10 筆只收 1 筆）是設計內的損失。取樣後的事件量直接影響後續所有環節的負載。取樣率的設定依據見<a href="/blog/monitoring/03-sdk-design/sensor-lifecycle-management/" data-link-title="感測器生命週期管理" data-link-desc="產品生命週期的五個階段各啟用什麼感測器 — feature flag 整合、取樣率動態調整、感測器開關的可觀察性">感測器生命週期管理</a>。</p>
<p><strong>SDK 未初始化</strong>：app 啟動後到 <code>monitor.init()</code> 完成之間的事件會被丟棄。如果 init 排在其他初始化邏輯之後，啟動階段的 crash 可能漏捕。商業 SDK（Sentry、Crashlytics）用 native crash handler 在 SDK 層之外攔截這類 crash，自架方案通常接受這個損失。</p>
<h3 id="環節二sdk-buffer-階段">環節二：SDK buffer 階段</h3>
<p>事件進入記憶體 buffer 後，等待 flush 觸發。Buffer 溢出和 app 強制終止是這段路徑上的兩個風險。</p>
<p><strong>FIFO 丟棄</strong>：記憶體 buffer 有容量上限（典型值 200-500 筆）。離線時間過長或事件產生速率過高時，buffer 滿了會丟棄最舊的事件。丟棄策略見<a href="/blog/monitoring/03-sdk-design/offline-buffer/" data-link-title="離線 buffer 與重試" data-link-desc="網路不可用時的事件保存策略 — FIFO 丟棄、本地 persistence、恢復後補發的取捨">離線 buffer 與重試</a>，優先級丟棄見 <a href="/blog/monitoring/04-collector/ingestion-scaling/" data-link-title="Ingestion Scaling" data-link-desc="四層防線應對 ingestion 端的流量擴展 — SDK 取樣、Collector 背壓、水平擴展、Queue 解耦">Ingestion Scaling 第一層</a>。</p>
<p><strong>App 強制終止</strong>：iOS 的 <code>kill</code>、Android 的 process death、Python 的 <code>SIGKILL</code> — 記憶體 buffer 中未 flush 的事件全部遺失。<a href="/blog/monitoring/03-sdk-design/batch-flush/" data-link-title="攢批送出策略" data-link-desc="flush interval / buffer size / flush on close 三個控制點決定事件何時離開 SDK — 平衡即時性和網路效率">攢批送出策略</a>的 close flush 嘗試在 app 正常退出時送出剩餘事件，但強制終止時連 close callback 都不會執行。</p>
<p><strong>動態取樣</strong>：收到 collector 的 HTTP 429（Too Many Requests，表示 collector 過載）後，SDK 自動降低取樣率（從 1.0 降到 0.5 → 0.1）。這是對 collector 過載的回饋反應 — 損失的事件量隨背壓程度增加。和靜態取樣的差異是動態取樣在正常情況下不生效，只在過載時啟用。</p>
<h3 id="環節三transport-階段">環節三：Transport 階段</h3>
<p>SDK flush 時透過 HTTP POST 送出 batch。網路故障和重試耗盡構成 transport 層的主要損失。</p>
<p><strong>HTTP 超時 / 連線失敗</strong>：collector 不可達時，batch 保留在 SDK buffer 等待下次 flush 重試。重試次數有上限（3 次），超過後丟棄 batch 並記錄 <code>sdk.flush.dropped</code> metric。重試策略見<a href="/blog/monitoring/03-sdk-design/batch-flush/" data-link-title="攢批送出策略" data-link-desc="flush interval / buffer size / flush on close 三個控制點決定事件何時離開 SDK — 平衡即時性和網路效率">攢批送出策略</a>。</p>
<p><strong>離線補發擁塞</strong>：離線恢復後，SDK 一次補發大量累積事件。如果補發速率過高（一批 500 筆 × 多個 SDK 同時恢復），collector 可能觸發背壓回 429，SDK 又進入動態降採樣 — 補發本身造成新的損失。<a href="/blog/monitoring/03-sdk-design/offline-buffer/" data-link-title="離線 buffer 與重試" data-link-desc="網路不可用時的事件保存策略 — FIFO 丟棄、本地 persistence、恢復後補發的取捨">離線 buffer 與重試</a>的分批補發（每批 50-100 筆、間隔 1-2 秒）用來避免這個問題。</p>
<h3 id="環節四collector-pipeline-階段">環節四：Collector pipeline 階段</h3>
<p>Collector 收到 HTTP request 後，事件進入處理鏈路。背壓、驗證拒絕和 pipeline 內部的 buffer 溢出都可能在這裡造成損失。</p>
<p><strong>Channel 背壓</strong>：Collector 內部用一個專屬的寫入 goroutine 搭配 Go channel 做序列化寫入（<a href="/blog/monitoring/04-collector/architecture/" data-link-title="Collector 架構" data-link-desc="HTTP endpoint → JSON Schema 驗證 → 儲存 → 查詢 → rule engine 的五段式處理鏈路">Collector 架構</a>的並發寫入策略段），channel 有固定容量。Channel 滿時 HTTP handler 回 429，事件被拒絕。SDK 收到 429 後保留事件在 buffer 等待重試，但如果 SDK buffer 也快滿，部分事件會被 FIFO 丟棄。這裡的損失是 SDK 層和 collector 層的連鎖反應 — collector 的背壓壓力最終由 SDK 的 buffer 承擔。</p>
<p><strong>Schema validation reject</strong>：事件格式不符合 JSON Schema 的事件被拒絕（400 或 207 中的 rejected 部分）。這是品質閘門而非容量限制 — 被拒絕的事件無論重試多少次都不會通過，SDK 應該清除這些事件並記錄 warning。問題在 SDK 端的事件建構邏輯（程式碼 bug），需要修 SDK 而非重試。</p>
<p><strong>429 後事件已回 202 但未寫入</strong>：collector 回了 202（已接受）但事件還在 channel buffer 中未寫入 storage 時，如果 collector crash 或被 SIGKILL，channel 中的事件遺失。這是「已承諾但未持久化」的窗口。<a href="/blog/monitoring/04-collector/container-deployment/" data-link-title="Container 部署設計" data-link-desc="Docker 部署 collector 的設計 — SQLite 在 overlay filesystem 的 I/O 考量、volume mount、graceful shutdown、資源限制">Container 部署設計</a>的 graceful shutdown 序列嘗試在 shutdown 時 flush pending writes，但非 graceful shutdown（OOMKill、硬體故障）無法保護。</p>
<h3 id="環節五storage-階段">環節五：Storage 階段</h3>
<p>事件從 channel 寫入 storage backend。寫入失敗和資料管理操作（downsample / purge）構成最後一段損失。</p>
<p><strong>SQLite <code>database is locked</code></strong>：busy timeout 到期後寫入失敗。Single-writer pattern 降低發生機率但不能完全消除 — downsample / purge job 執行期間持有 write lock，如果 job 跑太久（數秒以上），ingestion 的寫入可能逾時。</p>
<p><strong>磁碟空間不足</strong>：SQLite 寫入需要磁碟空間（WAL 檔案 + 主資料庫 + 臨時檔案）。磁碟滿時寫入失敗，事件遺失。保留策略的 purge job 負責控制磁碟使用量，但如果 purge 頻率低於寫入增長速率，磁碟可能在兩次 purge 之間被填滿。</p>
<p><strong>Downsample / purge 的設計內損失</strong>：保留策略到期的原始事件被刪除（purge），只保留聚合摘要（hourly_summary / daily_summary）。這是設計內的損失 — 原始事件的 stack trace、完整 JSON data 在 purge 後不可回復，只剩下計數。保留策略見<a href="/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進</a>的分層保留段。</p>
<h2 id="設計內損失-vs-異常損失">設計內損失 vs 異常損失</h2>
<p>上述損失點可以分成兩類，處理方式根本不同。</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>損失點</th>
          <th>特徵</th>
          <th>處理方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>設計內</td>
          <td>靜態取樣、動態取樣、FIFO 丟棄、purge</td>
          <td>有意識的取捨、可預測的量</td>
          <td>在 config 中設定、用指標監控</td>
      </tr>
      <tr>
          <td>異常</td>
          <td>crash 丟 buffer、disk full、WAL 損壞</td>
          <td>非預期的故障、不可預測</td>
          <td>用告警偵測、用恢復機制應對</td>
      </tr>
      <tr>
          <td>品質閘門</td>
          <td>schema reject</td>
          <td>SDK 端 bug 導致、重試無效</td>
          <td>修 SDK 程式碼、不在 collector</td>
      </tr>
  </tbody>
</table>
<p>設計內損失的目標是讓損失量可控 — 取樣率設 0.1 代表預期丟 90%，FIFO buffer 容量 200 代表離線超過 20 分鐘（每分鐘 10 筆）後開始丟棄。這些數字是 config 參數，可以根據業務需求調整。</p>
<p>異常損失的目標是儘早偵測 — collector crash 後 channel 中有多少筆未寫入？磁碟使用率到多少該告警？下方的完整性指標段專門處理偵測異常損失的方法。</p>
<p>品質閘門的處理在 SDK 端而非 collector 端 — schema validation reject 的事件無論重試多少次都不會通過，問題在事件建構邏輯。具體的 reject 行為和回應格式見<a href="#%e7%92%b0%e7%af%80%e5%9b%9bcollector-pipeline-%e9%9a%8e%e6%ae%b5">環節四的 Schema validation reject 段</a>。</p>
<h2 id="監控損失本身的方法">監控損失本身的方法</h2>
<p>監控系統的完整性需要「監控自己的監控」— 用獨立的指標追蹤每個環節的進出量，損失量 = 進量 - 出量。</p>
<h3 id="sdk-端指標">SDK 端指標</h3>
<p>SDK 內部維護計數器，每次 flush 成功後一起送出（作為 metric 類事件）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>含義</th>
          <th>計算方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>sdk.events.produced</code></td>
          <td>事件產生總數（取樣前）</td>
          <td>每次 <code>monitor.event()</code> 調用 +1</td>
      </tr>
      <tr>
          <td><code>sdk.events.sampled</code></td>
          <td>取樣後保留的事件數</td>
          <td>通過取樣邏輯的事件 +1</td>
      </tr>
      <tr>
          <td><code>sdk.events.sent</code></td>
          <td>成功送出的事件數（收到 200/207 的 accepted）</td>
          <td>flush 成功後按 accepted 累加</td>
      </tr>
      <tr>
          <td><code>sdk.events.dropped</code></td>
          <td>被 FIFO 丟棄或重試耗盡的事件數</td>
          <td>每次丟棄 +1</td>
      </tr>
      <tr>
          <td><code>sdk.flush.failures</code></td>
          <td>flush 失敗次數（429 / 5xx / timeout）</td>
          <td>每次 flush 失敗 +1</td>
      </tr>
      <tr>
          <td><code>sdk.sampling.rate</code></td>
          <td>當前動態取樣率</td>
          <td>收到 429 後更新</td>
      </tr>
  </tbody>
</table>
<p><code>produced - sampled</code> = 取樣損失（設計內）。<code>sampled - sent - dropped</code> 如果不為零，代表有事件卡在 buffer 中尚未送出或未被計入任何分類。</p>
<h3 id="collector-端指標">Collector 端指標</h3>
<p>Collector 在 <code>/metrics</code> endpoint（或 health endpoint 的擴展欄位）暴露處理計數器：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>含義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>collector.events.received</code></td>
          <td>收到的事件總數（HTTP handler 層計數）</td>
      </tr>
      <tr>
          <td><code>collector.events.rejected</code></td>
          <td>schema validation 拒絕的事件數</td>
      </tr>
      <tr>
          <td><code>collector.events.stored</code></td>
          <td>成功寫入 storage 的事件數</td>
      </tr>
      <tr>
          <td><code>collector.events.backpressure</code></td>
          <td>因 channel 滿回 429 的事件數</td>
      </tr>
      <tr>
          <td><code>collector.channel.depth</code></td>
          <td>當前 channel 中待寫入的事件數</td>
      </tr>
      <tr>
          <td><code>collector.storage.errors</code></td>
          <td>storage 寫入失敗的次數</td>
      </tr>
  </tbody>
</table>
<p><code>received - rejected - stored - backpressure</code> 如果不為零，代表有事件在 pipeline 中遺失（channel buffer 中的事件在 crash 時丟失就會造成這個差距）。</p>
<h3 id="端到端比對">端到端比對</h3>
<p>SDK 的 <code>sent</code> 和 collector 的 <code>received</code> 之間的差距是 transport 層的損失 — 網路丟包、中間件攔截（reverse proxy 的 body size limit）或 collector 重啟期間的連線失敗。</p>
<p>這個比對在自用場景下用手動 spot check 就夠（SDK log 的 sent count vs collector dashboard 的 received count）。小型以上規模需要自動化：一個定期 job 比對兩邊的計數器，差距超過閾值時告警。</p>
<h3 id="損失率的可接受範圍">損失率的可接受範圍</h3>
<table>
  <thead>
      <tr>
          <th>規模</th>
          <th>event 類損失率</th>
          <th>error 類損失率</th>
          <th>監控粒度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>自用</td>
          <td>&lt; 10%</td>
          <td>&lt; 1%</td>
          <td>手動 spot check</td>
      </tr>
      <tr>
          <td>小型團隊</td>
          <td>&lt; 5%</td>
          <td>&lt; 0.5%</td>
          <td>每日自動比對</td>
      </tr>
      <tr>
          <td>中型以上</td>
          <td>&lt; 1%</td>
          <td>&lt; 0.1%</td>
          <td>即時 dashboard + 告警</td>
      </tr>
  </tbody>
</table>
<p>閾值的推導邏輯：event 類的損失影響統計精度 — 取樣率 0.9 加上 transport 和 collector 層的少量損失，自用場景合計 &lt; 10% 是合理的上限；funnel 分析用取樣校正（除以取樣率）仍然有效。Error 類的損失直接影響 bug 發現速度 — 容忍度比 event 低一個數量級。中型以上規模的 &lt; 1% / &lt; 0.1% 接近商業方案（Sentry / Datadog）的 SLA 水準。</p>
<p><a href="/blog/monitoring/04-collector/ingestion-scaling/" data-link-title="Ingestion Scaling" data-link-desc="四層防線應對 ingestion 端的流量擴展 — SDK 取樣、Collector 背壓、水平擴展、Queue 解耦">Ingestion Scaling</a> 的 error 快通道設計就是基於這個優先級差異。</p>
<h2 id="被自己的-sdk-ddos">被自己的 SDK DDoS</h2>
<p>「SDK 產生的流量壓垮自己的 collector」是自架監控系統最常見的可靠性事故。來源是自家 SDK 的異常行為或正常行為在特定條件下的放大效應 — 內部流量失控，而非外部攻擊。外部偽造流量的防護見 <a href="/blog/monitoring/07-security-privacy/client-sdk-authentication/" data-link-title="Client-side SDK 認證的根本限制" data-link-desc="嵌在 client 端的 credential 必然可被提取 — 認清 architecture 天花板後的多層緩解策略，從 origin 驗證到 device attestation">Client-side SDK 認證</a>。</p>
<p>本段按觸發場景分類（SDK bug / 部署推送 / 使用者暴增），和 <a href="/blog/monitoring/04-collector/ingestion-scaling/" data-link-title="Ingestion Scaling" data-link-desc="四層防線應對 ingestion 端的流量擴展 — SDK 取樣、Collector 背壓、水平擴展、Queue 解耦">Ingestion Scaling</a> 的四層防線（SDK 端 / collector 單機 / 水平擴展 / queue 解耦）是不同切面。四層防線按防護位置劃分、說明機制怎麼做；本段按場景劃分、說明什麼時候哪些機制會被觸發。</p>
<h3 id="sdk-bug事件風暴">SDK bug：事件風暴</h3>
<p>SDK 程式碼 bug 導致事件無限迴圈 — 常見於事件處理器內再次觸發事件（error handler 中呼叫 <code>monitor.event()</code> 又觸發 error），或 UI 事件綁定錯誤導致每個 frame 產生一筆事件（60 fps = 每秒 60 筆）。</p>
<p><strong>損失路徑</strong>：事件風暴首先填滿 SDK buffer → 觸發高頻 flush → collector 收到大量 request → channel 滿觸發 429 → SDK 動態降採樣。如果 SDK 的動態降採樣邏輯本身也有 bug（降到 0.1 後不再降），collector 仍然會持續承壓。</p>
<p><strong>防護層級</strong>：</p>
<p>SDK 端 — 事件產生速率上限。SDK 內部維護每秒事件計數器，超過閾值（例如 100 events/sec）後的事件直接丟棄，不進 buffer。這個上限獨立於取樣和背壓機制，是防止 SDK 自身 bug 的最後一道防線。</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">// SDK 端的 rate limiter（偽碼，各語言實作不同）
</span></span><span class="line"><span class="ln">2</span><span class="cl">count = atomicIncrement(eventCounter)
</span></span><span class="line"><span class="ln">3</span><span class="cl">if count &gt; maxEventsPerSecond:
</span></span><span class="line"><span class="ln">4</span><span class="cl">    atomicIncrement(droppedCounter)
</span></span><span class="line"><span class="ln">5</span><span class="cl">    return  // 不進 buffer</span></span></code></pre></div><p>Collector 端 — per-key rate limit。每個 API key（或 source.app）的請求速率獨立限制。一個失控的 SDK 被限速時，其他 SDK 的事件不受影響。這和 <a href="/blog/monitoring/04-collector/ingestion-scaling/" data-link-title="Ingestion Scaling" data-link-desc="四層防線應對 ingestion 端的流量擴展 — SDK 取樣、Collector 背壓、水平擴展、Queue 解耦">Ingestion Scaling</a> 的 per-SDK rate limiting 是同一個機制。</p>
<p>Collector 端 — circuit breaker。如果某個 API key 的 429 回應次數在短時間內超過閾值，collector 暫時拒絕該 key 的所有請求（回 503），不再逐筆檢查 rate limit。冷卻期過後自動恢復。這降低了 rate limit 本身的 CPU 開銷 — 高頻 429 回應也有成本。閾值需高於正常 burst 的 per-key 429 頻率 — 如果正常 flush 在 burst 時每分鐘最多觸發 N 次 429，circuit breaker 閾值設為 5N-10N 避免誤觸。具體數字（例如 50 次/分鐘、5 分鐘冷卻）依部署規模調整。</p>
<h3 id="部署推送補發風暴">部署推送：補發風暴</h3>
<p>100 台機器同時重啟（rolling deploy），每台機器的 SDK 在啟動時：</p>
<ol>
<li>讀取本地 persistence 中的離線事件</li>
<li>初始化後立即 flush 離線事件 + 新的 lifecycle 事件</li>
</ol>
<p>100 個 SDK 在幾秒內同時發起離線補發 + 正常 flush，collector 瞬間承受 100 倍的正常流量。</p>
<p><strong>防護方式</strong>：init jitter — SDK 初始化後不立即 flush，而是等待一個隨機延遲（0 到 flush_interval 之間的均勻分佈）。100 個 SDK 的首次 flush 分散在 0-30 秒內，流量從一個尖峰變成斜坡。</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="kn">import</span> <span class="nn">random</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">initial_delay</span> <span class="o">=</span> <span class="n">random</span><span class="o">.</span><span class="n">uniform</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">flush_interval_seconds</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 第一次 flush 延遲 initial_delay 秒，後續按正常 interval</span></span></span></code></pre></div><p>離線補發也加 jitter — 每批補發之間的間隔從固定的 1 秒改為 1-3 秒的隨機值。100 個 SDK 的補發批次在時間軸上交錯，避免所有 SDK 以相同節奏同時送出。</p>
<h3 id="使用者行為高峰同時在線暴增">使用者行為高峰：同時在線暴增</h3>
<p>行銷活動、媒體報導、季節性高峰 — 同時在線使用者從 100 人暴增到 10,000 人。每個使用者的 SDK 正常運作，但總量超出 collector 的處理能力。</p>
<p>這個場景和 SDK bug 的差異：每個 SDK 的行為完全正常，問題在總量。Per-key rate limit 不會觸發（每個 SDK 的速率在正常範圍），需要的是全域流量控制。</p>
<p><strong>防護方式</strong>：Collector 端的全域 channel 背壓（<a href="/blog/monitoring/04-collector/ingestion-scaling/" data-link-title="Ingestion Scaling" data-link-desc="四層防線應對 ingestion 端的流量擴展 — SDK 取樣、Collector 背壓、水平擴展、Queue 解耦">Ingestion Scaling 第二層</a>）是第一道防線 — channel 滿時所有 SDK 收到 429，各自動態降採樣。如果動態降採樣後流量仍然過大，水平擴展（多 collector + load balancer）或 queue 解耦是解法。</p>
<p>行銷活動的可預測性是優勢 — 活動日期已知，可以提前擴展 collector 容量（加機器或調高 channel 容量）。突發的媒體報導則依賴動態降採樣和背壓的自動調節。</p>
<h3 id="三種場景的防護對照">三種場景的防護對照</h3>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>流量特徵</th>
          <th>首要防護</th>
          <th>次要防護</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SDK bug</td>
          <td>單 SDK 異常高頻</td>
          <td>SDK 端 rate limit + per-key limit</td>
          <td>Circuit breaker</td>
      </tr>
      <tr>
          <td>部署推送</td>
          <td>多 SDK 同時突發</td>
          <td>Init jitter + 補發 jitter</td>
          <td>Channel 背壓</td>
      </tr>
      <tr>
          <td>使用者暴增</td>
          <td>全域持續高量</td>
          <td>動態降採樣 + channel 背壓</td>
          <td>水平擴展 / queue 解耦</td>
      </tr>
  </tbody>
</table>
<h2 id="資料恢復-vs-接受損失">資料恢復 vs 接受損失</h2>
<p>每個損失點都可以投入工程努力降低損失量。問題是恢復的工程成本是否值得 — 監控資料不是交易紀錄，恢復的價值取決於損失的事件類型和數量。</p>
<h3 id="值得恢復的場景">值得恢復的場景</h3>
<p><strong>Error 事件</strong>：每筆 error 都可能對應一個需要修的 bug。Error 的損失代表 bug 可能更晚被發現、在更多使用者身上發生後才被注意到。值得投入本地 persistence、優先級丟棄（error 最後丟）、error 快通道等機制降低損失。</p>
<p><strong>Lifecycle 事件</strong>：session 邊界（session.begin / session.end）是 cohort 分析和 session replay 的基礎。丟失 session 邊界會讓整個 session 的事件無法正確歸屬。Lifecycle 事件量低（每 session 幾筆），保留成本小、損失影響大。</p>
<h3 id="接受損失的場景">接受損失的場景</h3>
<p><strong>高頻 metric 事件</strong>：render.frame_time 每秒 60 筆，丟幾筆對趨勢分析的影響在統計誤差範圍內。聚合前移（SDK 端每 5 秒送一筆 summary）比逐筆保留更有效率。</p>
<p><strong>行為 event 事件</strong>：button.click、page.view 在取樣後丟幾筆，funnel 的轉換率計算用取樣校正（除以取樣率）仍然有效。單筆行為事件的 debug 價值低 — 知道某使用者點了某按鈕通常不影響決策。</p>
<p><strong>超過保留期的原始事件</strong>：purge 後只剩聚合摘要。如果分析需求發現需要更長的原始事件保留期，調整 retention config，不要嘗試從聚合摘要「恢復」原始事件 — 那是不可能的。</p>
<h3 id="恢復成本的判斷">恢復成本的判斷</h3>
<p>本地 persistence（SDK 端把 buffer 寫到檔案系統）的實作成本和收益：</p>
<table>
  <thead>
      <tr>
          <th>因素</th>
          <th>記憶體 FIFO（簡單）</th>
          <th>本地 persistence（完整）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>實作成本</td>
          <td>array + 容量檢查</td>
          <td>檔案讀寫 + 並發安全 + 容量管理 + 去重</td>
      </tr>
      <tr>
          <td>保護範圍</td>
          <td>短暫離線（buffer 容量內）</td>
          <td>長時間離線（本地儲存容量內）</td>
      </tr>
      <tr>
          <td>不保護</td>
          <td>app 強制終止</td>
          <td>app 強制終止（寫入中的事件仍然遺失）</td>
      </tr>
      <tr>
          <td>適用場景</td>
          <td>自用工具、SDK 初期版本</td>
          <td>行動 app、離線場景頻繁的使用環境</td>
      </tr>
  </tbody>
</table>
<p>MVP 階段用記憶體 FIFO。本地 persistence 作為第二階段功能，在離線損失率超出可接受範圍時投入。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>SDK 端的離線保護 → <a href="/blog/monitoring/03-sdk-design/offline-buffer/" data-link-title="離線 buffer 與重試" data-link-desc="網路不可用時的事件保存策略 — FIFO 丟棄、本地 persistence、恢復後補發的取捨">離線 buffer 與重試</a></li>
<li>Collector 端的流量防護 → <a href="/blog/monitoring/04-collector/ingestion-scaling/" data-link-title="Ingestion Scaling" data-link-desc="四層防線應對 ingestion 端的流量擴展 — SDK 取樣、Collector 背壓、水平擴展、Queue 解耦">Ingestion Scaling</a></li>
<li>Collector 的處理鏈路 → <a href="/blog/monitoring/04-collector/architecture/" data-link-title="Collector 架構" data-link-desc="HTTP endpoint → JSON Schema 驗證 → 儲存 → 查詢 → rule engine 的五段式處理鏈路">Collector 架構</a></li>
<li>Container 環境的 graceful shutdown → <a href="/blog/monitoring/04-collector/container-deployment/" data-link-title="Container 部署設計" data-link-desc="Docker 部署 collector 的設計 — SQLite 在 overlay filesystem 的 I/O 考量、volume mount、graceful shutdown、資源限制">Container 部署設計</a></li>
<li>保留策略和降採樣 → <a href="/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進</a></li>
<li>SDK 認證和偽造流量防護 → <a href="/blog/monitoring/07-security-privacy/client-sdk-authentication/" data-link-title="Client-side SDK 認證的根本限制" data-link-desc="嵌在 client 端的 credential 必然可被提取 — 認清 architecture 天花板後的多層緩解策略，從 origin 驗證到 device attestation">Client-side SDK 認證</a></li>
</ul>
]]></content:encoded></item><item><title>Backpressure</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/</guid><description>&lt;p>Backpressure 的核心概念是「下游處理能力不足時，讓上游感知並放慢」。它把上游從「盲目送出」轉為「依下游能力送出」，讓系統在壓力下排隊、拒絕、降級或削峰，以保護下游資源並維持整體可預測性。Backpressure 的本質是「壓力從下游往上游傳遞」的訊號通道，覆蓋範圍比單純的拒絕策略更廣。 可先對照 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/in-process-channel/" data-link-title="In-Process Channel" data-link-desc="說明單一 process 內用來傳遞工作的 channel 或 queue abstraction">In-Process Channel&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Backpressure 出現在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/in-process-channel/" data-link-title="In-Process Channel" data-link-desc="說明單一 process 內用來傳遞工作的 channel 或 queue abstraction">in-process channel&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/http-client/" data-link-title="HTTP Client" data-link-desc="說明服務呼叫外部 HTTP 依賴時需要管理 timeout、連線與重試">HTTP client&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> 的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stream-pipeline/" data-link-title="Stream Pipeline" data-link-desc="說明連續資料流經多個處理階段時如何管理吞吐、順序與 backpressure ">stream pipeline&lt;/a>。它處理的是速度不匹配：進入速度高於處理速度。&lt;/p>
&lt;p>Backpressure 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit&lt;/a> 的差別在於資訊流向：rate limit 由上游主動設閘門（「每秒最多 N 個」），屬於容量規劃；backpressure 由下游回饋壓力（「我現在只能吃 M 個」），屬於動態調速。兩者常搭配使用：rate limit 處理已知的規劃容量，backpressure 處理無法預先預測的即時變化。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>需要 backpressure 的訊號包含 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth&lt;/a> 上升、記憶體持續增加、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 比例擴大、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a> 加深、下游 error rate 上升。當這些指標同時出現而上游流量維持穩定時，代表處理鏈某一段已成為瓶頸，壓力需要向上傳遞，而不是繼續往 buffer 堆積。&lt;/p>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>通知服務在行銷活動期間收到大量派送任務。若任務直接交給 worker 處理，worker 很快會塞滿下游第三方 API 的連線配額，latency 暴增、重試加倍，最終把佇列塞爆。導入 backpressure 後，服務依下游 API 實際吞吐動態調整 worker 取件速度：API 回應變慢時 worker 取件速度自動下降，上游請求端收到「任務已接收但延後送達」的回覆。整條 pipeline 的處理速度由最慢的一段決定，系統保留在可預測、可恢復的狀態。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Backpressure 導入後，團隊需要定義以下邊界：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a> 大小、排隊上限、等待期限、拒絕策略、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retry-policy/" data-link-title="Retry Policy" data-link-desc="說明重試策略如何區分暫時性錯誤、永久錯誤與副作用風險">retry policy&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-shedding/" data-link-title="Load Shedding" data-link-desc="說明服務過載時如何主動拒絕低優先工作以保護核心能力">load shedding&lt;/a> 與對使用者的回饋（429 / 503 / 延後通知）。觀測上應能看到 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth&lt;/a>、in-flight 數量、處理耗時、drop count、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a>、下游 error rate，並把關鍵指標放進 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>。&lt;/p>
&lt;p>設計取捨的核心是 buffer 尺度：buffer 太小會讓瞬間尖峰被過度拒絕，流失可接受的請求；buffer 太大則延遲失控並可能拖累記憶體。穩定做法是「有限 buffer + 明確拒絕策略」，讓系統在超載時 fail fast，避免把壓力延後累積成更大的雪崩。&lt;/p>
&lt;p>監控系統中 collector 用 HTTP 429 向 SDK 傳遞背壓的具體實作見 &lt;a href="https://tarrragon.github.io/blog/monitoring/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="下游處理能力不足時向上游回傳「慢下來」訊號的流量控制機制 — 監控系統中 collector 用 HTTP 429 向 SDK 傳遞背壓">監控知識卡：Backpressure&lt;/a>。&lt;/p>
&lt;!--
codex-check:
 C1-intent: pass
 C2-atomic: pass (聚焦 backpressure 本身，相鄰概念皆以連結引用)
 C3-business-first: pass (定義段落說明要解決的問題)
 C4-reasoning-path: pass (概念位置 → 訊號 → 例子 → 設計責任)
 C5-searchable: pass (關鍵詞 backpressure / buffer / queue depth 皆 grep 友善)
 C6-positive-framing: pass (「避免」僅用於安全警示語境，屬正面表述例外（物理限制、安全警示、反模式對照）)
 C7-source-verified: pass
 reviewed-at: 2026-04-23
 role: reference-sample (brief 檔案結構段指定正面範例卡片)
--></description><content:encoded><![CDATA[<p>Backpressure 的核心概念是「下游處理能力不足時，讓上游感知並放慢」。它把上游從「盲目送出」轉為「依下游能力送出」，讓系統在壓力下排隊、拒絕、降級或削峰，以保護下游資源並維持整體可預測性。Backpressure 的本質是「壓力從下游往上游傳遞」的訊號通道，覆蓋範圍比單純的拒絕策略更廣。 可先對照 <a href="/blog/backend/knowledge-cards/in-process-channel/" data-link-title="In-Process Channel" data-link-desc="說明單一 process 內用來傳遞工作的 channel 或 queue abstraction">In-Process Channel</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>Backpressure 出現在 <a href="/blog/backend/knowledge-cards/in-process-channel/" data-link-title="In-Process Channel" data-link-desc="說明單一 process 內用來傳遞工作的 channel 或 queue abstraction">in-process channel</a>、<a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a>、<a href="/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool</a>、<a href="/blog/backend/knowledge-cards/http-client/" data-link-title="HTTP Client" data-link-desc="說明服務呼叫外部 HTTP 依賴時需要管理 timeout、連線與重試">HTTP client</a>、<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a>、<a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> 的 <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 與 <a href="/blog/backend/knowledge-cards/stream-pipeline/" data-link-title="Stream Pipeline" data-link-desc="說明連續資料流經多個處理階段時如何管理吞吐、順序與 backpressure ">stream pipeline</a>。它處理的是速度不匹配：進入速度高於處理速度。</p>
<p>Backpressure 與 <a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit</a> 的差別在於資訊流向：rate limit 由上游主動設閘門（「每秒最多 N 個」），屬於容量規劃；backpressure 由下游回饋壓力（「我現在只能吃 M 個」），屬於動態調速。兩者常搭配使用：rate limit 處理已知的規劃容量，backpressure 處理無法預先預測的即時變化。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>需要 backpressure 的訊號包含 <a href="/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth</a> 上升、記憶體持續增加、<a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 比例擴大、<a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a> 加深、下游 error rate 上升。當這些指標同時出現而上游流量維持穩定時，代表處理鏈某一段已成為瓶頸，壓力需要向上傳遞，而不是繼續往 buffer 堆積。</p>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>通知服務在行銷活動期間收到大量派送任務。若任務直接交給 worker 處理，worker 很快會塞滿下游第三方 API 的連線配額，latency 暴增、重試加倍，最終把佇列塞爆。導入 backpressure 後，服務依下游 API 實際吞吐動態調整 worker 取件速度：API 回應變慢時 worker 取件速度自動下降，上游請求端收到「任務已接收但延後送達」的回覆。整條 pipeline 的處理速度由最慢的一段決定，系統保留在可預測、可恢復的狀態。</p>
<h2 id="設計責任">設計責任</h2>
<p>Backpressure 導入後，團隊需要定義以下邊界：<a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 大小、排隊上限、等待期限、拒絕策略、<a href="/blog/backend/knowledge-cards/retry-policy/" data-link-title="Retry Policy" data-link-desc="說明重試策略如何區分暫時性錯誤、永久錯誤與副作用風險">retry policy</a>、<a href="/blog/backend/knowledge-cards/load-shedding/" data-link-title="Load Shedding" data-link-desc="說明服務過載時如何主動拒絕低優先工作以保護核心能力">load shedding</a> 與對使用者的回饋（429 / 503 / 延後通知）。觀測上應能看到 <a href="/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth</a>、in-flight 數量、處理耗時、drop count、<a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a>、下游 error rate，並把關鍵指標放進 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>。</p>
<p>設計取捨的核心是 buffer 尺度：buffer 太小會讓瞬間尖峰被過度拒絕，流失可接受的請求；buffer 太大則延遲失控並可能拖累記憶體。穩定做法是「有限 buffer + 明確拒絕策略」，讓系統在超載時 fail fast，避免把壓力延後累積成更大的雪崩。</p>
<p>監控系統中 collector 用 HTTP 429 向 SDK 傳遞背壓的具體實作見 <a href="/blog/monitoring/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="下游處理能力不足時向上游回傳「慢下來」訊號的流量控制機制 — 監控系統中 collector 用 HTTP 429 向 SDK 傳遞背壓">監控知識卡：Backpressure</a>。</p>
<!--
codex-check:
  C1-intent: pass
  C2-atomic: pass (聚焦 backpressure 本身，相鄰概念皆以連結引用)
  C3-business-first: pass (定義段落說明要解決的問題)
  C4-reasoning-path: pass (概念位置 → 訊號 → 例子 → 設計責任)
  C5-searchable: pass (關鍵詞 backpressure / buffer / queue depth 皆 grep 友善)
  C6-positive-framing: pass (「避免」僅用於安全警示語境，屬正面表述例外（物理限制、安全警示、反模式對照）)
  C7-source-verified: pass
  reviewed-at: 2026-04-23
  role: reference-sample (brief 檔案結構段指定正面範例卡片)
-->
]]></content:encoded></item></channel></rss>