<?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>Loss on Tarragon</title><link>https://tarrragon.github.io/blog/tags/loss/</link><description>Recent content in Loss 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/loss/index.xml" rel="self" type="application/rss+xml"/><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></channel></rss>