<?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>模組三：流量管控 on Tarragon</title><link>https://tarrragon.github.io/blog/devops/03-traffic-management/</link><description>Recent content in 模組三：流量管控 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/devops/03-traffic-management/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>Rate Limiting</title><link>https://tarrragon.github.io/blog/devops/03-traffic-management/rate-limiting/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/devops/03-traffic-management/rate-limiting/</guid><description>&lt;p>Rate limiting 是主動的流量控制 — 在系統還沒過載之前，就限制每個來源的請求速率。和背壓不同，rate limit 的觸發依據是預設的速率上限，而非實際的系統負載。&lt;/p>
&lt;h2 id="兩個粒度">兩個粒度&lt;/h2>
&lt;h3 id="per-client每來源限速">Per-client（每來源限速）&lt;/h3>
&lt;p>限制每個 client（by API key / IP / SDK instance）的請求速率。防止單一來源打爆系統。&lt;/p>
&lt;p>自用場景下 per-client 限速的價值不高（只有自己的 SDK），但開源工具被多人部署後，per-client 限速防止某個失控的 SDK 影響其他來源。&lt;/p>
&lt;h3 id="global全局限速">Global（全局限速）&lt;/h3>
&lt;p>限制系統的總吞吐量。不管多少個 client，collector 每秒最多處理 N 個事件。&lt;/p>
&lt;p>Global 限速是系統保護的最後一道線 — 即使每個 client 都在限速內，所有 client 加起來可能超過系統承載。Global 限速確保總量不超過系統能力。&lt;/p>
&lt;h2 id="演算法">演算法&lt;/h2>
&lt;h3 id="token-bucket">Token Bucket&lt;/h3>
&lt;p>桶裡有固定數量的 token，每個請求消耗一個 token，token 按固定速率補充。桶空了就拒絕。&lt;/p>
&lt;p>特點：允許短暫 burst（桶滿時一次消耗多個 token），但長期平均不超過補充速率。適合「允許偶爾的高峰但長期平均要在限制內」的場景。&lt;/p>
&lt;h3 id="sliding-window">Sliding Window&lt;/h3>
&lt;p>在固定的時間窗口（如 1 分鐘）內計數請求。超過上限就拒絕。窗口結束時計數重設。&lt;/p>
&lt;p>特點：嚴格的速率限制（窗口內不會超過 N 個），但窗口邊界有突增風險（上一個窗口末尾 + 下一個窗口開頭各 N 個 = 瞬間 2N）。滑動窗口（sliding window log / counter）解決邊界問題但記憶體較高。&lt;/p>
&lt;h3 id="選擇">選擇&lt;/h3>
&lt;p>自架監控系統推薦 token bucket — 允許 SDK 的 flush burst（一次送 100 個事件是正常行為），但限制長期平均速率。&lt;/p>
&lt;h2 id="http-429--retry-after">HTTP 429 + Retry-After&lt;/h2>
&lt;p>限速觸發時回 HTTP 429 Too Many Requests，帶 &lt;code>Retry-After&lt;/code> header 和 rate limit 相關 header：&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">HTTP/1.1 429 Too Many Requests
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">Retry-After: 5
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">X-RateLimit-Limit: 1000
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">X-RateLimit-Remaining: 0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">X-RateLimit-Reset: 1719302400&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>SDK 收到 429 後觸發離線 buffer 暫存事件，&lt;code>Retry-After&lt;/code> 秒後重試。&lt;/p>
&lt;h2 id="優先級豁免">優先級豁免&lt;/h2>
&lt;p>某些請求不應被限速：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>請求類型&lt;/th>
 &lt;th>限速？&lt;/th>
 &lt;th>理由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Health check&lt;/td>
 &lt;td>不限&lt;/td>
 &lt;td>探活請求被限速等於 LB 誤判服務掛了&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Error 事件&lt;/td>
 &lt;td>不限或較寬&lt;/td>
 &lt;td>Debug 價值最高、丟了就查不到&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Event 事件&lt;/td>
 &lt;td>限速&lt;/td>
 &lt;td>量大、行為分析可以接受取樣&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Metric 事件&lt;/td>
 &lt;td>限速&lt;/td>
 &lt;td>高頻取樣可以降頻&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>優先級的判斷依據是「這個事件丟了的代價」。Error 事件丟了影響 debug 能力，event 事件丟了影響行為分析精度 — 前者的代價更高。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>被動的流量控制 → &lt;a href="https://tarrragon.github.io/blog/devops/03-traffic-management/backpressure/" data-link-title="背壓機制" data-link-desc="下游處理慢時上游怎麼減速 — 有限 buffer &amp;#43; 回壓訊號的設計、和 rate limit 的區別">背壓機制&lt;/a>&lt;/li>
