<?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>Batch on Tarragon</title><link>https://tarrragon.github.io/blog/tags/batch/</link><description>Recent content in Batch on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 19 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/batch/index.xml" rel="self" type="application/rss+xml"/><item><title>攢批送出策略</title><link>https://tarrragon.github.io/blog/monitoring/03-sdk-design/batch-flush/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/03-sdk-design/batch-flush/</guid><description>&lt;p>攢批送出策略控制事件從 SDK 內部 buffer 送到 collector 的時機。事件產生後先進入記憶體 buffer，累積到一定數量或間隔一定時間後，一次性透過 HTTP POST 送出整批事件。攢批的目的是減少網路請求次數 — 100 筆事件合併成一個 HTTP 請求，比 100 個獨立請求的網路開銷低。&lt;/p>
&lt;h2 id="三個觸發條件">三個觸發條件&lt;/h2>
&lt;h3 id="時間觸發flush-interval">時間觸發（flush interval）&lt;/h3>
&lt;p>固定間隔自動 flush。SDK 在 init 時啟動計時器，每隔 N 毫秒檢查 buffer 是否有待發事件，有則送出。&lt;/p>
&lt;p>合理的間隔範圍：10-60 秒。間隔太短（1 秒）接近逐筆送出，失去攢批的效益；間隔太長（5 分鐘）可能讓事件延遲到達 collector，影響即時監控和告警的反應速度。&lt;/p>
&lt;p>自用工具場景下 30 秒是合理的預設 — 事件量低，30 秒的延遲對 debug 分析沒有實質影響。商業產品可以降到 10 秒以獲得更接近即時的 error 告警。&lt;/p>
&lt;h3 id="數量觸發buffer-size">數量觸發（buffer size）&lt;/h3>
&lt;p>Buffer 內的事件數量達到上限時立即 flush。Buffer size 設定為一次 HTTP POST 的合理 payload 大小對應的事件數量。&lt;/p>
&lt;p>合理的數量範圍：50-200 筆。數量太少（10 筆）頻繁觸發 flush；數量太多（1000 筆）單次 HTTP POST 的 payload 過大，增加傳輸失敗的風險（超時、記憶體）。&lt;/p>
&lt;p>數量觸發和時間觸發互為備援。高頻事件場景（使用者快速操作）靠數量觸發避免 buffer 溢出；低頻事件場景（使用者長時間閒置）靠時間觸發確保事件在合理時間內送出。&lt;/p>
&lt;h3 id="關閉觸發flush-on-close">關閉觸發（flush on close）&lt;/h3>
&lt;p>SDK close 時強制 flush buffer 中所有剩餘事件。這是最後一道保障 — app 關閉後 buffer 中未送出的事件就永久遺失了。&lt;/p>
&lt;p>close flush 的挑戰是時間限制。iOS app 進入背景後約 5 秒會被系統 suspend，Android 的限制更嚴格。Close flush 必須在這個時間窗口內完成網路請求。如果 buffer 中事件太多導致 flush 超時，需要截斷 — 送出最近的 N 筆，放棄較舊的。&lt;/p>
&lt;h2 id="buffer-管理">Buffer 管理&lt;/h2>
&lt;h3 id="記憶體-buffer">記憶體 buffer&lt;/h3>
&lt;p>Buffer 在記憶體中維護一個事件陣列。新事件 append 到尾端，flush 時取出整個陣列送出並清空。&lt;/p>
&lt;p>記憶體 buffer 的上限應該設定為 buffer size 的 2-3 倍（允許 1-2 次 flush 失敗後累積的事件）。超過上限時丟棄最舊的事件（FIFO），保留最新的 — 最新的事件對 debug 和即時分析的價值更高。&lt;/p>
&lt;h3 id="離線-buffer">離線 buffer&lt;/h3>
&lt;p>網路不可用時，事件累積在記憶體 buffer 中。如果離線時間超過記憶體 buffer 容量，需要離線 persistence — 見 &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;/p>
&lt;h2 id="flush-失敗處理">Flush 失敗處理&lt;/h2>
&lt;p>HTTP POST 失敗時（網路中斷、server 回 5xx、超時），事件保留在 buffer 中等待下一次 flush 重試。不立即重試 — 連續失敗通常代表網路問題或 server 問題，立即重試只會增加負載。&lt;/p>
&lt;p>重試次數有上限（3 次）。超過重試上限的事件被丟棄，記錄一筆 &lt;code>sdk.flush.dropped&lt;/code> metric 事件（這筆 metric 本身也進 buffer，在下次成功 flush 時送出）。&lt;/p>
&lt;h3 id="sdk-對-collector-回應的處理">SDK 對 collector 回應的處理&lt;/h3>
&lt;p>SDK 只需要判斷 HTTP status code 就知道怎麼處理 buffer，不需要解析 response body 的細節。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Status&lt;/th>
 &lt;th>SDK 行為&lt;/th>
 &lt;th>理由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>200&lt;/td>
 &lt;td>清除已送出的 buffer&lt;/td>
 &lt;td>全部成功&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>207&lt;/td>
 &lt;td>清除 buffer + 記錄 warning log&lt;/td>
 &lt;td>合法事件已被接受；失敗事件是 schema 問題，重試也不會過&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>400&lt;/td>
 &lt;td>清除 buffer + 記錄 error log&lt;/td>
 &lt;td>Schema 問題重試也不會過，保留在 buffer 只會擋住後續事件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>503&lt;/td>
 &lt;td>保留 buffer + 等待 &lt;code>retry_after&lt;/code> 秒&lt;/td>
 &lt;td>collector 暫時不可用，事件本身沒問題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>其他（network error / timeout）&lt;/td>
 &lt;td>保留 buffer + 下次 flush 重試&lt;/td>
 &lt;td>暫時性問題，重試有機會成功&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>207 和 400 都清 buffer 的關鍵判斷：Schema 驗證失敗是 SDK 端產出了不合規的事件，問題在 SDK 的事件建構邏輯（程式碼 bug），不在 collector 或網路 — 重試相同事件永遠不會過。SDK 把失敗事件的 error 訊息記到 warning/error log 供開發者排查，然後放行後續事件。&lt;/p></description><content:encoded><![CDATA[<p>攢批送出策略控制事件從 SDK 內部 buffer 送到 collector 的時機。事件產生後先進入記憶體 buffer，累積到一定數量或間隔一定時間後，一次性透過 HTTP POST 送出整批事件。攢批的目的是減少網路請求次數 — 100 筆事件合併成一個 HTTP 請求，比 100 個獨立請求的網路開銷低。</p>
<h2 id="三個觸發條件">三個觸發條件</h2>
<h3 id="時間觸發flush-interval">時間觸發（flush interval）</h3>
<p>固定間隔自動 flush。SDK 在 init 時啟動計時器，每隔 N 毫秒檢查 buffer 是否有待發事件，有則送出。</p>
<p>合理的間隔範圍：10-60 秒。間隔太短（1 秒）接近逐筆送出，失去攢批的效益；間隔太長（5 分鐘）可能讓事件延遲到達 collector，影響即時監控和告警的反應速度。</p>
<p>自用工具場景下 30 秒是合理的預設 — 事件量低，30 秒的延遲對 debug 分析沒有實質影響。商業產品可以降到 10 秒以獲得更接近即時的 error 告警。</p>
<h3 id="數量觸發buffer-size">數量觸發（buffer size）</h3>
<p>Buffer 內的事件數量達到上限時立即 flush。Buffer size 設定為一次 HTTP POST 的合理 payload 大小對應的事件數量。</p>
<p>合理的數量範圍：50-200 筆。數量太少（10 筆）頻繁觸發 flush；數量太多（1000 筆）單次 HTTP POST 的 payload 過大，增加傳輸失敗的風險（超時、記憶體）。</p>
<p>數量觸發和時間觸發互為備援。高頻事件場景（使用者快速操作）靠數量觸發避免 buffer 溢出；低頻事件場景（使用者長時間閒置）靠時間觸發確保事件在合理時間內送出。</p>
<h3 id="關閉觸發flush-on-close">關閉觸發（flush on close）</h3>
<p>SDK close 時強制 flush buffer 中所有剩餘事件。這是最後一道保障 — app 關閉後 buffer 中未送出的事件就永久遺失了。</p>
<p>close flush 的挑戰是時間限制。iOS app 進入背景後約 5 秒會被系統 suspend，Android 的限制更嚴格。Close flush 必須在這個時間窗口內完成網路請求。如果 buffer 中事件太多導致 flush 超時，需要截斷 — 送出最近的 N 筆，放棄較舊的。</p>
<h2 id="buffer-管理">Buffer 管理</h2>
<h3 id="記憶體-buffer">記憶體 buffer</h3>
<p>Buffer 在記憶體中維護一個事件陣列。新事件 append 到尾端，flush 時取出整個陣列送出並清空。</p>
<p>記憶體 buffer 的上限應該設定為 buffer size 的 2-3 倍（允許 1-2 次 flush 失敗後累積的事件）。超過上限時丟棄最舊的事件（FIFO），保留最新的 — 最新的事件對 debug 和即時分析的價值更高。</p>
<h3 id="離線-buffer">離線 buffer</h3>
<p>網路不可用時，事件累積在記憶體 buffer 中。如果離線時間超過記憶體 buffer 容量，需要離線 persistence — 見 <a href="/blog/monitoring/03-sdk-design/offline-buffer/" data-link-title="離線 buffer 與重試" data-link-desc="網路不可用時的事件保存策略 — FIFO 丟棄、本地 persistence、恢復後補發的取捨">離線 buffer 與重試</a>。</p>
<h2 id="flush-失敗處理">Flush 失敗處理</h2>
<p>HTTP POST 失敗時（網路中斷、server 回 5xx、超時），事件保留在 buffer 中等待下一次 flush 重試。不立即重試 — 連續失敗通常代表網路問題或 server 問題，立即重試只會增加負載。</p>
<p>重試次數有上限（3 次）。超過重試上限的事件被丟棄，記錄一筆 <code>sdk.flush.dropped</code> metric 事件（這筆 metric 本身也進 buffer，在下次成功 flush 時送出）。</p>
<h3 id="sdk-對-collector-回應的處理">SDK 對 collector 回應的處理</h3>
<p>SDK 只需要判斷 HTTP status code 就知道怎麼處理 buffer，不需要解析 response body 的細節。</p>
<table>
  <thead>
      <tr>
          <th>Status</th>
          <th>SDK 行為</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>200</td>
          <td>清除已送出的 buffer</td>
          <td>全部成功</td>
      </tr>
      <tr>
          <td>207</td>
          <td>清除 buffer + 記錄 warning log</td>
          <td>合法事件已被接受；失敗事件是 schema 問題，重試也不會過</td>
      </tr>
      <tr>
          <td>400</td>
          <td>清除 buffer + 記錄 error log</td>
          <td>Schema 問題重試也不會過，保留在 buffer 只會擋住後續事件</td>
      </tr>
      <tr>
          <td>503</td>
          <td>保留 buffer + 等待 <code>retry_after</code> 秒</td>
          <td>collector 暫時不可用，事件本身沒問題</td>
      </tr>
      <tr>
          <td>其他（network error / timeout）</td>
          <td>保留 buffer + 下次 flush 重試</td>
          <td>暫時性問題，重試有機會成功</td>
      </tr>
  </tbody>
</table>
<p>207 和 400 都清 buffer 的關鍵判斷：Schema 驗證失敗是 SDK 端產出了不合規的事件，問題在 SDK 的事件建構邏輯（程式碼 bug），不在 collector 或網路 — 重試相同事件永遠不會過。SDK 把失敗事件的 error 訊息記到 warning/error log 供開發者排查，然後放行後續事件。</p>
<p>503 保留 buffer 的關鍵判斷：collector 暫時不可用是基礎設施問題（SQLite busy timeout、背壓），事件本身合法，等 collector 恢復後重試會成功。<code>retry_after</code> 由 collector 在回應中指定，SDK 用這個值設定下次 flush 的最小等待時間。</p>
<h2 id="batch-格式">Batch 格式</h2>
<p>SDK 在 flush 時把 buffer 中所有事件包裝成一個 batch，帶上 <code>batch_id</code> 送出。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;batch_id&#34;</span><span class="p">:</span> <span class="s2">&#34;019537a0-7b2c-7def-8a2b-3c4d5e6f7890&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;events&#34;</span><span class="p">:</span> <span class="p">[</span> <span class="err">...</span> <span class="p">]</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>batch_id</code> 由 SDK 在 flush 時產生。使用 UUID v7（<code>uuid.uuid7()</code>，Python 3.14+ 標準庫）——時間戳前綴保證有序（debug 時按 batch_id 排序即時間順序），隨機後綴保證唯一（高負載下多個 SDK 同時 flush 不碰撞）。用途是追蹤和 debug（collector log 中標記同一批事件的來源）。Collector 不依賴 batch_id 做去重 — 同一批事件被 SDK 重試時會帶不同的 batch_id（每次 flush 重新產生），collector 按事件內容（timestamp + source + name）判斷是否重複。</p>
<p>UUID v7 而非時間戳格式的選型理由：時間戳格式（<code>b-{YYYYMMDD}-{HHMMSSfff}</code>）在同毫秒多次 flush 時會碰撞，雖然 MVP 的 debug 用途碰撞無害，但 batch_id 碰撞在後續版本的離線補發去重場景（見 <a href="/blog/monitoring/03-sdk-design/offline-buffer/" data-link-title="離線 buffer 與重試" data-link-desc="網路不可用時的事件保存策略 — FIFO 丟棄、本地 persistence、恢復後補發的取捨">離線 buffer 與重試</a>）會造成歧義。UUID v7 兼顧有序和唯一，一次到位。</p>
<h2 id="heartbeat-和-flush-的整合">Heartbeat 和 flush 的整合</h2>
<p>DevOps dashboard 需要 <code>sdk.heartbeat</code> 事件判斷 SDK 是否存活。Heartbeat 不需要獨立的 timer — 整合在 flush timer 中：</p>
<p>flush timer 觸發時，如果 buffer 為空且距上次 heartbeat 超過設定間隔（預設 5 分鐘），自動注入一筆 <code>sdk.heartbeat</code> lifecycle 事件後送出。App idle 時仍有心跳但不多一個 timer；app 活躍時 heartbeat 被正常事件的 flush 取代（buffer 不會為空）。</p>
<p>Heartbeat 間隔由 SDK init config 的 <code>heartbeatInterval</code> 設定。設為 0 停用 heartbeat。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>離線場景的處理 → <a href="/blog/monitoring/03-sdk-design/offline-buffer/" data-link-title="離線 buffer 與重試" data-link-desc="網路不可用時的事件保存策略 — FIFO 丟棄、本地 persistence、恢復後補發的取捨">離線 buffer 與重試</a></li>
<li>SDK 公開 API → <a href="/blog/monitoring/03-sdk-design/public-api/" data-link-title="SDK 公開 API 設計" data-link-desc="init / event / error / metric / flush / close 六個方法構成 SDK 的完整生命週期 — 跨平台共用相同 API 介面">SDK 公開 API 設計</a></li>
<li>Collector 端如何接收批次事件 → <a href="/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">模組四 Collector 架構</a></li>
</ul>
]]></content:encoded></item></channel></rss>