&lt;li>依賴失敗時的快速失敗 → &lt;a href="https://tarrragon.github.io/blog/devops/03-traffic-management/circuit-breaker/" data-link-title="熔斷器" data-link-desc="依賴服務失敗時怎麼快速失敗而非拖慢自己 — 三狀態模型（closed → open → half-open）和熔斷判斷條件">熔斷器&lt;/a>&lt;/li>
&lt;li>不同工作負載的資源隔離 → &lt;a href="https://tarrragon.github.io/blog/devops/03-traffic-management/bulkhead/" data-link-title="Bulkhead 隔離" data-link-desc="不同工作負載的資源池隔離 — 一個功能過載不拖垮其他功能的隔艙設計">Bulkhead 隔離&lt;/a>&lt;/li>
&lt;li>Backend 的 rate limit 實作（middleware / Redis / 配額設計）→ &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/rate-limit-implementation/" data-link-title="Rate Limit 實作" data-link-desc="單機 middleware / Redis 分散式限速 / 配額設計 — 概念見 DevOps 流量管控，本章聚焦後端實作">Rate Limit 實作&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Rate limiting 是主動的流量控制 — 在系統還沒過載之前，就限制每個來源的請求速率。和背壓不同，rate limit 的觸發依據是預設的速率上限，而非實際的系統負載。</p>
<h2 id="兩個粒度">兩個粒度</h2>
<h3 id="per-client每來源限速">Per-client（每來源限速）</h3>
<p>限制每個 client（by API key / IP / SDK instance）的請求速率。防止單一來源打爆系統。</p>
<p>自用場景下 per-client 限速的價值不高（只有自己的 SDK），但開源工具被多人部署後，per-client 限速防止某個失控的 SDK 影響其他來源。</p>
<h3 id="global全局限速">Global（全局限速）</h3>
<p>限制系統的總吞吐量。不管多少個 client，collector 每秒最多處理 N 個事件。</p>
<p>Global 限速是系統保護的最後一道線 — 即使每個 client 都在限速內，所有 client 加起來可能超過系統承載。Global 限速確保總量不超過系統能力。</p>
<h2 id="演算法">演算法</h2>
<h3 id="token-bucket">Token Bucket</h3>
<p>桶裡有固定數量的 token，每個請求消耗一個 token，token 按固定速率補充。桶空了就拒絕。</p>
<p>特點：允許短暫 burst（桶滿時一次消耗多個 token），但長期平均不超過補充速率。適合「允許偶爾的高峰但長期平均要在限制內」的場景。</p>
<h3 id="sliding-window">Sliding Window</h3>
<p>在固定的時間窗口（如 1 分鐘）內計數請求。超過上限就拒絕。窗口結束時計數重設。</p>
<p>特點：嚴格的速率限制（窗口內不會超過 N 個），但窗口邊界有突增風險（上一個窗口末尾 + 下一個窗口開頭各 N 個 = 瞬間 2N）。滑動窗口（sliding window log / counter）解決邊界問題但記憶體較高。</p>
<h3 id="選擇">選擇</h3>
<p>自架監控系統推薦 token bucket — 允許 SDK 的 flush burst（一次送 100 個事件是正常行為），但限制長期平均速率。</p>
<h2 id="http-429--retry-after">HTTP 429 + Retry-After</h2>
<p>限速觸發時回 HTTP 429 Too Many Requests，帶 <code>Retry-After</code> header 和 rate limit 相關 header：</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">HTTP/1.1 429 Too Many Requests
</span></span><span class="line"><span class="ln">2</span><span class="cl">Retry-After: 5
</span></span><span class="line"><span class="ln">3</span><span class="cl">X-RateLimit-Limit: 1000
</span></span><span class="line"><span class="ln">4</span><span class="cl">X-RateLimit-Remaining: 0
</span></span><span class="line"><span class="ln">5</span><span class="cl">X-RateLimit-Reset: 1719302400</span></span></code></pre></div><p>SDK 收到 429 後觸發離線 buffer 暫存事件，<code>Retry-After</code> 秒後重試。</p>
<h2 id="優先級豁免">優先級豁免</h2>
<p>某些請求不應被限速：</p>
<table>
  <thead>
      <tr>
          <th>請求類型</th>
          <th>限速？</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Health check</td>
          <td>不限</td>
          <td>探活請求被限速等於 LB 誤判服務掛了</td>
      </tr>
      <tr>
          <td>Error 事件</td>
          <td>不限或較寬</td>
          <td>Debug 價值最高、丟了就查不到</td>
      </tr>
      <tr>
          <td>Event 事件</td>
          <td>限速</td>
          <td>量大、行為分析可以接受取樣</td>
      </tr>
      <tr>
          <td>Metric 事件</td>
          <td>限速</td>
          <td>高頻取樣可以降頻</td>
      </tr>
  </tbody>
</table>
<p>優先級的判斷依據是「這個事件丟了的代價」。Error 事件丟了影響 debug 能力，event 事件丟了影響行為分析精度 — 前者的代價更高。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>被動的流量控制 → <a href="/blog/devops/03-traffic-management/backpressure/" data-link-title="背壓機制" data-link-desc="下游處理慢時上游怎麼減速 — 有限 buffer &#43; 回壓訊號的設計、和 rate limit 的區別">背壓機制</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/03-traffic-management/bulkhead/" data-link-title="Bulkhead 隔離" data-link-desc="不同工作負載的資源池隔離 — 一個功能過載不拖垮其他功能的隔艙設計">Bulkhead 隔離</a></li>
<li>Backend 的 rate limit 實作（middleware / Redis / 配額設計）→ <a href="/blog/backend/09-performance-capacity/rate-limit-implementation/" data-link-title="Rate Limit 實作" data-link-desc="單機 middleware / Redis 分散式限速 / 配額設計 — 概念見 DevOps 流量管控，本章聚焦後端實作">Rate Limit 實作</a></li>
</ul>
]]></content:encoded></item><item><title>熔斷器</title><link>https://tarrragon.github.io/blog/devops/03-traffic-management/circuit-breaker/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/devops/03-traffic-management/circuit-breaker/</guid><description>&lt;p>熔斷器保護的是「呼叫外部依賴」的路徑。當外部依賴（資料庫、第三方 API、通知服務）持續失敗時，熔斷器讓後續的呼叫立即失敗（回傳預設值或錯誤），而非每次都等待逾時。等待逾時的代價是佔住 goroutine / thread 不釋放，積累到一定數量就拖垮整個服務。&lt;/p>
&lt;h2 id="三狀態模型">三狀態模型&lt;/h2>
&lt;h3 id="closed正常">Closed（正常）&lt;/h3>
&lt;p>所有呼叫正常通過。熔斷器記錄成功和失敗的計數。&lt;/p>
&lt;h3 id="open熔斷">Open（熔斷）&lt;/h3>
&lt;p>當失敗率或連續失敗次數超過閾值時，熔斷器進入 open 狀態。此後所有呼叫&lt;strong>立即回傳錯誤&lt;/strong>，不實際呼叫外部依賴。&lt;/p>
&lt;p>Open 狀態持續固定時間（如 30 秒），時間到後進入 half-open。&lt;/p>
&lt;h3 id="half-open探測">Half-open（探測）&lt;/h3>
&lt;p>允許少量呼叫（如 1 個）實際通過到外部依賴。如果成功 → 回到 closed；如果失敗 → 回到 open（重設計時器）。&lt;/p>
&lt;p>Half-open 的目的是自動探測依賴是否恢復，不需要人工介入。&lt;/p>
&lt;h2 id="熔斷判斷條件">熔斷判斷條件&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>條件&lt;/th>
 &lt;th>適用場景&lt;/th>
 &lt;th>參數&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>連續 N 次失敗&lt;/td>
 &lt;td>依賴完全不可用&lt;/td>
 &lt;td>N = 5-10&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失敗率 &amp;gt; X%&lt;/td>
 &lt;td>依賴間歇性失敗&lt;/td>
 &lt;td>X = 50%，統計窗口 = 10 秒&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>平均延遲 &amp;gt; Y ms&lt;/td>
 &lt;td>依賴變慢但未失敗&lt;/td>
 &lt;td>Y = 依據 SLA 設定&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「失敗」的定義需要明確：HTTP 5xx 是失敗、4xx 通常不是（client 的問題）、timeout 是失敗、connection refused 是失敗。&lt;/p>
&lt;h2 id="熔斷時的-fallback">熔斷時的 fallback&lt;/h2>
&lt;p>熔斷觸發後，呼叫端收到的是「快速失敗」而非逾時。呼叫端需要有 fallback 策略：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>依賴&lt;/th>
 &lt;th>Fallback&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>通知服務（Slack webhook）&lt;/td>
 &lt;td>記錄到本地 log、恢復後補發&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>外部 API（enrichment）&lt;/td>
 &lt;td>回傳無 enrichment 的原始資料&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>認證服務&lt;/td>
 &lt;td>用本地 cache 的 token 驗證（短暫降級）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>沒有 fallback 的依賴被熔斷 = 對應功能完全不可用。熔斷器保護的是「不讓不可用的功能拖垮整個服務」。&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> 的 rule engine 在規則命中時可能呼叫外部服務（Slack webhook、HTTP POST 到 alert endpoint）。如果外部服務掛了，每個命中的規則都會等待逾時 — 大量規則命中時 goroutine 積壓。&lt;/p>
&lt;p>熔斷器包在 rule engine 的「執行外部動作」環節：連續 5 次外部呼叫失敗 → 熔斷 → 後續規則命中不再嘗試外部呼叫、改寫本地 log → 30 秒後探測一次 → 外部服務恢復 → 恢復正常呼叫。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>被動的流量控制 → &lt;a href="https://tarrragon.github.io/blog/devops/03-traffic-management/backpressure/" data-link-title="背壓機制" data-link-desc="下游處理慢時上游怎麼減速 — 有限 buffer &amp;#43; 回壓訊號的設計、和 rate limit 的區別">背壓機制&lt;/a>&lt;/li>
&lt;li>主動的速率限制 → &lt;a href="https://tarrragon.github.io/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&lt;/a>&lt;/li>
&lt;li>不同工作負載的資源隔離 → &lt;a href="https://tarrragon.github.io/blog/devops/03-traffic-management/bulkhead/" data-link-title="Bulkhead 隔離" data-link-desc="不同工作負載的資源池隔離 — 一個功能過載不拖垮其他功能的隔艙設計">Bulkhead 隔離&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>熔斷器保護的是「呼叫外部依賴」的路徑。當外部依賴（資料庫、第三方 API、通知服務）持續失敗時，熔斷器讓後續的呼叫立即失敗（回傳預設值或錯誤），而非每次都等待逾時。等待逾時的代價是佔住 goroutine / thread 不釋放，積累到一定數量就拖垮整個服務。</p>
<h2 id="三狀態模型">三狀態模型</h2>
<h3 id="closed正常">Closed（正常）</h3>
<p>所有呼叫正常通過。熔斷器記錄成功和失敗的計數。</p>
<h3 id="open熔斷">Open（熔斷）</h3>
<p>當失敗率或連續失敗次數超過閾值時，熔斷器進入 open 狀態。此後所有呼叫<strong>立即回傳錯誤</strong>，不實際呼叫外部依賴。</p>
<p>Open 狀態持續固定時間（如 30 秒），時間到後進入 half-open。</p>
<h3 id="half-open探測">Half-open（探測）</h3>
<p>允許少量呼叫（如 1 個）實際通過到外部依賴。如果成功 → 回到 closed；如果失敗 → 回到 open（重設計時器）。</p>
<p>Half-open 的目的是自動探測依賴是否恢復，不需要人工介入。</p>
<h2 id="熔斷判斷條件">熔斷判斷條件</h2>
<table>
  <thead>
      <tr>
          <th>條件</th>
          <th>適用場景</th>
          <th>參數</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>連續 N 次失敗</td>
          <td>依賴完全不可用</td>
          <td>N = 5-10</td>
      </tr>
      <tr>
          <td>失敗率 &gt; X%</td>
          <td>依賴間歇性失敗</td>
          <td>X = 50%，統計窗口 = 10 秒</td>
      </tr>
      <tr>
          <td>平均延遲 &gt; Y ms</td>
          <td>依賴變慢但未失敗</td>
          <td>Y = 依據 SLA 設定</td>
      </tr>
  </tbody>
</table>
<p>「失敗」的定義需要明確：HTTP 5xx 是失敗、4xx 通常不是（client 的問題）、timeout 是失敗、connection refused 是失敗。</p>
<h2 id="熔斷時的-fallback">熔斷時的 fallback</h2>
<p>熔斷觸發後，呼叫端收到的是「快速失敗」而非逾時。呼叫端需要有 fallback 策略：</p>
<table>
  <thead>
      <tr>
          <th>依賴</th>
          <th>Fallback</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>通知服務（Slack webhook）</td>
          <td>記錄到本地 log、恢復後補發</td>
      </tr>
      <tr>
          <td>外部 API（enrichment）</td>
          <td>回傳無 enrichment 的原始資料</td>
      </tr>
      <tr>
          <td>認證服務</td>
          <td>用本地 cache 的 token 驗證（短暫降級）</td>
      </tr>
  </tbody>
</table>
<p>沒有 fallback 的依賴被熔斷 = 對應功能完全不可用。熔斷器保護的是「不讓不可用的功能拖垮整個服務」。</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> 的 rule engine 在規則命中時可能呼叫外部服務（Slack webhook、HTTP POST 到 alert endpoint）。如果外部服務掛了，每個命中的規則都會等待逾時 — 大量規則命中時 goroutine 積壓。</p>
<p>熔斷器包在 rule engine 的「執行外部動作」環節：連續 5 次外部呼叫失敗 → 熔斷 → 後續規則命中不再嘗試外部呼叫、改寫本地 log → 30 秒後探測一次 → 外部服務恢復 → 恢復正常呼叫。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>被動的流量控制 → <a href="/blog/devops/03-traffic-management/backpressure/" data-link-title="背壓機制" data-link-desc="下游處理慢時上游怎麼減速 — 有限 buffer &#43; 回壓訊號的設計、和 rate limit 的區別">背壓機制</a></li>
<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/bulkhead/" data-link-title="Bulkhead 隔離" data-link-desc="不同工作負載的資源池隔離 — 一個功能過載不拖垮其他功能的隔艙設計">Bulkhead 隔離</a></li>
</ul>
]]></content:encoded></item><item><title>Bulkhead 隔離</title><link>https://tarrragon.github.io/blog/devops/03-traffic-management/bulkhead/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/devops/03-traffic-management/bulkhead/</guid><description>&lt;p>Bulkhead 的概念來自船舶的隔艙設計 — 船體分成多個獨立的水密隔艙，一個隔艙進水不會讓整艘船沉沒。服務設計中，bulkhead 把不同的工作負載隔離到各自的資源池，一個工作負載的過載或故障不會消耗其他工作負載的資源。&lt;/p>
&lt;h2 id="隔離什麼">隔離什麼&lt;/h2>
&lt;p>服務中的共享資源是 bulkhead 的隔離對象：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>共享資源&lt;/th>
 &lt;th>不隔離時的風險&lt;/th>
 &lt;th>隔離方式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Goroutine/Thread&lt;/td>
 &lt;td>一個慢查詢佔住所有 goroutine，整個服務不回應&lt;/td>
 &lt;td>每類工作分配獨立的 goroutine pool&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料庫連線&lt;/td>
 &lt;td>一個大查詢佔住 connection pool，其他查詢排隊&lt;/td>
 &lt;td>不同工作類型用不同的連線池&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>記憶體&lt;/td>
 &lt;td>一個功能的 buffer 無限增長，OOM 殺掉整個 process&lt;/td>
 &lt;td>每個功能的 buffer 有獨立上限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CPU&lt;/td>
 &lt;td>一個計算密集任務佔滿 CPU，其他請求延遲&lt;/td>
 &lt;td>cgroup 或 GOMAXPROCS 限制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="實作模式">實作模式&lt;/h2>
&lt;h3 id="獨立-goroutine-pool">獨立 Goroutine Pool&lt;/h3>
&lt;p>Go 中用有限容量的 channel 模擬 goroutine pool：&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="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">ingestPool&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="kd">struct&lt;/span>&lt;span class="p">{},&lt;/span> &lt;span class="mi">100&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// ingestion 最多 100 goroutine&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">queryPool&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="kd">struct&lt;/span>&lt;span class="p">{},&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// query 最多 20 goroutine&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">rulePool&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="kd">struct&lt;/span>&lt;span class="p">{},&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// rule engine 最多 10 goroutine&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&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"> 8&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"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="nx">ingestPool&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="kd">struct&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="k">defer&lt;/span> &lt;span class="kd">func&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">ingestPool&lt;/span> &lt;span class="p">}()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nf">processIngest&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">12&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">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;ingestion overloaded&amp;#34;&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">StatusServiceUnavailable&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Ingestion 的 100 個 goroutine 全部被佔用時，新的 ingestion 請求被拒絕（503），但 query 和 rule engine 的 goroutine 不受影響。&lt;/p>
&lt;h3 id="獨立-connection-pool">獨立 Connection Pool&lt;/h3>
&lt;p>資料庫連線池按工作類型分開：&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="c1">// driver name 依實際使用的 driver 而定（modernc.org/sqlite 用 &amp;#34;sqlite&amp;#34;、mattn/go-sqlite3 用 &amp;#34;sqlite3&amp;#34;）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nx">ingestDB&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">sql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Open&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;sqlite&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;events.db&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">ingestDB&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">SetMaxOpenConns&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// ingestion 專用&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nx">queryDB&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">sql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Open&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;sqlite&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;events.db&amp;#34;&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="nx">queryDB&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">SetMaxOpenConns&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// query 專用&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>SQLite 的特殊情況：SQLite 是檔案級鎖定，多個連線池打開同一個檔案時仍共享 write lock。連線池隔離在 SQLite 上主要隔離的是 Go 層的 goroutine 等待，不是 DB 層的鎖定。PostgreSQL 的連線池隔離則是真正的資源隔離。&lt;/p>
&lt;h2 id="容量分配">容量分配&lt;/h2>
&lt;p>Bulkhead 的每個隔艙分配多少資源是設計決策。分配依據是「這個工作負載的優先順序和預期併發量」：&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Ingestion&lt;/td>
 &lt;td>高（不能丟事件）&lt;/td>
 &lt;td>高（多 SDK 同時 flush）&lt;/td>
 &lt;td>60%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query&lt;/td>
 &lt;td>中（dashboard 查詢）&lt;/td>
 &lt;td>低（dashboard 定期刷新）&lt;/td>
 &lt;td>25%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Rule engine&lt;/td>
 &lt;td>低（觸發可延遲）&lt;/td>
 &lt;td>低（規則命中是少數事件）&lt;/td>
 &lt;td>15%&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>分配比例不需要精確 — 重點是每個隔艙有獨立的上限，而非共享一個無差別的總上限。&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> 同時承載 ingestion（接收事件）、query（dashboard 查詢）和 rule engine（規則評估）三種工作。不隔離時，一個複雜的 dashboard 查詢（full table scan）可能佔住所有資料庫連線，讓 ingestion 的寫入也排隊等待。&lt;/p>
&lt;p>Bulkhead 設計讓 ingestion 和 query 各自的過載互不影響：&lt;/p></description><content:encoded><![CDATA[<p>Bulkhead 的概念來自船舶的隔艙設計 — 船體分成多個獨立的水密隔艙，一個隔艙進水不會讓整艘船沉沒。服務設計中，bulkhead 把不同的工作負載隔離到各自的資源池，一個工作負載的過載或故障不會消耗其他工作負載的資源。</p>
<h2 id="隔離什麼">隔離什麼</h2>
<p>服務中的共享資源是 bulkhead 的隔離對象：</p>
<table>
  <thead>
      <tr>
          <th>共享資源</th>
          <th>不隔離時的風險</th>
          <th>隔離方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Goroutine/Thread</td>
          <td>一個慢查詢佔住所有 goroutine，整個服務不回應</td>
          <td>每類工作分配獨立的 goroutine pool</td>
      </tr>
      <tr>
          <td>資料庫連線</td>
          <td>一個大查詢佔住 connection pool，其他查詢排隊</td>
          <td>不同工作類型用不同的連線池</td>
      </tr>
      <tr>
          <td>記憶體</td>
          <td>一個功能的 buffer 無限增長，OOM 殺掉整個 process</td>
          <td>每個功能的 buffer 有獨立上限</td>
      </tr>
      <tr>
          <td>CPU</td>
          <td>一個計算密集任務佔滿 CPU，其他請求延遲</td>
          <td>cgroup 或 GOMAXPROCS 限制</td>
      </tr>
  </tbody>
</table>
<h2 id="實作模式">實作模式</h2>
<h3 id="獨立-goroutine-pool">獨立 Goroutine Pool</h3>
<p>Go 中用有限容量的 channel 模擬 goroutine pool：</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="p">(</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">ingestPool</span> <span class="p">=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kd">struct</span><span class="p">{},</span> <span class="mi">100</span><span class="p">)</span>  <span class="c1">// ingestion 最多 100 goroutine</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">queryPool</span>  <span class="p">=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kd">struct</span><span class="p">{},</span> <span class="mi">20</span><span class="p">)</span>   <span class="c1">// query 最多 20 goroutine</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">rulePool</span>   <span class="p">=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kd">struct</span><span class="p">{},</span> <span class="mi">10</span><span class="p">)</span>   <span class="c1">// rule engine 最多 10 goroutine</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">)</span>
</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 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"> 8</span><span class="cl">    <span class="k">select</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">case</span> <span class="nx">ingestPool</span> <span class="o">&lt;-</span> <span class="kd">struct</span><span class="p">{}{}:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">defer</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> <span class="o">&lt;-</span><span class="nx">ingestPool</span> <span class="p">}()</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="nf">processIngest</span><span class="p">(</span><span class="nx">r</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">default</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="nx">http</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">&#34;ingestion overloaded&#34;</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StatusServiceUnavailable</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></code></pre></div><p>Ingestion 的 100 個 goroutine 全部被佔用時，新的 ingestion 請求被拒絕（503），但 query 和 rule engine 的 goroutine 不受影響。</p>
<h3 id="獨立-connection-pool">獨立 Connection Pool</h3>
<p>資料庫連線池按工作類型分開：</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">// driver name 依實際使用的 driver 而定（modernc.org/sqlite 用 &#34;sqlite&#34;、mattn/go-sqlite3 用 &#34;sqlite3&#34;）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">ingestDB</span> <span class="o">:=</span> <span class="nx">sql</span><span class="p">.</span><span class="nf">Open</span><span class="p">(</span><span class="s">&#34;sqlite&#34;</span><span class="p">,</span> <span class="s">&#34;events.db&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">ingestDB</span><span class="p">.</span><span class="nf">SetMaxOpenConns</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span> <span class="c1">// ingestion 專用</span>
</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"><span class="nx">queryDB</span> <span class="o">:=</span> <span class="nx">sql</span><span class="p">.</span><span class="nf">Open</span><span class="p">(</span><span class="s">&#34;sqlite&#34;</span><span class="p">,</span> <span class="s">&#34;events.db&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">queryDB</span><span class="p">.</span><span class="nf">SetMaxOpenConns</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>   <span class="c1">// query 專用</span></span></span></code></pre></div><p>SQLite 的特殊情況：SQLite 是檔案級鎖定，多個連線池打開同一個檔案時仍共享 write lock。連線池隔離在 SQLite 上主要隔離的是 Go 層的 goroutine 等待，不是 DB 層的鎖定。PostgreSQL 的連線池隔離則是真正的資源隔離。</p>
<h2 id="容量分配">容量分配</h2>
<p>Bulkhead 的每個隔艙分配多少資源是設計決策。分配依據是「這個工作負載的優先順序和預期併發量」：</p>
<table>
  <thead>
      <tr>
          <th>工作負載</th>
          <th>優先順序</th>
          <th>預期併發</th>
          <th>分配</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ingestion</td>
          <td>高（不能丟事件）</td>
          <td>高（多 SDK 同時 flush）</td>
          <td>60%</td>
      </tr>
      <tr>
          <td>Query</td>
          <td>中（dashboard 查詢）</td>
          <td>低（dashboard 定期刷新）</td>
          <td>25%</td>
      </tr>
      <tr>
          <td>Rule engine</td>
          <td>低（觸發可延遲）</td>
          <td>低（規則命中是少數事件）</td>
          <td>15%</td>
      </tr>
  </tbody>
</table>
<p>分配比例不需要精確 — 重點是每個隔艙有獨立的上限，而非共享一個無差別的總上限。</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> 同時承載 ingestion（接收事件）、query（dashboard 查詢）和 rule engine（規則評估）三種工作。不隔離時，一個複雜的 dashboard 查詢（full table scan）可能佔住所有資料庫連線，讓 ingestion 的寫入也排隊等待。</p>
<p>Bulkhead 設計讓 ingestion 和 query 各自的過載互不影響：</p>
<ul>
<li>Ingestion 的 goroutine pool 滿了 → SDK 收到 429 → 離線 buffer 接手</li>
<li>Query 的 goroutine pool 滿了 → dashboard 暫時顯示 loading → 不影響 ingestion</li>
<li>Rule engine 的 goroutine pool 滿了 → 規則評估延遲 → 不影響事件接收和查詢</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>背壓的流量控制 → <a href="/blog/devops/03-traffic-management/backpressure/" data-link-title="背壓機制" data-link-desc="下游處理慢時上游怎麼減速 — 有限 buffer &#43; 回壓訊號的設計、和 rate limit 的區別">背壓機制</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></channel></rss>