<?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>Lifecycle on Tarragon</title><link>https://tarrragon.github.io/blog/tags/lifecycle/</link><description>Recent content in Lifecycle on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 23 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/lifecycle/index.xml" rel="self" type="application/rss+xml"/><item><title>SDK 公開 API 設計</title><link>https://tarrragon.github.io/blog/monitoring/03-sdk-design/public-api/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/03-sdk-design/public-api/</guid><description>&lt;p>SDK 的公開 API 是應用程式和監控系統之間的契約。六個方法涵蓋 SDK 的完整生命週期：初始化、四類事件上報、資料送出控制和資源釋放。跨平台的 SDK（JS / Flutter / Python）共用相同的方法簽名，讓開發者在不同平台上使用一致的 API。&lt;/p>
&lt;h2 id="六個方法">六個方法&lt;/h2>
&lt;h3 id="init">init&lt;/h3>
&lt;p>SDK 初始化。設定 collector endpoint、app 識別資訊、flush 間隔、buffer 大小。在 app 啟動時呼叫一次。&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">Monitor.init({
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> endpoint: &amp;#39;https://collector.example.com/v1/events&amp;#39;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> app: &amp;#39;my_app&amp;#39;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> version: &amp;#39;1.2.0&amp;#39;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> flushInterval: 30000, // 毫秒
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> bufferSize: 100,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">})&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>init 負責建立 session、記錄 lifecycle.session.start 事件、啟動 flush 計時器。init 之前呼叫其他方法應該拋出明確錯誤（SDK 未初始化），而非靜默忽略。&lt;/p>
&lt;p>&lt;strong>連線驗證策略：lazy&lt;/strong>。init 不驗證 collector 是否可達 — 不發 HTTP 請求、不 ping endpoint。init 的失敗只代表配置錯誤（缺少 endpoint 參數），不代表網路問題。網路問題在第一次 flush 時才浮現，flush 失敗時事件保留在 buffer 等待重試。&lt;/p>
&lt;p>Lazy 策略的理由：SDK 不應阻塞主程式的啟動流程。如果 init 驗證連線，collector 暫時不可用時 app 會啟動失敗 — 監控工具反而變成可用性的瓶頸。短生命週期腳本（&lt;a href="https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/python-platform/" data-link-title="Python 平台適配" data-link-desc="GIL 與 threading、atexit 可靠性、subprocess 監控 — Python SDK 的平台特殊考量">Python 平台適配：短生命週期腳本&lt;/a>）對這一點更敏感 — hook 腳本不能因為 collector 沒啟動就拒絕執行。&lt;/p>
&lt;h3 id="event">event&lt;/h3>
&lt;p>記錄使用者操作事件（&lt;a href="https://tarrragon.github.io/blog/monitoring/01-mental-model/four-event-types/" data-link-title="四類事件的完整定義" data-link-desc="Event / Error / Metric / Lifecycle 四類事件各自的語意、觸發時機和典型用途 — 分類是監控體系的統一語言">四類事件中的 Event 類&lt;/a>）。接受事件名稱和可選的 data 物件。&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">Monitor.event(&amp;#39;terminal.connect.start&amp;#39;, { url: &amp;#39;wss://...&amp;#39; })
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">Monitor.event(&amp;#39;enrollment.qr.scan&amp;#39;)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>event 方法是非阻塞的 — 事件進入內部 buffer 立即返回，不等待網路送出。應用程式的操作流程不應該被監控 SDK 的網路延遲阻塞。&lt;/p>
&lt;h3 id="error">error&lt;/h3>
&lt;p>記錄錯誤事件。接受 Error/Exception 物件或自訂的錯誤描述。自動附加 stack trace、錯誤類型、觸發位置。&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">Monitor.error(exception, { step: &amp;#39;ws_connect&amp;#39; })
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">Monitor.error(&amp;#39;Auth token missing&amp;#39;, { context: &amp;#39;handshake&amp;#39; })&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>error 方法和自動攔截機制（&lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/auto-intercept/" data-link-title="自動攔截機制" data-link-desc="JS window.onerror / Flutter FlutterError.onError / Python sys.excepthook — 各平台攔截未捕獲例外的機制和限制">自動攔截&lt;/a>）互補 — 自動攔截處理未捕獲的例外，error 方法處理開發者主動上報的已知錯誤。&lt;/p>
&lt;h3 id="metric">metric&lt;/h3>
&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">Monitor.metric(&amp;#39;connect.duration_ms&amp;#39;, 320)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">Monitor.metric(&amp;#39;terminal.fps&amp;#39;, 58.5)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>metric 方法記錄的是離散的數值快照。聚合計算（平均、百分位、趨勢）在 collector 端完成，SDK 端只負責記錄原始值。&lt;/p>
&lt;h3 id="flush">flush&lt;/h3>
&lt;p>強制送出 buffer 中所有待發事件。正常情況下 SDK 按 flushInterval 定期自動 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>）。flush 方法用於需要確保事件已送出的場景 — 例如 app 即將進入背景或使用者手動觸發 log 上傳。&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">await Monitor.flush()&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>flush 是非同步方法 — 需要等待網路請求完成。呼叫端可以 await 確認送出成功，也可以 fire-and-forget。&lt;/p></description><content:encoded><![CDATA[<p>SDK 的公開 API 是應用程式和監控系統之間的契約。六個方法涵蓋 SDK 的完整生命週期：初始化、四類事件上報、資料送出控制和資源釋放。跨平台的 SDK（JS / Flutter / Python）共用相同的方法簽名，讓開發者在不同平台上使用一致的 API。</p>
<h2 id="六個方法">六個方法</h2>
<h3 id="init">init</h3>
<p>SDK 初始化。設定 collector endpoint、app 識別資訊、flush 間隔、buffer 大小。在 app 啟動時呼叫一次。</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">Monitor.init({
</span></span><span class="line"><span class="ln">2</span><span class="cl">  endpoint: &#39;https://collector.example.com/v1/events&#39;,
</span></span><span class="line"><span class="ln">3</span><span class="cl">  app: &#39;my_app&#39;,
</span></span><span class="line"><span class="ln">4</span><span class="cl">  version: &#39;1.2.0&#39;,
</span></span><span class="line"><span class="ln">5</span><span class="cl">  flushInterval: 30000,   // 毫秒
</span></span><span class="line"><span class="ln">6</span><span class="cl">  bufferSize: 100,
</span></span><span class="line"><span class="ln">7</span><span class="cl">})</span></span></code></pre></div><p>init 負責建立 session、記錄 lifecycle.session.start 事件、啟動 flush 計時器。init 之前呼叫其他方法應該拋出明確錯誤（SDK 未初始化），而非靜默忽略。</p>
<p><strong>連線驗證策略：lazy</strong>。init 不驗證 collector 是否可達 — 不發 HTTP 請求、不 ping endpoint。init 的失敗只代表配置錯誤（缺少 endpoint 參數），不代表網路問題。網路問題在第一次 flush 時才浮現，flush 失敗時事件保留在 buffer 等待重試。</p>
<p>Lazy 策略的理由：SDK 不應阻塞主程式的啟動流程。如果 init 驗證連線，collector 暫時不可用時 app 會啟動失敗 — 監控工具反而變成可用性的瓶頸。短生命週期腳本（<a href="/blog/monitoring/05-platform-adaptation/python-platform/" data-link-title="Python 平台適配" data-link-desc="GIL 與 threading、atexit 可靠性、subprocess 監控 — Python SDK 的平台特殊考量">Python 平台適配：短生命週期腳本</a>）對這一點更敏感 — hook 腳本不能因為 collector 沒啟動就拒絕執行。</p>
<h3 id="event">event</h3>
<p>記錄使用者操作事件（<a href="/blog/monitoring/01-mental-model/four-event-types/" data-link-title="四類事件的完整定義" data-link-desc="Event / Error / Metric / Lifecycle 四類事件各自的語意、觸發時機和典型用途 — 分類是監控體系的統一語言">四類事件中的 Event 類</a>）。接受事件名稱和可選的 data 物件。</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">Monitor.event(&#39;terminal.connect.start&#39;, { url: &#39;wss://...&#39; })
</span></span><span class="line"><span class="ln">2</span><span class="cl">Monitor.event(&#39;enrollment.qr.scan&#39;)</span></span></code></pre></div><p>event 方法是非阻塞的 — 事件進入內部 buffer 立即返回，不等待網路送出。應用程式的操作流程不應該被監控 SDK 的網路延遲阻塞。</p>
<h3 id="error">error</h3>
<p>記錄錯誤事件。接受 Error/Exception 物件或自訂的錯誤描述。自動附加 stack trace、錯誤類型、觸發位置。</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">Monitor.error(exception, { step: &#39;ws_connect&#39; })
</span></span><span class="line"><span class="ln">2</span><span class="cl">Monitor.error(&#39;Auth token missing&#39;, { context: &#39;handshake&#39; })</span></span></code></pre></div><p>error 方法和自動攔截機制（<a href="/blog/monitoring/03-sdk-design/auto-intercept/" data-link-title="自動攔截機制" data-link-desc="JS window.onerror / Flutter FlutterError.onError / Python sys.excepthook — 各平台攔截未捕獲例外的機制和限制">自動攔截</a>）互補 — 自動攔截處理未捕獲的例外，error 方法處理開發者主動上報的已知錯誤。</p>
<h3 id="metric">metric</h3>
<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">Monitor.metric(&#39;connect.duration_ms&#39;, 320)
</span></span><span class="line"><span class="ln">2</span><span class="cl">Monitor.metric(&#39;terminal.fps&#39;, 58.5)</span></span></code></pre></div><p>metric 方法記錄的是離散的數值快照。聚合計算（平均、百分位、趨勢）在 collector 端完成，SDK 端只負責記錄原始值。</p>
<h3 id="flush">flush</h3>
<p>強制送出 buffer 中所有待發事件。正常情況下 SDK 按 flushInterval 定期自動 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>）。flush 方法用於需要確保事件已送出的場景 — 例如 app 即將進入背景或使用者手動觸發 log 上傳。</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">await Monitor.flush()</span></span></code></pre></div><p>flush 是非同步方法 — 需要等待網路請求完成。呼叫端可以 await 確認送出成功，也可以 fire-and-forget。</p>
<h3 id="close">close</h3>
<p>SDK 資源釋放。停止 flush 計時器、送出 buffer 中剩餘事件、關閉網路連線、記錄 lifecycle.session.end 事件。</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">await Monitor.close()</span></span></code></pre></div><p>close 在 app 關閉時呼叫。呼叫後 SDK 進入已關閉狀態，後續的 event/error/metric 呼叫應該被靜默忽略（不拋錯，因為 app 正在關閉）。</p>
<h2 id="api-設計原則">API 設計原則</h2>
<p><strong>方法名稱和四類事件對齊</strong>。event / error / metric 三個方法直接對應三類事件，lifecycle 事件由 init 和 close 自動產生。開發者看到方法名稱就知道對應哪類事件。</p>
<p><strong>所有上報方法非阻塞</strong>。event、error、metric 進 buffer 立即返回。監控 SDK 阻塞應用程式的操作流程是反模式。</p>
<p><strong>init 和 close 成對出現</strong>。init 開始 session，close 結束 session。兩者界定 SDK 的活躍期間。</p>
<p>各平台的 SDK 整合範例（Flutter 的 pubspec.yaml + main.dart init、Python 的 pip install + init code、JS 的 script tag + init）見 monitor repo 各 SDK 的 README。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>自動攔截未捕獲的錯誤 → <a href="/blog/monitoring/03-sdk-design/auto-intercept/" data-link-title="自動攔截機制" data-link-desc="JS window.onerror / Flutter FlutterError.onError / Python sys.excepthook — 各平台攔截未捕獲例外的機制和限制">自動攔截機制</a></li>
<li>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></li>
<li>SDK 端的資料脫敏 → <a href="/blog/monitoring/03-sdk-design/redaction-helper/" data-link-title="SDK redaction helper" data-link-desc="在事件離開 SDK 前移除敏感資訊 — 預設 redaction rule 處理常見 pattern，自訂 rule 處理業務特定的 secret">SDK redaction helper</a></li>
<li>SDK 的 HTTP POST 行為需要 protocol test → <a href="/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">testing 模組三 協議整合測試</a></li>
</ul>
]]></content:encoded></item><item><title>四類事件的完整定義</title><link>https://tarrragon.github.io/blog/monitoring/01-mental-model/four-event-types/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/01-mental-model/four-event-types/</guid><description>&lt;p>監控資料由四類事件構成。每類事件回答不同的問題，觸發時機不同，消費方式不同。分類的目的是讓「我要收集什麼」有結構化的答案，而非在每個功能上各自決定要不要加 log。&lt;/p>
&lt;h2 id="event使用者做了什麼">Event：使用者做了什麼&lt;/h2>
&lt;p>Event 記錄使用者主動發起的操作。按鈕點擊、頁面瀏覽、表單提交、搜尋查詢 — 每個 event 代表使用者的一個意圖表達。&lt;/p>
&lt;p>Event 的觸發時機是使用者操作發生時。程式碼中的位置通常是 UI 事件處理器（onClick、onSubmit、onNavigate）。&lt;/p>
&lt;p>Event 的消費方式：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Debug context&lt;/strong>：問題發生前使用者做了哪些操作。和 error 事件搭配使用，還原問題的操作路徑。&lt;/li>
&lt;li>&lt;strong>行為分析&lt;/strong>：使用者做了哪些操作、操作順序是什麼、在哪一步停止。&lt;a href="https://tarrragon.github.io/blog/monitoring/knowledge-cards/funnel-analysis/" data-link-title="Funnel Analysis" data-link-desc="說明追蹤使用者在多步驟流程中每一步的轉換率和流失率的分析方法">Funnel analysis&lt;/a> 的原料（&lt;a href="https://tarrragon.github.io/blog/monitoring/08-business-analytics/" data-link-title="模組八：行為資料的商業利用" data-link-desc="Funnel / Cohort / Attribution / A/B test / 推薦系統 / RFM — 從 debug 工具到商業資產的翻轉">模組八&lt;/a>）。&lt;/li>
&lt;li>&lt;strong>功能使用率&lt;/strong>：哪些功能被頻繁使用、哪些很少被觸發。功能優先順序的決策依據。&lt;/li>
&lt;/ul>
&lt;h2 id="error什麼出了問題">Error：什麼出了問題&lt;/h2>
&lt;p>Error 記錄程式碼執行中的非預期狀態。例外拋出、assertion 失敗、非預期的 API 回應、資源存取失敗。&lt;/p>
&lt;p>Error 的觸發時機是非預期狀態被偵測到時。來源包括：語言層級的 try/catch 捕獲、框架的全域錯誤處理器（Flutter 的 &lt;code>FlutterError.onError&lt;/code>、JavaScript 的 &lt;code>window.onerror&lt;/code>）、自訂的錯誤檢查邏輯。&lt;/p>
&lt;p>Error 的消費方式：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>即時告警&lt;/strong>：特定類型的 error 或 error 數量超過閾值時通知開發者。&lt;/li>
&lt;li>&lt;strong>趨勢分析&lt;/strong>：error 數量隨時間的變化。新版本部署後 error 是否增加。&lt;/li>
&lt;li>&lt;strong>根因分析&lt;/strong>：error 的 stack trace、觸發條件、影響範圍。和 event 搭配還原「使用者做了什麼導致 error」。&lt;/li>
&lt;/ul>
&lt;h2 id="metric系統狀態的數值快照">Metric：系統狀態的數值快照&lt;/h2>
&lt;p>Metric 記錄系統狀態的可量化指標。回應時間、記憶體使用量、佇列長度、連線數、frame rate。&lt;/p>
&lt;p>Metric 的觸發時機是定期取樣或特定事件發生時。定期取樣適合持續變化的指標（記憶體使用量每 30 秒取一次），事件觸發適合離散的測量（每次 API 回應記錄回應時間）。&lt;/p>
&lt;p>Metric 的消費方式：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>效能監控&lt;/strong>：回應時間的 P50 / P95 / P99 分佈。記憶體使用量的趨勢。&lt;/li>
&lt;li>&lt;strong>容量規劃&lt;/strong>：佇列長度接近上限、連線數接近 pool 上限 — 需要擴容的訊號。&lt;/li>
&lt;li>&lt;strong>SLA 追蹤&lt;/strong>：服務可用性、回應時間是否在承諾範圍內。&lt;/li>
&lt;/ul>
&lt;h2 id="lifecycle系統經歷了什麼階段">Lifecycle：系統經歷了什麼階段&lt;/h2>
&lt;p>Lifecycle 記錄系統本身的狀態轉換。App 啟動、前景/背景切換、連線建立/斷開、版本更新、設定變更。&lt;/p>
&lt;p>Lifecycle 的觸發時機是系統狀態轉換發生時。來源包括：app 生命週期回呼（onCreate、onResume、onPause）、連線狀態變化事件、部署和設定變更鉤子。&lt;/p>
&lt;p>Lifecycle 的消費方式：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Session 分析&lt;/strong>：使用者一次使用多久、啟動頻率、前後景切換頻率。&lt;/li>
&lt;li>&lt;strong>環境資訊&lt;/strong>：Error 發生時的系統狀態（app 版本、OS 版本、網路狀態）。&lt;/li>
&lt;li>&lt;strong>連線品質&lt;/strong>：連線建立成功率、斷線頻率、重連次數（&lt;a href="https://tarrragon.github.io/blog/testing/02-client-observability/three-layer-log-design/" data-link-title="三層 log 設計" data-link-desc="連線生命週期 log、protocol 訊息 log、使用者行為 log — 三層各自的職責、詳細程度和啟停控制">testing 模組二 三層 log&lt;/a>）。&lt;/li>
&lt;/ul>
&lt;h2 id="四類事件的區別">四類事件的區別&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Event&lt;/th>
 &lt;th>Error&lt;/th>
 &lt;th>Metric&lt;/th>
 &lt;th>Lifecycle&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>觸發者&lt;/td>
 &lt;td>使用者操作&lt;/td>
 &lt;td>系統非預期狀態&lt;/td>
 &lt;td>定期取樣或事件觸發&lt;/td>
 &lt;td>系統狀態轉換&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回答&lt;/td>
 &lt;td>使用者做了什麼&lt;/td>
 &lt;td>什麼出了問題&lt;/td>
 &lt;td>系統現在怎麼樣&lt;/td>
 &lt;td>系統經歷了什麼&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>頻率&lt;/td>
 &lt;td>依使用者行為&lt;/td>
 &lt;td>低（理想狀態）&lt;/td>
 &lt;td>固定間隔或事件驅動&lt;/td>
 &lt;td>低（狀態轉換才有）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>消費&lt;/td>
 &lt;td>行為分析、funnel&lt;/td>
 &lt;td>告警、根因分析&lt;/td>
 &lt;td>效能監控、容量規劃&lt;/td>
 &lt;td>session、環境資訊&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>事件命名規範 → &lt;a href="https://tarrragon.github.io/blog/monitoring/01-mental-model/event-naming-convention/" data-link-title="事件命名規範" data-link-desc="namespace.action 格式的事件命名、命名一致性的工程價值、和商業方案命名慣例的對應">事件命名規範&lt;/a>&lt;/li>
&lt;li>從需求推導收集策略 → &lt;a href="https://tarrragon.github.io/blog/monitoring/01-mental-model/derive-collection-from-requirements/" data-link-title="從需求推導「該收集哪些事件」" data-link-desc="從 debug 需求、行為分析需求、效能需求、合規需求四個方向推導事件收集策略 — 避免「什麼都收」和「什麼都不收」">從需求推導「該收集哪些事件」&lt;/a>&lt;/li>
&lt;li>Event 類事件在商業分析中的用途 → &lt;a href="https://tarrragon.github.io/blog/monitoring/08-business-analytics/" data-link-title="模組八：行為資料的商業利用" data-link-desc="Funnel / Cohort / Attribution / A/B test / 推薦系統 / RFM — 從 debug 工具到商業資產的翻轉">模組八 行為資料的商業利用&lt;/a>&lt;/li>
&lt;li>Log 點的設計方法 → &lt;a href="https://tarrragon.github.io/blog/testing/02-client-observability/" data-link-title="模組二：客戶端可觀測性" data-link-desc="連線生命週期 log、protocol 訊息 log、使用者行為 log — log 設計是功能規格的一部分">testing 模組二 客戶端可觀測性&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>監控資料由四類事件構成。每類事件回答不同的問題，觸發時機不同，消費方式不同。分類的目的是讓「我要收集什麼」有結構化的答案，而非在每個功能上各自決定要不要加 log。</p>
<h2 id="event使用者做了什麼">Event：使用者做了什麼</h2>
<p>Event 記錄使用者主動發起的操作。按鈕點擊、頁面瀏覽、表單提交、搜尋查詢 — 每個 event 代表使用者的一個意圖表達。</p>
<p>Event 的觸發時機是使用者操作發生時。程式碼中的位置通常是 UI 事件處理器（onClick、onSubmit、onNavigate）。</p>
<p>Event 的消費方式：</p>
<ul>
<li><strong>Debug context</strong>：問題發生前使用者做了哪些操作。和 error 事件搭配使用，還原問題的操作路徑。</li>
<li><strong>行為分析</strong>：使用者做了哪些操作、操作順序是什麼、在哪一步停止。<a href="/blog/monitoring/knowledge-cards/funnel-analysis/" data-link-title="Funnel Analysis" data-link-desc="說明追蹤使用者在多步驟流程中每一步的轉換率和流失率的分析方法">Funnel analysis</a> 的原料（<a href="/blog/monitoring/08-business-analytics/" data-link-title="模組八：行為資料的商業利用" data-link-desc="Funnel / Cohort / Attribution / A/B test / 推薦系統 / RFM — 從 debug 工具到商業資產的翻轉">模組八</a>）。</li>
<li><strong>功能使用率</strong>：哪些功能被頻繁使用、哪些很少被觸發。功能優先順序的決策依據。</li>
</ul>
<h2 id="error什麼出了問題">Error：什麼出了問題</h2>
<p>Error 記錄程式碼執行中的非預期狀態。例外拋出、assertion 失敗、非預期的 API 回應、資源存取失敗。</p>
<p>Error 的觸發時機是非預期狀態被偵測到時。來源包括：語言層級的 try/catch 捕獲、框架的全域錯誤處理器（Flutter 的 <code>FlutterError.onError</code>、JavaScript 的 <code>window.onerror</code>）、自訂的錯誤檢查邏輯。</p>
<p>Error 的消費方式：</p>
<ul>
<li><strong>即時告警</strong>：特定類型的 error 或 error 數量超過閾值時通知開發者。</li>
<li><strong>趨勢分析</strong>：error 數量隨時間的變化。新版本部署後 error 是否增加。</li>
<li><strong>根因分析</strong>：error 的 stack trace、觸發條件、影響範圍。和 event 搭配還原「使用者做了什麼導致 error」。</li>
</ul>
<h2 id="metric系統狀態的數值快照">Metric：系統狀態的數值快照</h2>
<p>Metric 記錄系統狀態的可量化指標。回應時間、記憶體使用量、佇列長度、連線數、frame rate。</p>
<p>Metric 的觸發時機是定期取樣或特定事件發生時。定期取樣適合持續變化的指標（記憶體使用量每 30 秒取一次），事件觸發適合離散的測量（每次 API 回應記錄回應時間）。</p>
<p>Metric 的消費方式：</p>
<ul>
<li><strong>效能監控</strong>：回應時間的 P50 / P95 / P99 分佈。記憶體使用量的趨勢。</li>
<li><strong>容量規劃</strong>：佇列長度接近上限、連線數接近 pool 上限 — 需要擴容的訊號。</li>
<li><strong>SLA 追蹤</strong>：服務可用性、回應時間是否在承諾範圍內。</li>
</ul>
<h2 id="lifecycle系統經歷了什麼階段">Lifecycle：系統經歷了什麼階段</h2>
<p>Lifecycle 記錄系統本身的狀態轉換。App 啟動、前景/背景切換、連線建立/斷開、版本更新、設定變更。</p>
<p>Lifecycle 的觸發時機是系統狀態轉換發生時。來源包括：app 生命週期回呼（onCreate、onResume、onPause）、連線狀態變化事件、部署和設定變更鉤子。</p>
<p>Lifecycle 的消費方式：</p>
<ul>
<li><strong>Session 分析</strong>：使用者一次使用多久、啟動頻率、前後景切換頻率。</li>
<li><strong>環境資訊</strong>：Error 發生時的系統狀態（app 版本、OS 版本、網路狀態）。</li>
<li><strong>連線品質</strong>：連線建立成功率、斷線頻率、重連次數（<a href="/blog/testing/02-client-observability/three-layer-log-design/" data-link-title="三層 log 設計" data-link-desc="連線生命週期 log、protocol 訊息 log、使用者行為 log — 三層各自的職責、詳細程度和啟停控制">testing 模組二 三層 log</a>）。</li>
</ul>
<h2 id="四類事件的區別">四類事件的區別</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Event</th>
          <th>Error</th>
          <th>Metric</th>
          <th>Lifecycle</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>觸發者</td>
          <td>使用者操作</td>
          <td>系統非預期狀態</td>
          <td>定期取樣或事件觸發</td>
          <td>系統狀態轉換</td>
      </tr>
      <tr>
          <td>回答</td>
          <td>使用者做了什麼</td>
          <td>什麼出了問題</td>
          <td>系統現在怎麼樣</td>
          <td>系統經歷了什麼</td>
      </tr>
      <tr>
          <td>頻率</td>
          <td>依使用者行為</td>
          <td>低（理想狀態）</td>
          <td>固定間隔或事件驅動</td>
          <td>低（狀態轉換才有）</td>
      </tr>
      <tr>
          <td>消費</td>
          <td>行為分析、funnel</td>
          <td>告警、根因分析</td>
          <td>效能監控、容量規劃</td>
          <td>session、環境資訊</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>事件命名規範 → <a href="/blog/monitoring/01-mental-model/event-naming-convention/" data-link-title="事件命名規範" data-link-desc="namespace.action 格式的事件命名、命名一致性的工程價值、和商業方案命名慣例的對應">事件命名規範</a></li>
<li>從需求推導收集策略 → <a href="/blog/monitoring/01-mental-model/derive-collection-from-requirements/" data-link-title="從需求推導「該收集哪些事件」" data-link-desc="從 debug 需求、行為分析需求、效能需求、合規需求四個方向推導事件收集策略 — 避免「什麼都收」和「什麼都不收」">從需求推導「該收集哪些事件」</a></li>
<li>Event 類事件在商業分析中的用途 → <a href="/blog/monitoring/08-business-analytics/" data-link-title="模組八：行為資料的商業利用" data-link-desc="Funnel / Cohort / Attribution / A/B test / 推薦系統 / RFM — 從 debug 工具到商業資產的翻轉">模組八 行為資料的商業利用</a></li>
<li>Log 點的設計方法 → <a href="/blog/testing/02-client-observability/" data-link-title="模組二：客戶端可觀測性" data-link-desc="連線生命週期 log、protocol 訊息 log、使用者行為 log — log 設計是功能規格的一部分">testing 模組二 客戶端可觀測性</a></li>
</ul>
]]></content:encoded></item><item><title>Flutter 平台適配</title><link>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/flutter-platform/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/flutter-platform/</guid><description>&lt;p>Flutter 應用程式在 Dart VM 中執行，有自己的執行緒模型（Isolate）、原生平台橋接（Platform channel）和 app 生命週期管理。監控 SDK 在 Flutter 中需要處理的平台特殊問題集中在這三個面向。&lt;/p>
&lt;h2 id="isolate-安全">Isolate 安全&lt;/h2>
&lt;p>Dart 的 Isolate 是獨立的記憶體空間，Isolate 之間不共享記憶體，只能透過 message passing 溝通。SDK 的記憶體 buffer 存在於 main isolate 中，其他 isolate 產生的事件需要透過 port 傳送到 main isolate 才能進入 buffer。&lt;/p>
&lt;p>SDK 端的適配：&lt;/p>
&lt;p>提供 &lt;code>Monitor.eventFromIsolate(SendPort port)&lt;/code> 方法，在子 isolate 中透過 port 把事件送回 main isolate。或者提供 isolate-aware 的 &lt;code>Monitor.init()&lt;/code> 變體，在子 isolate 中初始化一個輕量的 event forwarder。&lt;/p>
&lt;p>如果 SDK 使用 compute 或 Isolate.spawn 做背景任務（例如壓縮 buffer），需要透過 port 把結果送回 main isolate — 背景 isolate 無法直接存取 main isolate 的 HTTP client 或 buffer。&lt;/p>
&lt;h2 id="platform-channel-攔截">Platform channel 攔截&lt;/h2>
&lt;p>Flutter 透過 Platform channel 呼叫原生平台功能（iOS 的 Swift/ObjC、Android 的 Kotlin/Java）。Platform channel 的呼叫可能失敗（原生端未實作、參數格式錯誤、原生端拋出例外），這些錯誤在 Dart 端表現為 &lt;code>PlatformException&lt;/code>。&lt;/p>
&lt;p>SDK 可以攔截 Platform channel 的呼叫記錄每次呼叫的方法名稱、參數、結果和耗時。攔截方式是替換 &lt;code>ServicesBinding.defaultBinaryMessenger&lt;/code> 的處理器，在轉發前後記錄事件。&lt;/p>
&lt;p>攔截的價值是：Platform channel 的錯誤通常難以 debug（stack trace 跨越 Dart 和原生兩層），監控記錄提供「呼叫了哪個 channel method、傳了什麼參數、在哪一層失敗」的完整 context。&lt;/p>
&lt;p>注意：攔截 Platform channel 會增加每次呼叫的延遲（記錄事件的開銷）。對高頻的 Platform channel 呼叫（例如每幀都呼叫的渲染相關 channel），攔截可能影響效能。SDK 應該提供 channel 過濾機制 — 只攔截特定 channel 或只在 debug mode 攔截。&lt;/p>
&lt;h2 id="app-lifecycle-事件">App lifecycle 事件&lt;/h2>
&lt;p>Flutter 的 &lt;code>WidgetsBindingObserver&lt;/code> 提供 app 生命週期回呼：&lt;/p>
&lt;ul>
&lt;li>&lt;code>didChangeAppLifecycleState(AppLifecycleState state)&lt;/code> — app 在 resumed（前景）、inactive（部分可見）、paused（背景）、detached（即將關閉）之間切換。&lt;/li>
&lt;/ul>
&lt;p>SDK 在 init 時註冊 observer，記錄每次狀態轉換為 lifecycle 事件。&lt;/p>
&lt;p>lifecycle 事件在 flush 策略中有特殊意義：&lt;/p>
&lt;p>&lt;strong>paused（進入背景）&lt;/strong>：觸發 flush — 把 buffer 中的事件送出，因為 app 在背景可能被系統殺掉，buffer 中的事件會遺失。iOS 在 app 進入背景後約 5 秒 suspend，flush 必須在這個時間窗口內完成。&lt;/p>
&lt;p>&lt;strong>resumed（回到前景）&lt;/strong>：檢查上次 flush 是否成功。如果 paused 時的 flush 失敗（網路超時），在 resumed 時重試。&lt;/p>
&lt;p>&lt;strong>detached（即將關閉）&lt;/strong>：呼叫 &lt;code>Monitor.close()&lt;/code> 做最後一次 flush 和資源釋放。detached 的時間窗口更短，close flush 可能被截斷。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>Python 平台的適配 → &lt;a href="https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/python-platform/" data-link-title="Python 平台適配" data-link-desc="GIL 與 threading、atexit 可靠性、subprocess 監控 — Python SDK 的平台特殊考量">Python 平台適配&lt;/a>&lt;/li>
&lt;li>跨平台 timestamp 一致性 → &lt;a href="https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/cross-platform-timestamp/" data-link-title="跨平台 timestamp 一致性" data-link-desc="時區、精度、clock drift — 不同平台產生的 timestamp 在 collector 端需要能正確比對和排序">跨平台 timestamp 一致性&lt;/a>&lt;/li>
&lt;li>自動攔截機制 → &lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/auto-intercept/" data-link-title="自動攔截機制" data-link-desc="JS window.onerror / Flutter FlutterError.onError / Python sys.excepthook — 各平台攔截未捕獲例外的機制和限制">模組三 自動攔截&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Flutter 應用程式在 Dart VM 中執行，有自己的執行緒模型（Isolate）、原生平台橋接（Platform channel）和 app 生命週期管理。監控 SDK 在 Flutter 中需要處理的平台特殊問題集中在這三個面向。</p>
<h2 id="isolate-安全">Isolate 安全</h2>
<p>Dart 的 Isolate 是獨立的記憶體空間，Isolate 之間不共享記憶體，只能透過 message passing 溝通。SDK 的記憶體 buffer 存在於 main isolate 中，其他 isolate 產生的事件需要透過 port 傳送到 main isolate 才能進入 buffer。</p>
<p>SDK 端的適配：</p>
<p>提供 <code>Monitor.eventFromIsolate(SendPort port)</code> 方法，在子 isolate 中透過 port 把事件送回 main isolate。或者提供 isolate-aware 的 <code>Monitor.init()</code> 變體，在子 isolate 中初始化一個輕量的 event forwarder。</p>
<p>如果 SDK 使用 compute 或 Isolate.spawn 做背景任務（例如壓縮 buffer），需要透過 port 把結果送回 main isolate — 背景 isolate 無法直接存取 main isolate 的 HTTP client 或 buffer。</p>
<h2 id="platform-channel-攔截">Platform channel 攔截</h2>
<p>Flutter 透過 Platform channel 呼叫原生平台功能（iOS 的 Swift/ObjC、Android 的 Kotlin/Java）。Platform channel 的呼叫可能失敗（原生端未實作、參數格式錯誤、原生端拋出例外），這些錯誤在 Dart 端表現為 <code>PlatformException</code>。</p>
<p>SDK 可以攔截 Platform channel 的呼叫記錄每次呼叫的方法名稱、參數、結果和耗時。攔截方式是替換 <code>ServicesBinding.defaultBinaryMessenger</code> 的處理器，在轉發前後記錄事件。</p>
<p>攔截的價值是：Platform channel 的錯誤通常難以 debug（stack trace 跨越 Dart 和原生兩層），監控記錄提供「呼叫了哪個 channel method、傳了什麼參數、在哪一層失敗」的完整 context。</p>
<p>注意：攔截 Platform channel 會增加每次呼叫的延遲（記錄事件的開銷）。對高頻的 Platform channel 呼叫（例如每幀都呼叫的渲染相關 channel），攔截可能影響效能。SDK 應該提供 channel 過濾機制 — 只攔截特定 channel 或只在 debug mode 攔截。</p>
<h2 id="app-lifecycle-事件">App lifecycle 事件</h2>
<p>Flutter 的 <code>WidgetsBindingObserver</code> 提供 app 生命週期回呼：</p>
<ul>
<li><code>didChangeAppLifecycleState(AppLifecycleState state)</code> — app 在 resumed（前景）、inactive（部分可見）、paused（背景）、detached（即將關閉）之間切換。</li>
</ul>
<p>SDK 在 init 時註冊 observer，記錄每次狀態轉換為 lifecycle 事件。</p>
<p>lifecycle 事件在 flush 策略中有特殊意義：</p>
<p><strong>paused（進入背景）</strong>：觸發 flush — 把 buffer 中的事件送出，因為 app 在背景可能被系統殺掉，buffer 中的事件會遺失。iOS 在 app 進入背景後約 5 秒 suspend，flush 必須在這個時間窗口內完成。</p>
<p><strong>resumed（回到前景）</strong>：檢查上次 flush 是否成功。如果 paused 時的 flush 失敗（網路超時），在 resumed 時重試。</p>
<p><strong>detached（即將關閉）</strong>：呼叫 <code>Monitor.close()</code> 做最後一次 flush 和資源釋放。detached 的時間窗口更短，close flush 可能被截斷。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Python 平台的適配 → <a href="/blog/monitoring/05-platform-adaptation/python-platform/" data-link-title="Python 平台適配" data-link-desc="GIL 與 threading、atexit 可靠性、subprocess 監控 — Python SDK 的平台特殊考量">Python 平台適配</a></li>
<li>跨平台 timestamp 一致性 → <a href="/blog/monitoring/05-platform-adaptation/cross-platform-timestamp/" data-link-title="跨平台 timestamp 一致性" data-link-desc="時區、精度、clock drift — 不同平台產生的 timestamp 在 collector 端需要能正確比對和排序">跨平台 timestamp 一致性</a></li>
<li>自動攔截機制 → <a href="/blog/monitoring/03-sdk-design/auto-intercept/" data-link-title="自動攔截機制" data-link-desc="JS window.onerror / Flutter FlutterError.onError / Python sys.excepthook — 各平台攔截未捕獲例外的機制和限制">模組三 自動攔截</a></li>
</ul>
]]></content:encoded></item><item><title>5.6 Platform Lifecycle Contract</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/</guid><description>&lt;p>Platform lifecycle contract 的核心責任是讓服務和部署平台對同一組生命週期訊號有共同解讀。進入 Kubernetes、systemd、Docker、ELB 或 Envoy 前，讀者需要先理解「服務啟動」和「服務可接流量」是不同狀態。&lt;/p>
&lt;h2 id="lifecycle-contract">Lifecycle Contract&lt;/h2>
&lt;p>Lifecycle contract 定義平台如何啟動、檢查、接流量、停止與回收服務實例。它包含 runtime、startup、readiness、liveness、shutdown 與 drain。&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>runtime&lt;/td>
 &lt;td>固定 image、entrypoint、config 與 resource&lt;/td>
 &lt;td>提供可預期執行環境&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>startup&lt;/td>
 &lt;td>初始化依賴與內部狀態&lt;/td>
 &lt;td>避免過早重啟慢啟動服務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>readiness&lt;/td>
 &lt;td>宣告可安全接流量&lt;/td>
 &lt;td>只把流量導向 ready instance&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>liveness&lt;/td>
 &lt;td>宣告基本運作能力&lt;/td>
 &lt;td>在不可恢復時重建 instance&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>shutdown&lt;/td>
 &lt;td>停接新工作並釋放資源&lt;/td>
 &lt;td>給予 termination window&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>drain&lt;/td>
 &lt;td>完成在途請求或連線退場&lt;/td>
 &lt;td>從路由集合摘除 instance&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些狀態分開後，部署事故才能定位是啟動、接流量、退場還是平台判讀問題。&lt;/p>
&lt;p>runtime 與 startup 決定服務能否形成可運行實例。readiness 與 liveness 決定平台何時導入流量與何時重建實例。shutdown 與 drain 決定版本退場時是否能保護在途工作。這些狀態都屬於生命週期合約，卻對應不同的事故處理路徑。&lt;/p>
&lt;h2 id="startup-與-readiness">Startup 與 Readiness&lt;/h2>
&lt;p>startup 的責任是確認服務初始化完成。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 的責任是確認服務可承接實際流量。啟動完成不代表依賴已就緒，也不代表背景任務、config、secret 或 connection pool 都可用。&lt;/p>
&lt;p>慢啟動服務需要 startup gate，避免 liveness 在初始化期間反覆重啟。依賴敏感服務需要 readiness gate，避免尚未連上資料庫、cache 或 queue 時就接收請求。&lt;/p>
&lt;h3 id="啟動時間的組成與壓縮">啟動時間的組成與壓縮&lt;/h3>
&lt;p>服務啟動時間的長短決定 rollout 節奏的下限。啟動時間由四段組成，每段有不同壓縮策略：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>runtime 初始化&lt;/strong>：語言 VM、GC 初始化、class loading（JVM warmup 可達 10-30 秒）。壓縮手段是 ahead-of-time compilation（GraalVM native image、Go 靜態編譯啟動速度快）或 CDS（Class Data Sharing）。&lt;/li>
&lt;li>&lt;strong>依賴建立&lt;/strong>：資料庫連線池、cache 連線、queue consumer 註冊。壓縮手段是 lazy initialization（按需建立）或 connection pool pre-warming（啟動時建好但不阻擋 readiness）。&lt;/li>
&lt;li>&lt;strong>資料預載&lt;/strong>：config 同步、feature flag 初始拉取、本地快取預熱。壓縮手段是區分必要載入與非必要載入——必要的阻擋 readiness，非必要的平行載入。&lt;/li>
&lt;li>&lt;strong>就緒驗證&lt;/strong>：自我健康檢查、依賴可達性驗證。壓縮手段是平行驗證多個依賴，避免串行等待。&lt;/li>
&lt;/ol>
&lt;p>啟動時間超過平台預設 startup timeout 時，先拆成這四段分析瓶頸，再決定調大 timeout 還是壓縮啟動流程。盲目調大 timeout 會掩蓋啟動退化問題，讓單次 rollout 的最短觀察窗拉長。&lt;/p>
&lt;h3 id="readiness-設計的核心取捨">Readiness 設計的核心取捨&lt;/h3>
&lt;p>readiness 太鬆（只檢查 HTTP port 是否可達）會讓尚未就緒的實例接到流量。readiness 太緊（檢查所有下游可達性）會讓非自身問題的下游故障觸發連鎖 not-ready，放大故障面。&lt;/p>
&lt;p>取捨的判讀框架是「這個依賴不可用時，服務是否仍能提供有意義的回應」：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>必要依賴&lt;/strong>：資料庫、auth service——不可用時服務完全無法處理請求。這類依賴的可達性應納入 readiness 條件。&lt;/li>
&lt;li>&lt;strong>可降級依賴&lt;/strong>：推薦引擎、非關鍵 cache——不可用時服務可回傳降級結果。這類依賴不應納入 readiness，改用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/circuit-breaker/" data-link-title="Circuit Breaker" data-link-desc="說明下游持續失敗時如何暫停呼叫並保護系統">circuit breaker&lt;/a> 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback&lt;/a> 處理。&lt;/li>
&lt;li>&lt;strong>觀測依賴&lt;/strong>：metrics collector、log shipper——不可用不影響業務流量。這類依賴進 readiness 是常見誤判，會讓觀測基礎設施故障擊倒整個服務。&lt;/li>
&lt;/ul>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s migration&lt;/a>：揭露「跨平台遷移本質是能力遷移、部署 / 觀測 / 恢復與團隊流程都需要同步重建」。遷移到新平台時，舊平台的 readiness 條件不能直接搬——新平台的依賴可達路徑、DNS 解析速度、secret 注入方式可能改變，readiness 條件要重新驗證。&lt;/p>
&lt;h2 id="liveness-與-restart">Liveness 與 Restart&lt;/h2>
&lt;p>liveness 的責任是偵測無法自我恢復的狀態。短暫下游故障適合交給 readiness、circuit breaker 或 fallback 處理，否則平台會用重啟放大故障。&lt;/p>
&lt;p>liveness 太敏感會造成 restart loop；liveness 太寬鬆會讓壞實例長期留在線上。設計時要先定義哪些錯誤可由服務內部恢復，哪些才需要平台重建。&lt;/p>
&lt;h3 id="liveness-適合偵測的失敗模式">Liveness 適合偵測的失敗模式&lt;/h3>
&lt;p>liveness 的工程價值在於捕捉服務自己無法修復的狀態。把 liveness 當成通用健康檢查是過度使用，會讓正常的瞬態故障觸發不必要的重建。&lt;/p></description><content:encoded><![CDATA[<p>Platform lifecycle contract 的核心責任是讓服務和部署平台對同一組生命週期訊號有共同解讀。進入 Kubernetes、systemd、Docker、ELB 或 Envoy 前，讀者需要先理解「服務啟動」和「服務可接流量」是不同狀態。</p>
<h2 id="lifecycle-contract">Lifecycle Contract</h2>
<p>Lifecycle contract 定義平台如何啟動、檢查、接流量、停止與回收服務實例。它包含 runtime、startup、readiness、liveness、shutdown 與 drain。</p>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>服務責任</th>
          <th>平台責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>runtime</td>
          <td>固定 image、entrypoint、config 與 resource</td>
          <td>提供可預期執行環境</td>
      </tr>
      <tr>
          <td>startup</td>
          <td>初始化依賴與內部狀態</td>
          <td>避免過早重啟慢啟動服務</td>
      </tr>
      <tr>
          <td>readiness</td>
          <td>宣告可安全接流量</td>
          <td>只把流量導向 ready instance</td>
      </tr>
      <tr>
          <td>liveness</td>
          <td>宣告基本運作能力</td>
          <td>在不可恢復時重建 instance</td>
      </tr>
      <tr>
          <td>shutdown</td>
          <td>停接新工作並釋放資源</td>
          <td>給予 termination window</td>
      </tr>
      <tr>
          <td>drain</td>
          <td>完成在途請求或連線退場</td>
          <td>從路由集合摘除 instance</td>
      </tr>
  </tbody>
</table>
<p>這些狀態分開後，部署事故才能定位是啟動、接流量、退場還是平台判讀問題。</p>
<p>runtime 與 startup 決定服務能否形成可運行實例。readiness 與 liveness 決定平台何時導入流量與何時重建實例。shutdown 與 drain 決定版本退場時是否能保護在途工作。這些狀態都屬於生命週期合約，卻對應不同的事故處理路徑。</p>
<h2 id="startup-與-readiness">Startup 與 Readiness</h2>
<p>startup 的責任是確認服務初始化完成。<a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 的責任是確認服務可承接實際流量。啟動完成不代表依賴已就緒，也不代表背景任務、config、secret 或 connection pool 都可用。</p>
<p>慢啟動服務需要 startup gate，避免 liveness 在初始化期間反覆重啟。依賴敏感服務需要 readiness gate，避免尚未連上資料庫、cache 或 queue 時就接收請求。</p>
<h3 id="啟動時間的組成與壓縮">啟動時間的組成與壓縮</h3>
<p>服務啟動時間的長短決定 rollout 節奏的下限。啟動時間由四段組成，每段有不同壓縮策略：</p>
<ol>
<li><strong>runtime 初始化</strong>：語言 VM、GC 初始化、class loading（JVM warmup 可達 10-30 秒）。壓縮手段是 ahead-of-time compilation（GraalVM native image、Go 靜態編譯啟動速度快）或 CDS（Class Data Sharing）。</li>
<li><strong>依賴建立</strong>：資料庫連線池、cache 連線、queue consumer 註冊。壓縮手段是 lazy initialization（按需建立）或 connection pool pre-warming（啟動時建好但不阻擋 readiness）。</li>
<li><strong>資料預載</strong>：config 同步、feature flag 初始拉取、本地快取預熱。壓縮手段是區分必要載入與非必要載入——必要的阻擋 readiness，非必要的平行載入。</li>
<li><strong>就緒驗證</strong>：自我健康檢查、依賴可達性驗證。壓縮手段是平行驗證多個依賴，避免串行等待。</li>
</ol>
<p>啟動時間超過平台預設 startup timeout 時，先拆成這四段分析瓶頸，再決定調大 timeout 還是壓縮啟動流程。盲目調大 timeout 會掩蓋啟動退化問題，讓單次 rollout 的最短觀察窗拉長。</p>
<h3 id="readiness-設計的核心取捨">Readiness 設計的核心取捨</h3>
<p>readiness 太鬆（只檢查 HTTP port 是否可達）會讓尚未就緒的實例接到流量。readiness 太緊（檢查所有下游可達性）會讓非自身問題的下游故障觸發連鎖 not-ready，放大故障面。</p>
<p>取捨的判讀框架是「這個依賴不可用時，服務是否仍能提供有意義的回應」：</p>
<ul>
<li><strong>必要依賴</strong>：資料庫、auth service——不可用時服務完全無法處理請求。這類依賴的可達性應納入 readiness 條件。</li>
<li><strong>可降級依賴</strong>：推薦引擎、非關鍵 cache——不可用時服務可回傳降級結果。這類依賴不應納入 readiness，改用 <a href="/blog/backend/knowledge-cards/circuit-breaker/" data-link-title="Circuit Breaker" data-link-desc="說明下游持續失敗時如何暫停呼叫並保護系統">circuit breaker</a> 或 <a href="/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback</a> 處理。</li>
<li><strong>觀測依賴</strong>：metrics collector、log shipper——不可用不影響業務流量。這類依賴進 readiness 是常見誤判，會讓觀測基礎設施故障擊倒整個服務。</li>
</ul>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s migration</a>：揭露「跨平台遷移本質是能力遷移、部署 / 觀測 / 恢復與團隊流程都需要同步重建」。遷移到新平台時，舊平台的 readiness 條件不能直接搬——新平台的依賴可達路徑、DNS 解析速度、secret 注入方式可能改變，readiness 條件要重新驗證。</p>
<h2 id="liveness-與-restart">Liveness 與 Restart</h2>
<p>liveness 的責任是偵測無法自我恢復的狀態。短暫下游故障適合交給 readiness、circuit breaker 或 fallback 處理，否則平台會用重啟放大故障。</p>
<p>liveness 太敏感會造成 restart loop；liveness 太寬鬆會讓壞實例長期留在線上。設計時要先定義哪些錯誤可由服務內部恢復，哪些才需要平台重建。</p>
<h3 id="liveness-適合偵測的失敗模式">Liveness 適合偵測的失敗模式</h3>
<p>liveness 的工程價值在於捕捉服務自己無法修復的狀態。把 liveness 當成通用健康檢查是過度使用，會讓正常的瞬態故障觸發不必要的重建。</p>
<p>適合 liveness 偵測的狀態：</p>
<ul>
<li><strong>deadlock</strong>：所有 worker thread 被卡住，無法處理新請求也無法回傳錯誤。liveness endpoint 設在獨立 goroutine / thread 上，如果 worker pool 卡住但 liveness goroutine 能回應，問題在業務邏輯而非 deadlock。</li>
<li><strong>memory leak 導致的 OOM 前兆</strong>：記憶體使用率持續上升不回落，GC 已無法回收。此時主動回報 unhealthy 讓平台在 OOM kill 前重建，比被動等 OOM 更可控——OOM kill 不走 <a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a>，在途請求直接中斷。</li>
<li><strong>essential background task 永久停止</strong>：必要的定期任務（如 license renewal、session cleanup）超過預期間隔仍未執行。這類失敗靜默發生，只有 liveness 主動偵測能發現。</li>
</ul>
<p>不適合 liveness 偵測的狀態：下游資料庫短暫不可用、外部 API timeout、cache miss 率升高。這些由 readiness 或 circuit breaker 處理——用 liveness 重建不會修好下游，只會用重啟放大問題。</p>
<h3 id="restart-的代價量化">Restart 的代價量化</h3>
<p>每次 liveness 觸發的重啟會產生四類代價：</p>
<ol>
<li><strong>在途請求中斷</strong>：被重啟的實例正在處理的請求直接失敗。</li>
<li><strong>連線重建成本</strong>：資料庫連線池、cache 連線、queue consumer 重新建立。</li>
<li><strong>啟動期間的容量缺口</strong>：重啟到 readiness 通過之間，整體服務容量降低。</li>
<li><strong>thundering herd 風險</strong>：多實例同時被 liveness 判定失敗並重啟時，同時重建連線、同時搶資源、下游壓力瞬間放大。</li>
</ol>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio 升級治理</a>：揭露「基礎平台元件升級若缺乏分批治理、會形成全域風險放大器」。以下基於通用工程知識展開：Istio 等 service mesh 升級期間的 sidecar 重啟可觸發大量服務的 liveness 暫時失敗，若 liveness 太敏感會放大成全域 restart storm。升級期的 liveness 閾值應比穩態更寬鬆，或在升級批次中暫時加大 liveness failure threshold。</p>
<h2 id="shutdown-與-drain">Shutdown 與 Drain</h2>
<p>shutdown 的責任是讓服務停止接新工作並完成資源釋放。<a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a> 的責任是讓平台在移除實例前，讓 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a> request、長連線或背景工作有時間收束。</p>
<p>短 request API、長連線服務與 background worker 的 drain 條件不同。短 API 主要看在途請求歸零；長連線看 reconnect 節奏；worker 看已領取工作能否完成或重新排隊。tunnel 入口的 startup / readiness / drain 對齊見 <a href="/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10 Outbound Tunnel 入口</a>。</p>
<h3 id="三種-workload-的-drain-差異">三種 Workload 的 Drain 差異</h3>
<p>不同 workload 類型的 drain 完成條件與時間尺度完全不同，用同一套 drain 設定覆蓋所有 workload 會在至少一類服務上出事。</p>
<p><strong>短 request API</strong>（HTTP REST、gRPC unary）：drain 窗口通常在 5-30 秒。核心條件是在途請求數歸零。風險點是 load balancer 的 deregistration delay——LB 可能在服務已標記 not-ready 後仍送幾秒流量（取決於 health check interval 與 deregistration delay），所以服務端 drain 窗口要覆蓋這段延遲。endpoint 摘除的傳播窗口與 preStop 等待策略見 <a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4 摘除節奏與 Drain 的配合</a>。</p>
<p><strong>長連線服務</strong>（WebSocket、gRPC streaming、SSE）：drain 窗口通常在 30 秒到數分鐘。核心條件是現有連線收斂且 reconnect 波形穩定。風險點是客戶端 reconnect 策略——服務端 drain 完成不代表客戶端已連上新實例。若客戶端沒有 backoff 或 reconnect 目標選擇邏輯，會形成 reconnect storm。drain 設計要跟客戶端 reconnect 策略一起規劃。</p>
<p><strong>Background worker</strong>（queue consumer、定時任務、batch job）：drain 窗口取決於單一工作的最長執行時間。核心條件是已領取的工作完成處理或安全重新排隊。風險點是不可中斷工作——某些 job 做到一半無法重試（例如外部 API 呼叫已發出但回應尚未確認），drain 時序要覆蓋這類 job 的最長完成時間，否則 job 被中斷後產生不一致狀態。</p>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例：平台切流未先 Draining</a>：揭露「切流失敗常在 connection lifecycle 管理」「drain / idle timeout / health check / client retry 沒有同一節奏」。反例中的事故擴大機制正是不同 workload 類型的 drain 條件被忽略——短 API 的 drain 完成了，長連線的 reconnect 仍在震盪，worker 的 job 被中斷重試造成重複處理。</p>
<h3 id="shutdown-信號的傳遞路徑">Shutdown 信號的傳遞路徑</h3>
<p>platform 到 application 的 shutdown 信號傳遞有多個可能斷點。信號從平台送到容器 PID 1、PID 1 轉發到應用進程——PID 1 的信號處理語意與常見陷阱見 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 PID 1 與信號處理</a>。本段聚焦 lifecycle 層的時序問題：</p>
<ul>
<li><strong>preStop hook 與 SIGTERM 時序</strong>：Kubernetes 先執行 preStop hook、再送 SIGTERM。preStop hook 可用來等 LB 摘流量（sleep 幾秒讓 <a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">endpoint 從可用集合移除</a>），讓 SIGTERM 到達時在途流量已經減少。</li>
<li><strong>terminationGracePeriodSeconds</strong>：平台等待的最長時間。超過後 SIGKILL 強制結束，不走 graceful shutdown。這個值要覆蓋 preStop + drain + 資源釋放的總時間。</li>
</ul>
<p>shutdown 信號傳遞的驗證方式是在 staging 環境觸發 pod delete，觀察應用 log 中是否出現 shutdown handler 的紀錄。沒看到 shutdown log 代表信號沒傳到、要先修傳遞路徑再談 drain 設計。</p>
<h2 id="不同-workload-的-lifecycle-特性對照">不同 Workload 的 Lifecycle 特性對照</h2>
<p>生命週期合約的參數設定要依 workload 類型調整。以下是三類常見 workload 的特性差異。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>短 request API</th>
          <th>長連線服務</th>
          <th>Background worker</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>startup 關注點</td>
          <td>依賴連線池建立</td>
          <td>依賴連線池 + 監聽埠就緒</td>
          <td>queue consumer 註冊完成</td>
      </tr>
      <tr>
          <td>readiness 條件</td>
          <td>必要依賴可達 + 連線池滿</td>
          <td>必要依賴可達 + 可接受新連線</td>
          <td>consumer 已註冊 + 可拉取新工作</td>
      </tr>
      <tr>
          <td>liveness 偵測</td>
          <td>deadlock、OOM 前兆</td>
          <td>連線管理 thread 存活</td>
          <td>worker loop 存活、queue 輪詢正常</td>
      </tr>
      <tr>
          <td>drain 完成條件</td>
          <td>在途請求數歸零</td>
          <td>現有連線收斂、reconnect 穩</td>
          <td>已領取工作完成或重新排隊</td>
      </tr>
      <tr>
          <td>drain 窗口</td>
          <td>5-30 秒</td>
          <td>30 秒 - 數分鐘</td>
          <td>取決於最長 job 執行時間</td>
      </tr>
      <tr>
          <td>shutdown 風險</td>
          <td>LB 延遲仍送流量</td>
          <td>reconnect storm</td>
          <td>不可中斷 job 被強制結束</td>
      </tr>
      <tr>
          <td>rollout 節奏建議</td>
          <td>可激進（秒級觀察窗）</td>
          <td>保守（分鐘級、等 reconnect）</td>
          <td>依 job 粒度（完成當前批次再切）</td>
      </tr>
  </tbody>
</table>
<p>這張表是選型前判準的操作化：先確認服務屬於哪類 workload，再套用對應的 lifecycle 參數基線。混合 workload（例如同時提供 HTTP API 和 WebSocket）要取各層的嚴格值——drain 窗口取最長的、readiness 取最嚴格的。</p>
<h2 id="平台如何表達-lifecycle-差異">平台如何表達 Lifecycle 差異</h2>
<p>不同部署平台表達生命週期合約的能力不同。選型時要問的是「這個平台能不能分別設定 startup、readiness、liveness 與 drain」。</p>
<table>
  <thead>
      <tr>
          <th>平台</th>
          <th>startup gate</th>
          <th>readiness 與 liveness 分離</th>
          <th>drain 能力</th>
          <th>termination 窗口</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Kubernetes</td>
          <td>startupProbe</td>
          <td>readinessProbe / livenessProbe 獨立</td>
          <td>preStop hook + endpoint 摘除</td>
          <td>terminationGracePeriodSeconds</td>
      </tr>
      <tr>
          <td>systemd</td>
          <td>無原生 startup probe</td>
          <td>靠 sd_notify(READY=1)</td>
          <td>ExecStop + KillSignal</td>
          <td>TimeoutStopSec</td>
      </tr>
      <tr>
          <td>Docker</td>
          <td>HEALTHCHECK（不分離）</td>
          <td>單一 HEALTHCHECK</td>
          <td>stop_grace_period</td>
          <td>stop_grace_period</td>
      </tr>
      <tr>
          <td>ECS</td>
          <td>startupHealthCheck</td>
          <td>health check（不分離）</td>
          <td>deregistration delay</td>
          <td>stopTimeout</td>
      </tr>
  </tbody>
</table>
<p>Kubernetes 在 lifecycle 表達力上最完整，但參數最多也最容易配錯。systemd 靠 sd_notify 協議明確宣告 readiness，在單機部署場景下反而比 K8s 的 probe 直接。Docker 和 ECS 不分離 readiness 與 liveness，需要在應用層自行實作降級邏輯。</p>
<p>選平台不只看功能清單，要看它表達 lifecycle 差異的粒度是否覆蓋服務需求。若服務需要分離 startup 和 readiness 但平台只有一個 health check，這個差距要在應用層補——代價是複雜度從平台設定轉移到程式碼。</p>
<h2 id="遷移期的-lifecycle-重新驗證">遷移期的 Lifecycle 重新驗證</h2>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/airbnb-kubernetes-cluster-scaling-evolution/" data-link-title="5.C6 Airbnb：Kubernetes 叢集擴縮演進" data-link-desc="從手動擴縮走向自動化容量治理的部署平台案例。">5.C6 Airbnb Kubernetes 叢集擴縮演進</a>：揭露「擴縮策略版本化與可回放」「不同 workload 區分擴縮政策」。以下基於通用工程知識展開：叢集演進過程中，lifecycle 參數的假設會改變——workload 從穩態變成高波動、從單一類型變成混合類型、從小規模變成大規模。lifecycle contract 的參數不是設一次就好，要隨叢集演進重新驗證。</p>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 對照：規模差異下的平台遷移</a>：揭露「小型組織最容易漏掉回退腳本化」「中型組織依賴錯位、服務切過去但資料面 / 認證面 / 觀測面沒同步」。lifecycle contract 在遷移後的完整性驗證不只看 probe 設定——secret 注入時序、資料庫連線池的 endpoint 是否切到新叢集、observability pipeline 的 readiness 是否對齊，都是 lifecycle 合約的一部分。</p>
<p>遷移後的 lifecycle 驗證清單：</p>
<ol>
<li><strong>startup 時序重測</strong>：新平台的 image pull 時間、secret mount 時間、DNS 解析路徑可能不同，原本的 startup timeout 可能不夠。</li>
<li><strong>readiness 依賴路徑檢查</strong>：readiness 檢查的依賴是否仍可達（新叢集到舊資料庫的 latency 是否增加、跨叢集 <a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">service discovery</a> 是否對齊、DNS TTL 與快取行為是否改變）。</li>
<li><strong>drain 行為驗證</strong>：在新平台觸發 pod delete、觀察 drain 完成時間與在途請求處理是否符合預期。</li>
<li><strong>信號傳遞驗證</strong>：在新平台觸發 shutdown、確認 SIGTERM 到達應用進程並觸發 graceful shutdown handler。</li>
</ol>
<h2 id="選型前判準">選型前判準</h2>
<p>部署平台選型前要先回答：</p>
<ol>
<li>服務啟動需要多久，哪些依賴是 readiness 條件。</li>
<li>服務失敗時應由自己恢復，還是由平台重建。</li>
<li>服務停止時有哪些 in-flight request、connection 或 job。</li>
<li>平台是否能表達 startup、readiness、liveness 與 drain 的差異。</li>
</ol>
<p>這些問題決定後續要比較 Kubernetes probe、systemd restart policy、load balancer health check 或 service mesh drain 能力。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>rollout 期間新版本反覆重啟</td>
          <td>startup timeout 小於實際啟動時間</td>
          <td>拆分啟動四段分析瓶頸、調整 startup gate</td>
      </tr>
      <tr>
          <td>新版本 readiness 通過但首批請求錯誤率高</td>
          <td>readiness 條件太鬆、依賴未就緒就接流量</td>
          <td>加入必要依賴檢查、分離可降級依賴</td>
      </tr>
      <tr>
          <td>下游故障時大量實例被 liveness 重啟</td>
          <td>liveness 檢查了不該檢查的下游依賴</td>
          <td>把下游可達性移到 readiness、liveness 只看自身</td>
      </tr>
      <tr>
          <td>shutdown 後仍有請求中斷</td>
          <td>SIGTERM 未正確傳達或 drain 窗口不足</td>
          <td>驗證信號傳遞路徑、調整 terminationGracePeriod</td>
      </tr>
      <tr>
          <td>長連線服務切版後 reconnect storm</td>
          <td>drain 設計未考慮客戶端 reconnect 策略</td>
          <td>拉長 drain、分批切流、搭配 reconnect backoff</td>
      </tr>
      <tr>
          <td>worker 切版後出現重複處理</td>
          <td>job 被中斷後重試、但前次已產生副作用</td>
          <td>drain 窗口覆蓋最長 job、或 job 支援冪等</td>
      </tr>
      <tr>
          <td>遷移新平台後啟動時間變長</td>
          <td>新平台 image pull / secret mount 路徑不同</td>
          <td>重測啟動四段、調整新平台的 startup timeout</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把所有 probe 設成同一個 <code>/health</code> endpoint，會讓 startup、readiness 與 liveness 的語意混在一起。三種 probe 回答不同問題：startup 問「初始化完了嗎」、readiness 問「可以接流量嗎」、liveness 問「還活著嗎」。同一個 endpoint 無法同時回答三個問題，因為初始化完成不代表依賴就緒，依賴暫時不可達不代表服務本身壞了。</p>
<p>把 drain 窗口設成固定值不分 workload 類型，會在某一類服務上出事。5 秒對短 API 足夠、對長連線不夠、對 batch job 遠遠不夠。drain 窗口要依服務實際 workload 設定，不是用平台預設值。</p>
<p>把 liveness 失敗當成「服務壞了」而不問代價，會忽略重啟本身的連鎖效應。每次重啟都有在途請求中斷、連線重建、容量缺口的代價——特別是多實例同時被判定 liveness 失敗時，代價會被放大。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>lifecycle contract 的完整性可用多個案例交叉驗證。<a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s migration</a> 揭露遷移後 readiness 依賴路徑改變的風險。<a href="/blog/backend/05-deployment-platform/cases/failure-platform-cutover-without-drain/" data-link-title="5.C9 反例：平台切流未先 Draining" data-link-desc="切流時忽略連線清退造成請求錯誤與重試風暴。">5.C9 反例</a> 揭露不同 workload 的 drain 條件被忽略造成的事故擴大。<a href="/blog/backend/05-deployment-platform/cases/airbnb-istio-upgrade-governance/" data-link-title="5.C7 Airbnb：Istio 升級治理" data-link-desc="service mesh 升級在大規模環境下如何保持高可用。">5.C7 Airbnb Istio 升級治理</a> 揭露基礎平台元件升級缺乏分批治理會形成全域風險放大器。<a href="/blog/backend/05-deployment-platform/cases/contrast-platform-migration-by-scale/" data-link-title="5.C10 對照：規模差異下的平台遷移" data-link-desc="平台遷移策略在小中大型組織下的差異。">5.C10 對照</a> 揭露不同規模下 lifecycle 驗證的缺口模式。</p>
<p>這些案例共同支撐的判讀是「lifecycle contract 的每個狀態都有不同的失敗模式，混在一起處理會在事故時無法定位」。流量切換或連線生命週期問題路由到 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a>。runtime 產物穩定性問題路由到 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 container 與 runtime</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>lifecycle contract 是部署模組的概念基底，後續章節都會引用本篇的狀態分類。</p>
<ol>
<li>與 5.1 的交接：runtime 與 entrypoint 定義 startup 行為回到 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">container 與 runtime</a>。</li>
<li>與 5.2 的交接：probe 設定與 rollout 節奏回到 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">Kubernetes 部署策略</a>。</li>
<li>與 5.3 的交接：drain 與流量退場回到 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">load balancer 合約</a>。</li>
<li>與 5.10 的交接：tunnel 入口的 readiness 與 drain 對齊回到 <a href="/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">Outbound Tunnel 入口</a>。</li>
<li>與 4.20 的交接：lifecycle 事件的證據收集回到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>。</li>
<li>與 6.8 的交接：lifecycle 狀態作為 release gate 判定條件回到 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看 Kubernetes 如何承接這組生命週期，接著讀 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes 部署策略</a>。要看流量退場如何和 LB 對齊，接著讀 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a>。要看不同平台的 lifecycle 表達力比較，接著讀 <a href="/blog/backend/05-deployment-platform/vendors/" data-link-title="部署平台 Vendor 清單" data-link-desc="規劃 workload runtime、orchestration、traffic、IaC 與 discovery 的服務頁撰寫順序與判準">vendors/</a>。</p>
]]></content:encoded></item><item><title>感測器生命週期管理</title><link>https://tarrragon.github.io/blog/monitoring/03-sdk-design/sensor-lifecycle-management/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/03-sdk-design/sensor-lifecycle-management/</guid><description>&lt;p>感測器的啟用組合隨產品階段變化。早期開發只需要 error 和 lifecycle 幫助 debug，production 上線後需要商業事件和效能量測，A/B 測試期間需要實驗專用感測器。把所有感測器一次全開會浪費頻寬和儲存、產生大量低價值事件；全程只開 error 則在需要行為分析時發現沒有資料。感測器的啟停是設計決策，由 SDK config、collector 下發和 feature flag 三層機制控制。&lt;/p>
&lt;h2 id="五個階段">五個階段&lt;/h2>
&lt;h3 id="早期開發">早期開發&lt;/h3>
&lt;p>開發期的首要需求是 debug — 程式碼寫完跑起來、出問題時能定位。&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>error&lt;/td>
 &lt;td>全開&lt;/td>
 &lt;td>每個例外都要看到&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>lifecycle&lt;/td>
 &lt;td>全開&lt;/td>
 &lt;td>app 啟動、連線、狀態轉換的步驟紀錄&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>開發期的取樣率全部設 1.0（全收）— 事件量極低（開發者自己操作），不需要取樣。&lt;/p>
&lt;h3 id="功能測試">功能測試&lt;/h3>
&lt;p>針對被測功能開啟完整感測器，驗證功能的行為事件和效能指標是否正確觸發。&lt;/p>
&lt;p>被測功能的 event 和 metric 全開。其他功能維持開發期設定。測試期間的感測器設定通常由測試 config 檔覆寫 SDK 預設值。&lt;/p>
&lt;h3 id="production-上線">Production 上線&lt;/h3>
&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>error&lt;/td>
 &lt;td>全收&lt;/td>
 &lt;td>每個 production error 都有 debug 價值&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>lifecycle&lt;/td>
 &lt;td>全收&lt;/td>
 &lt;td>session 分析和環境資訊需要完整紀錄&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>event（高頻 UI）&lt;/td>
 &lt;td>取樣&lt;/td>
 &lt;td>scroll、mousemove、hover 等高頻操作只取部分&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>metric&lt;/td>
 &lt;td>取樣&lt;/td>
 &lt;td>效能指標按時間取樣（每 30 秒一次而非每 frame）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>安全事件&lt;/td>
 &lt;td>全收&lt;/td>
 &lt;td>auth 失敗、權限越界、敏感操作不取樣&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="ab-測試">A/B 測試&lt;/h3>
&lt;p>實驗感測器只對 treatment group 啟用。Control group 不觸發實驗事件，避免污染對照組資料。&lt;/p>
&lt;p>實驗專用事件（&lt;code>experiment.pricing_test.assigned&lt;/code>、&lt;code>experiment.pricing_test.converted&lt;/code>）由 feature flag 控制 — flag 開啟時 SDK 才送這些事件。實驗結束後 flag 關閉，感測器自動停止。&lt;/p>
&lt;p>實驗事件的保留期和實驗週期綁定，實驗結束 + 分析完成後可以 purge。&lt;/p>
&lt;h3 id="功能下線">功能下線&lt;/h3>
&lt;p>功能移除時，對應的感測器 config 一起移除。Collector 端 purge 該功能的歷史事件（或降級到聚合摘要）。&lt;/p>
&lt;p>移除 checklist：SDK config 移除事件名稱 → SDK 版本部署 → 確認 collector 不再收到該事件 → purge 歷史資料（可選）。&lt;/p>
&lt;h2 id="控制機制">控制機制&lt;/h2>
&lt;p>三層控制機制各自適合不同的變更頻率：&lt;/p>
&lt;h3 id="sdk-init-config靜態">SDK init config（靜態）&lt;/h3>
&lt;p>隨 app 版本部署的本地設定檔。變更需要發新版本。適合穩定的感測器組合。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nt">sensors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">error&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled: true, sampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1.0&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">lifecycle&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled: true, sampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1.0&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">event&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">funnel.*&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled: true, sampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1.0&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">click.*&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled: true, sampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.1&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metric&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">duration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled: true, sampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.5&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">experiment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pricing_test&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="w"> &lt;/span>}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="collector-端下發動態">Collector 端下發（動態）&lt;/h3>
&lt;p>SDK 啟動時從 collector 的 &lt;code>/config&lt;/code> endpoint 拉取當前的感測器設定。Collector 端修改設定後，下一次 SDK 重啟或定期 refresh（每 5 分鐘）時生效。適合需要動態調整但不值得接 feature flag 服務的場景。&lt;/p></description><content:encoded><![CDATA[<p>感測器的啟用組合隨產品階段變化。早期開發只需要 error 和 lifecycle 幫助 debug，production 上線後需要商業事件和效能量測，A/B 測試期間需要實驗專用感測器。把所有感測器一次全開會浪費頻寬和儲存、產生大量低價值事件；全程只開 error 則在需要行為分析時發現沒有資料。感測器的啟停是設計決策，由 SDK config、collector 下發和 feature flag 三層機制控制。</p>
<h2 id="五個階段">五個階段</h2>
<h3 id="早期開發">早期開發</h3>
<p>開發期的首要需求是 debug — 程式碼寫完跑起來、出問題時能定位。</p>
<table>
  <thead>
      <tr>
          <th>感測器類型</th>
          <th>啟用</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>error</td>
          <td>全開</td>
          <td>每個例外都要看到</td>
      </tr>
      <tr>
          <td>lifecycle</td>
          <td>全開</td>
          <td>app 啟動、連線、狀態轉換的步驟紀錄</td>
      </tr>
      <tr>
          <td>event</td>
          <td>按需</td>
          <td>正在開發的功能手動加埋點，其他關閉</td>
      </tr>
      <tr>
          <td>metric</td>
          <td>關閉</td>
          <td>效能量測在功能穩定前沒有意義</td>
      </tr>
  </tbody>
</table>
<p>開發期的取樣率全部設 1.0（全收）— 事件量極低（開發者自己操作），不需要取樣。</p>
<h3 id="功能測試">功能測試</h3>
<p>針對被測功能開啟完整感測器，驗證功能的行為事件和效能指標是否正確觸發。</p>
<p>被測功能的 event 和 metric 全開。其他功能維持開發期設定。測試期間的感測器設定通常由測試 config 檔覆寫 SDK 預設值。</p>
<h3 id="production-上線">Production 上線</h3>
<p>上線後的感測器組合平衡覆蓋率和成本：</p>
<table>
  <thead>
      <tr>
          <th>感測器類型</th>
          <th>策略</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>error</td>
          <td>全收</td>
          <td>每個 production error 都有 debug 價值</td>
      </tr>
      <tr>
          <td>lifecycle</td>
          <td>全收</td>
          <td>session 分析和環境資訊需要完整紀錄</td>
      </tr>
      <tr>
          <td>event（核心操作）</td>
          <td>全收</td>
          <td>漏斗關鍵步驟、轉換事件不能漏</td>
      </tr>
      <tr>
          <td>event（高頻 UI）</td>
          <td>取樣</td>
          <td>scroll、mousemove、hover 等高頻操作只取部分</td>
      </tr>
      <tr>
          <td>metric</td>
          <td>取樣</td>
          <td>效能指標按時間取樣（每 30 秒一次而非每 frame）</td>
      </tr>
      <tr>
          <td>安全事件</td>
          <td>全收</td>
          <td>auth 失敗、權限越界、敏感操作不取樣</td>
      </tr>
  </tbody>
</table>
<h3 id="ab-測試">A/B 測試</h3>
<p>實驗感測器只對 treatment group 啟用。Control group 不觸發實驗事件，避免污染對照組資料。</p>
<p>實驗專用事件（<code>experiment.pricing_test.assigned</code>、<code>experiment.pricing_test.converted</code>）由 feature flag 控制 — flag 開啟時 SDK 才送這些事件。實驗結束後 flag 關閉，感測器自動停止。</p>
<p>實驗事件的保留期和實驗週期綁定，實驗結束 + 分析完成後可以 purge。</p>
<h3 id="功能下線">功能下線</h3>
<p>功能移除時，對應的感測器 config 一起移除。Collector 端 purge 該功能的歷史事件（或降級到聚合摘要）。</p>
<p>移除 checklist：SDK config 移除事件名稱 → SDK 版本部署 → 確認 collector 不再收到該事件 → purge 歷史資料（可選）。</p>
<h2 id="控制機制">控制機制</h2>
<p>三層控制機制各自適合不同的變更頻率：</p>
<h3 id="sdk-init-config靜態">SDK init config（靜態）</h3>
<p>隨 app 版本部署的本地設定檔。變更需要發新版本。適合穩定的感測器組合。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">sensors</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">error</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">enabled: true, sampling</span><span class="p">:</span><span class="w"> </span><span class="m">1.0</span><span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">lifecycle</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">enabled: true, sampling</span><span class="p">:</span><span class="w"> </span><span class="m">1.0</span><span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">event</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">funnel.*</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">enabled: true, sampling</span><span class="p">:</span><span class="w"> </span><span class="m">1.0</span><span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">click.*</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">enabled: true, sampling</span><span class="p">:</span><span class="w"> </span><span class="m">0.1</span><span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">metric</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">duration</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">enabled: true, sampling</span><span class="p">:</span><span class="w"> </span><span class="m">0.5</span><span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">experiment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">pricing_test</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w"> </span>}</span></span></code></pre></div><h3 id="collector-端下發動態">Collector 端下發（動態）</h3>
<p>SDK 啟動時從 collector 的 <code>/config</code> endpoint 拉取當前的感測器設定。Collector 端修改設定後，下一次 SDK 重啟或定期 refresh（每 5 分鐘）時生效。適合需要動態調整但不值得接 feature flag 服務的場景。</p>
<p>MVP 階段跳過 collector 下發，只用 SDK 本地 config。下發 API 的定義和實作標為第二階段 — 感測器的開關在 SDK 本地 config 已經能完全控制。</p>
<h3 id="feature-flag-服務整合">Feature flag 服務整合</h3>
<p>SDK 在送出事件前查詢 feature flag 判斷感測器是否啟用。適合 A/B 測試 — flag 可以按使用者 / 百分比 / 條件分群啟用。</p>
<h3 id="優先順序">優先順序</h3>
<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">Feature flag &gt; Collector 下發 &gt; SDK 本地 config</span></span></code></pre></div><p>SDK 本地 config 是 baseline。Collector 下發覆蓋 baseline 的特定欄位。Feature flag 覆蓋一切 — 即使本地 config 和 collector 都說啟用，flag 說關閉就關閉。</p>
<h2 id="取樣率設計">取樣率設計</h2>
<p>取樣率決定「多少比例的事件會被實際送出」。取樣在 SDK 端執行 — 不送的事件不佔頻寬和儲存。</p>
<h3 id="全收sampling-10">全收（sampling: 1.0）</h3>
<p>每筆事件都送。適用於：</p>
<ul>
<li><strong>error</strong>：每個 production error 都有 debug 價值，漏掉的 error 可能是最嚴重的那個</li>
<li><strong>安全事件</strong>：auth 失敗、權限越界的取樣可能讓攻擊嘗試隱形</li>
<li><strong>漏斗關鍵步驟</strong>：funnel 分析的轉換率計算需要精確的步驟計數</li>
</ul>
<h3 id="百分比取樣001-05">百分比取樣（0.01-0.5）</h3>
<p>只送一定比例的事件。適用於高頻且個別事件價值低的場景：</p>
<ul>
<li>scroll / mousemove / hover：每秒觸發數十次，全收會產生大量事件。取樣 1-10% 足以分析使用者行為模式</li>
<li>frame rate 量測：每幀一筆 metric 太多，每秒或每 30 秒取一筆足夠</li>
</ul>
<p>取樣的實作用 SDK 端的隨機數 — <code>if random() &lt; sampling_rate then send(event)</code> — 不需要 server 端參與。</p>
<h3 id="條件取樣retrospective-full-capture">條件取樣（retrospective full capture）</h3>
<p>正常情況取樣，但發生 error 時回溯收集該 session 的全部事件。實作方式是 SDK 在記憶體中保留最近 N 筆事件的環形 buffer，觸發 error 時把 buffer 中的事件一併送出。</p>
<p>條件取樣讓「error session 的上下文完整」和「正常 session 不過度收集」兩個目標共存。</p>
<h2 id="感測器開關的可觀察性">感測器開關的可觀察性</h2>
<p>感測器本身的狀態變化需要被觀察 — 如果感測器靜默失效（config 錯誤導致某類事件停送），開發者可能很久後才發現「怎麼最近沒有 funnel 資料」。</p>
<h3 id="啟動時-log-感測器清單">啟動時 log 感測器清單</h3>
<p>SDK 初始化完成時 log 當前啟用的感測器清單和取樣率。開發者在 debug console 就能看到「哪些感測器在跑」。</p>
<h3 id="config-變更事件">Config 變更事件</h3>
<p>感測器 config 變更時（collector 下發新 config、或 feature flag 變化），SDK 送一個 lifecycle 事件：</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;type&#34;</span><span class="p">:</span> <span class="s2">&#34;lifecycle&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;sensor.config.changed&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nt">&#34;data&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;collector_push&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nt">&#34;changed&#34;</span><span class="p">:</span> <span class="p">{</span><span class="nt">&#34;click.*&#34;</span><span class="p">:</span> <span class="p">{</span><span class="nt">&#34;sampling&#34;</span><span class="p">:</span> <span class="s2">&#34;0.1 → 0.05&#34;</span><span class="p">}},</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nt">&#34;active_sensors&#34;</span><span class="p">:</span> <span class="mi">12</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這筆事件讓開發者在查詢時能看到「某個時間點感測器 config 改變了」，和事件量的變化做交叉比對。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>感測器偵測哪些行為 → <a href="/blog/monitoring/03-sdk-design/frontend-sensor-design/" data-link-title="前端感測器設計" data-link-desc="什麼行為值得埋感測器、每類感測器的實作方式、取樣策略和效能影響 — 和 auto-intercept 的被動攔截互補">前端感測器設計</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>四類事件的定義 → <a href="/blog/monitoring/01-mental-model/four-event-types/" data-link-title="四類事件的完整定義" data-link-desc="Event / Error / Metric / Lifecycle 四類事件各自的語意、觸發時機和典型用途 — 分類是監控體系的統一語言">四類事件的完整定義</a></li>
<li>事件枚舉方法 → <a href="/blog/monitoring/01-mental-model/event-enumeration-method/" data-link-title="事件枚舉與補齊檢查" data-link-desc="從操作盤點系統性地推導出完整的事件清單 — 四類補齊檢查確保沒有遺漏、粒度判準確保每個事件只記一個事實">事件枚舉與補齊檢查</a></li>
</ul>
]]></content:encoded></item><item><title>Hands-on：LLM 運行中 + 結束的資源管理</title><link>https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/resource-management/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/01-local-llm-services/hands-on/resource-management/</guid><description>&lt;p>跑本地 LLM 的核心 invariant 跟雲端不一樣：&lt;strong>Mac 是 shared resource、不是 dedicated GPU&lt;/strong>。雲端 inference server 跑進 dedicated container、結束 instance 自然回收所有資源；本地&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/inference-server/" data-link-title="Inference Server" data-link-desc="載入模型權重、處理 prompt、產生 token 的常駐 process">推論伺服器&lt;/a>跑在你日常用的 Mac、跟 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/unified-memory/" data-link-title="Unified Memory Architecture" data-link-desc="Apple Silicon 讓 CPU / GPU / NE 共用同一塊記憶體：跑大模型的優勢來源">統一記憶體&lt;/a> 共享同一塊容量，忘記管理會 silently 吃光 RAM、磁碟、port、最後讓系統變慢甚至 swap。&lt;/p>
&lt;p>本篇紀錄三個 dimension（RAM / 磁碟 / port）的觀察工具跟釋放姿勢、對比 Ollama 跟 ComfyUI 兩種典型 lifecycle、加上實測釋放數字。對應 &lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">0.7 隱私資料流原理&lt;/a>「每個 hop 都要 audit」這條思維——資源管理也是 hop 級的 audit、不是「裝完就忘」。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>驗證日期&lt;/strong>：2026-05-12
&lt;strong>環境&lt;/strong>：macOS 14、Apple Silicon、Ollama 0.23.2、ComfyUI 0.21.0、SDXL base 1.0&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼這事重要">為什麼這事重要&lt;/h2>
&lt;p>雲端 inference：&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">Container start → load model → serve requests → container stop → 所有 RAM / 磁碟 / port 自動回收&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>本地 inference：&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">brew services start → load model on demand → serve → ??? → 你忘記 stop
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> → RAM / 磁碟一直被佔
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> → 下次重開機才釋放&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>具體會踩到的問題：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>RAM&lt;/strong>：18 GB SDXL 模型載入後不會自動卸、即使 ComfyUI idle、Python process 仍占 RAM&lt;/li>
&lt;li>&lt;strong>磁碟&lt;/strong>：&lt;code>ollama pull&lt;/code> 累積、&lt;code>~/.ollama/models/blobs&lt;/code> 半年可長到 50 GB+、不主動清不會減&lt;/li>
&lt;li>&lt;strong>Port&lt;/strong>：上次 crash 的 &lt;code>ollama serve&lt;/code> 進程沒乾淨清、port 11434 還占著、下次啟動報「address already in use」&lt;/li>
&lt;li>&lt;strong>GPU / Metal&lt;/strong>：模型載入後 Metal context 佔住、跟其他 GPU-using app（影片剪輯、遊戲）競爭&lt;/li>
&lt;/ul>
&lt;h2 id="三個-dimension--觀察工具">三個 dimension + 觀察工具&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Dimension&lt;/th>
 &lt;th>觀察指令&lt;/th>
 &lt;th>看什麼&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>RAM&lt;/td>
 &lt;td>&lt;code>vm_stat | head -5&lt;/code>&lt;/td>
 &lt;td>Pages free（每 page 16 KB）、空閒越多越好&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>RAM（per process）&lt;/td>
 &lt;td>Activity Monitor 或 &lt;code>ps aux | sort -k6 -rn | head&lt;/code>&lt;/td>
 &lt;td>哪個 process 佔最多記憶體&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>磁碟&lt;/td>
 &lt;td>&lt;code>df -h ~ | tail -1&lt;/code>&lt;/td>
 &lt;td>系統 volume 剩餘&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>磁碟（per dir）&lt;/td>
 &lt;td>&lt;code>du -sh ~/.ollama/models/blobs&lt;/code>&lt;/td>
 &lt;td>LLM models 累積量&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Port&lt;/td>
 &lt;td>&lt;code>lsof -i :11434&lt;/code>&lt;/td>
 &lt;td>誰在 listen 該 port&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Process&lt;/td>
 &lt;td>&lt;code>ps aux | grep -i ollama | grep -v grep&lt;/code>&lt;/td>
 &lt;td>Ollama / ComfyUI / Python 跑哪幾個&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ollama loaded models&lt;/td>
 &lt;td>&lt;code>ollama ps&lt;/code>&lt;/td>
 &lt;td>哪些 model 在 RAM、size、idle timer&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>實測：剛 kill 完 ComfyUI（SDXL + Python venv）後、&lt;code>vm_stat&lt;/code> 看到 free pages 從 619K 變 1090K（每 page 16 KB）、約 &lt;strong>+7.5 GB RAM 釋放&lt;/strong>——這就是 SDXL + ComfyUI process 一直占的記憶體量。&lt;/p></description><content:encoded><![CDATA[<p>跑本地 LLM 的核心 invariant 跟雲端不一樣：<strong>Mac 是 shared resource、不是 dedicated GPU</strong>。雲端 inference server 跑進 dedicated container、結束 instance 自然回收所有資源；本地<a href="/blog/llm/knowledge-cards/inference-server/" data-link-title="Inference Server" data-link-desc="載入模型權重、處理 prompt、產生 token 的常駐 process">推論伺服器</a>跑在你日常用的 Mac、跟 <a href="/blog/llm/knowledge-cards/unified-memory/" data-link-title="Unified Memory Architecture" data-link-desc="Apple Silicon 讓 CPU / GPU / NE 共用同一塊記憶體：跑大模型的優勢來源">統一記憶體</a> 共享同一塊容量，忘記管理會 silently 吃光 RAM、磁碟、port、最後讓系統變慢甚至 swap。</p>
<p>本篇紀錄三個 dimension（RAM / 磁碟 / port）的觀察工具跟釋放姿勢、對比 Ollama 跟 ComfyUI 兩種典型 lifecycle、加上實測釋放數字。對應 <a href="/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">0.7 隱私資料流原理</a>「每個 hop 都要 audit」這條思維——資源管理也是 hop 級的 audit、不是「裝完就忘」。</p>
<blockquote>
<p><strong>驗證日期</strong>：2026-05-12
<strong>環境</strong>：macOS 14、Apple Silicon、Ollama 0.23.2、ComfyUI 0.21.0、SDXL base 1.0</p></blockquote>
<h2 id="為什麼這事重要">為什麼這事重要</h2>
<p>雲端 inference：</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">Container start → load model → serve requests → container stop → 所有 RAM / 磁碟 / port 自動回收</span></span></code></pre></div><p>本地 inference：</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">brew services start → load model on demand → serve → ??? → 你忘記 stop
</span></span><span class="line"><span class="ln">2</span><span class="cl">                                              → RAM / 磁碟一直被佔
</span></span><span class="line"><span class="ln">3</span><span class="cl">                                              → 下次重開機才釋放</span></span></code></pre></div><p>具體會踩到的問題：</p>
<ul>
<li><strong>RAM</strong>：18 GB SDXL 模型載入後不會自動卸、即使 ComfyUI idle、Python process 仍占 RAM</li>
<li><strong>磁碟</strong>：<code>ollama pull</code> 累積、<code>~/.ollama/models/blobs</code> 半年可長到 50 GB+、不主動清不會減</li>
<li><strong>Port</strong>：上次 crash 的 <code>ollama serve</code> 進程沒乾淨清、port 11434 還占著、下次啟動報「address already in use」</li>
<li><strong>GPU / Metal</strong>：模型載入後 Metal context 佔住、跟其他 GPU-using app（影片剪輯、遊戲）競爭</li>
</ul>
<h2 id="三個-dimension--觀察工具">三個 dimension + 觀察工具</h2>
<table>
  <thead>
      <tr>
          <th>Dimension</th>
          <th>觀察指令</th>
          <th>看什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>RAM</td>
          <td><code>vm_stat | head -5</code></td>
          <td>Pages free（每 page 16 KB）、空閒越多越好</td>
      </tr>
      <tr>
          <td>RAM（per process）</td>
          <td>Activity Monitor 或 <code>ps aux | sort -k6 -rn | head</code></td>
          <td>哪個 process 佔最多記憶體</td>
      </tr>
      <tr>
          <td>磁碟</td>
          <td><code>df -h ~ | tail -1</code></td>
          <td>系統 volume 剩餘</td>
      </tr>
      <tr>
          <td>磁碟（per dir）</td>
          <td><code>du -sh ~/.ollama/models/blobs</code></td>
          <td>LLM models 累積量</td>
      </tr>
      <tr>
          <td>Port</td>
          <td><code>lsof -i :11434</code></td>
          <td>誰在 listen 該 port</td>
      </tr>
      <tr>
          <td>Process</td>
          <td><code>ps aux | grep -i ollama | grep -v grep</code></td>
          <td>Ollama / ComfyUI / Python 跑哪幾個</td>
      </tr>
      <tr>
          <td>Ollama loaded models</td>
          <td><code>ollama ps</code></td>
          <td>哪些 model 在 RAM、size、idle timer</td>
      </tr>
  </tbody>
</table>
<p>實測：剛 kill 完 ComfyUI（SDXL + Python venv）後、<code>vm_stat</code> 看到 free pages 從 619K 變 1090K（每 page 16 KB）、約 <strong>+7.5 GB RAM 釋放</strong>——這就是 SDXL + ComfyUI process 一直占的記憶體量。</p>
<h2 id="ollama-的-lifecycleauto-unload-模式">Ollama 的 lifecycle（auto-unload 模式）</h2>
<p>Ollama 走「按需 load / idle unload」設計：</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">brew services start ollama          → daemon 啟動、沒 model 載入、RAM 占用 ~200 MB
</span></span><span class="line"><span class="ln">2</span><span class="cl">                                     port 11434 listening
</span></span><span class="line"><span class="ln">3</span><span class="cl">ollama run gemma3:4b &#34;hello&#34;        → 把 model 載入 RAM (~4-5 GB)
</span></span><span class="line"><span class="ln">4</span><span class="cl">                                     立刻 generate response
</span></span><span class="line"><span class="ln">5</span><span class="cl">                                     model 留在 RAM
</span></span><span class="line"><span class="ln">6</span><span class="cl">(idle 5 分鐘、無新 request)         → Ollama 自動 unload model
</span></span><span class="line"><span class="ln">7</span><span class="cl">                                     RAM 釋放、daemon 仍跑著
</span></span><span class="line"><span class="ln">8</span><span class="cl">ollama run gemma3:4b &#34;next&#34;         → 重新 load model（~5-10 秒）、generate
</span></span><span class="line"><span class="ln">9</span><span class="cl">brew services stop ollama           → daemon 結束、port 釋放</span></span></code></pre></div><p><strong>關鍵參數 <code>OLLAMA_KEEP_ALIVE</code></strong>（環境變數、預設 <code>5m</code>）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 看當前 loaded models</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">ollama ps
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># NAME         ID              SIZE      PROCESSOR    UNTIL</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># gemma3:4b    a2af6cc3eb7f    5.5 GB    100% Metal   4 minutes from now</span>
</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 class="c1"># 啟動時調 keep_alive（持續佔 RAM 直到 ollama 重啟）</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nv">OLLAMA_KEEP_ALIVE</span><span class="o">=</span>-1 brew services restart ollama
</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"><span class="c1"># 啟動時讓 model 用完立即 unload</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nv">OLLAMA_KEEP_ALIVE</span><span class="o">=</span><span class="m">0</span> brew services restart ollama</span></span></code></pre></div><p>選 keep_alive 的 trade-off：</p>
<table>
  <thead>
      <tr>
          <th>設定</th>
          <th>RAM 占用</th>
          <th>首字延遲</th>
          <th>適合場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>0</code></td>
          <td>最低（generate 完立即釋放）</td>
          <td>高（每次都重 load）</td>
          <td>偶爾用、RAM 緊張</td>
      </tr>
      <tr>
          <td><code>5m</code>（預設）</td>
          <td>中（活躍用占住、閒 5 分鐘後釋放）</td>
          <td>低（活躍期不重 load）</td>
          <td>大多場景</td>
      </tr>
      <tr>
          <td><code>-1</code></td>
          <td>高（永久占住）</td>
          <td>最低</td>
          <td>整天頻繁用、RAM 充裕</td>
      </tr>
  </tbody>
</table>
<p><strong>主動 unload 指令</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 把 idle 的 model 立刻從 RAM 卸掉、但 daemon 仍跑</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">curl -s http://localhost:11434/api/generate <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  -d <span class="s1">&#39;{&#34;model&#34;: &#34;gemma3:4b&#34;, &#34;keep_alive&#34;: 0}&#39;</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="c1"># 或關掉整個 daemon</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">brew services stop ollama</span></span></code></pre></div><h2 id="comfyui-的-lifecycle持續占用模式">ComfyUI 的 lifecycle（持續占用模式）</h2>
<p>ComfyUI 走完全不同模式：<strong>model 載入後一直在 RAM、直到 server process 結束</strong>。沒有 auto-unload 機制。</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">python main.py                      → ComfyUI server start、port 8188 listening
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">                                     RAM ~3 GB（Python venv + 框架）
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">第一次 Queue Prompt (用 SDXL)        → 載入 sd_xl_base_1.0.safetensors (~6 GB)
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">                                     RAM 跳到 ~9-10 GB
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">                                     generate 完成、model 留在 RAM
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">連續多張生成                          → 維持 ~9-10 GB、沒 unload
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">idle 1 小時                          → 仍 ~9-10 GB（沒 timer）
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">切到 ControlNet workflow             → 多載 ControlNet model (~2 GB)、ComfyUI 自動 swap
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">                                     RAM 暫升、SD 部分可能被 evict 到 disk
</span></span><span class="line"><span class="ln">10</span><span class="cl">Ctrl+C / pkill                       → process 結束、RAM 完全釋放</span></span></code></pre></div><p>要釋放 ComfyUI 占的 RAM、<strong>唯一方法是結束 server</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 找 PID</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">ps aux <span class="p">|</span> grep <span class="s2">&#34;ComfyUI/main.py&#34;</span> <span class="p">|</span> grep -v grep
</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 class="c1"># 優雅關（讓它 cleanup）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">pkill -INT -f <span class="s2">&#34;ComfyUI/main.py&#34;</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="c1"># 強制 kill（如果上面沒反應、最多等 5 秒再強制）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">pkill -KILL -f <span class="s2">&#34;ComfyUI/main.py&#34;</span>
</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 class="c1"># 確認 port 釋放</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">lsof -i :8188 <span class="p">|</span> head -3</span></span></code></pre></div><p>實測：M4 Pro 32GB、SDXL base 載入後 ComfyUI process 占 ~8 GB RAM；<code>pkill -9</code> 後 <code>vm_stat</code> 顯示 free pages 增加 ~470K page（<strong>7.5 GB 釋放</strong>）。</p>
<h3 id="為什麼-ollama-跟-comfyui-設計不同">為什麼 Ollama 跟 ComfyUI 設計不同</h3>
<table>
  <thead>
      <tr>
          <th>因素</th>
          <th>Ollama 設計</th>
          <th>ComfyUI 設計</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主要使用模式</td>
          <td>API 服務、IDE plugin 透過 HTTP 用</td>
          <td>互動 GUI、user 連續調 prompt</td>
      </tr>
      <tr>
          <td>Model 切換頻率</td>
          <td>高（不同任務換不同 model）</td>
          <td>低（一次 session 通常一個 model）</td>
      </tr>
      <tr>
          <td>User 期待的 latency</td>
          <td>低首字延遲（IDE 補完場景）</td>
          <td>高 throughput（連續生圖）</td>
      </tr>
      <tr>
          <td>結論</td>
          <td>Auto-unload 釋 RAM 給其他 model</td>
          <td>持續載入避免重複 load 浪費</td>
      </tr>
  </tbody>
</table>
<p>兩種設計都 valid、適合不同使用模式。理解差異後就知道 ComfyUI 一直占 RAM「不是 bug」、是設計選擇。</p>
<h2 id="跟其他本地-server-對比">跟其他本地 server 對比</h2>
<table>
  <thead>
      <tr>
          <th>Server</th>
          <th>Auto-unload</th>
          <th>主動 unload 指令</th>
          <th>占 RAM 觀察</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ollama</td>
          <td>有（5 分鐘 idle）</td>
          <td><code>keep_alive: 0</code> 或 stop daemon</td>
          <td><code>ollama ps</code></td>
      </tr>
      <tr>
          <td>LM Studio</td>
          <td>無（GUI 主動關閉 model 才釋）</td>
          <td>GUI Eject Model</td>
          <td>Activity Monitor</td>
      </tr>
      <tr>
          <td>llama.cpp <code>llama-server</code></td>
          <td>無</td>
          <td>kill process</td>
          <td><code>lsof -i :8080</code></td>
      </tr>
      <tr>
          <td>ComfyUI</td>
          <td>無</td>
          <td>kill process</td>
          <td><code>ps aux | grep ComfyUI</code></td>
      </tr>
      <tr>
          <td>oMLX</td>
          <td>有（per model 可配）</td>
          <td>API endpoint</td>
          <td>server log</td>
      </tr>
  </tbody>
</table>
<p><strong>結論</strong>：只有 Ollama 跟 oMLX 內建 auto-unload、其他都要手動釋放。GUI server（LM Studio）通常給 user 一個「Eject」按鈕、CLI server 通常要 kill process。</p>
<h2 id="標準釋放程序">標準釋放程序</h2>
<p>寫 code 完一天結束、要釋放所有資源、按下表順序操作：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 確認當前狀態（記下要還回去多少 RAM）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">vm_stat <span class="p">|</span> head -3
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">df -h ~ <span class="p">|</span> tail -1
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">ollama ps
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">ps aux <span class="p">|</span> grep -E <span class="s2">&#34;ollama|ComfyUI|llama-server&#34;</span> <span class="p">|</span> grep -v grep
</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="c1"># 2. 釋放當前載入的 LLM models（Ollama）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">brew services stop ollama
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 或保留 daemon、只 unload model：</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># curl -s http://localhost:11434/api/generate -d &#39;{&#34;model&#34;: &#34;&lt;your model&gt;&#34;, &#34;keep_alive&#34;: 0}&#39;</span>
</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"><span class="c1"># 3. 結束 ComfyUI / 其他 GUI server</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">pkill -INT -f <span class="s2">&#34;ComfyUI/main.py&#34;</span> 2&gt;/dev/null
</span></span><span class="line"><span class="ln">14</span><span class="cl">pkill -INT -f <span class="s2">&#34;llama-server&#34;</span> 2&gt;/dev/null
</span></span><span class="line"><span class="ln">15</span><span class="cl">sleep <span class="m">5</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 強制（如果上面沒清乾淨）</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">pkill -KILL -f <span class="s2">&#34;ComfyUI/main.py&#34;</span> 2&gt;/dev/null
</span></span><span class="line"><span class="ln">18</span><span class="cl">pkill -KILL -f <span class="s2">&#34;llama-server&#34;</span> 2&gt;/dev/null
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"># 4. 驗證所有 port 釋放</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">lsof -i :11434 -i :1234 -i :8080 -i :8188 -i :8000 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">|</span> head
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"># 5. 確認釋放量</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">vm_stat <span class="p">|</span> head -3
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="c1"># free pages 該明顯增加</span></span></span></code></pre></div><h3 id="容易出錯的釋放方式">容易出錯的「釋放方式」</h3>
<ul>
<li><strong><code>killall Python</code></strong>：會 kill 所有 Python process、包括其他 dev tool（如 jupyter、Django）。用 <code>pkill -f &quot;ComfyUI/main.py&quot;</code> 等明確 pattern。</li>
<li><strong><code>rm -rf ~/.ollama</code></strong>：會清掉所有 model registry、下次要重 pull 全部 model。Cleanup 用 <code>ollama rm &lt;model&gt;</code> 才精準。</li>
<li><strong><code>brew uninstall ollama</code></strong>：直接卸載 Ollama 本身、過 reinstall 麻煩。Stop service 就夠。</li>
<li><strong>重開機釋放</strong>：work 但太重、會中斷其他工作。用 process-level 操作即可。</li>
</ul>
<h2 id="磁碟長期累積管理">磁碟長期累積管理</h2>
<p>Models 一旦 <code>pull</code> 進 <code>~/.ollama/models/blobs</code>、不主動 <code>rm</code> 不會減少。半年累積可長到 50 GB+。</p>
<p>Ollama models 只是磁碟大戶之一。整台 Mac 突然被吃光、要從哪裡查起的全機診斷順序（先排除快照浮動、再用實際佔用值逐層找大戶），見 <a href="/blog/other/macos-%E7%A3%81%E7%A2%9F%E7%A9%BA%E9%96%93%E8%A2%AB%E5%90%83%E5%85%89%E7%9A%84%E8%A8%BA%E6%96%B7%E6%B5%81%E7%A8%8B/" data-link-title="macOS 磁碟空間被吃光的診斷流程" data-link-desc="Mac 空間莫名歸零、清 cache 沒救、或空間掉了又回來時的排查順序。避開 sparse 假大小和本地快照浮動的誤判。含 disk-report 腳本。">macOS 磁碟空間診斷流程</a>——那篇的佔用大戶表也會把 ollama 列為其中一項、再連回本篇的專屬清理 idiom。</p>
<h3 id="觀察累積">觀察累積</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Ollama models 總占用</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">du -sh ~/.ollama/models/blobs
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 4.1G    /Users/tarragon/.ollama/models/blobs</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="c1"># 逐 model 看大小</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">ollama list
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># NAME                       ID              SIZE      MODIFIED</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># gemma4:e4b                 c6eb396dbd59    9.6 GB    Less than a second ago</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># nomic-embed-text:latest    0a109f422b47    274 MB    3 hours ago</span>
</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 class="c1"># ComfyUI checkpoints 累積</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">du -sh ~/.ollama ~/Projects/ComfyUI/models 2&gt;/dev/null
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 4.2G    /Users/tarragon/.ollama</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 7.0G    /Users/tarragon/Projects/ComfyUI/models</span></span></span></code></pre></div><h3 id="清理策略">清理策略</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 刪掉很久沒用的 model</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">ollama rm &lt;model-tag&gt;
</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 class="c1"># 一次清掉所有 Ollama models（保留 daemon）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">ollama list <span class="p">|</span> tail -n +2 <span class="p">|</span> awk <span class="s1">&#39;{print $1}&#39;</span> <span class="p">|</span> xargs -I <span class="o">{}</span> ollama rm <span class="o">{}</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="c1"># 看 ComfyUI checkpoints 哪些可清</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">ls -lh ~/Projects/ComfyUI/models/checkpoints/
</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 class="c1"># 手動刪不要的 .safetensors（小心、不能 undo）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">rm ~/Projects/ComfyUI/models/checkpoints/&lt;old-model&gt;.safetensors</span></span></code></pre></div><h3 id="磁碟管理-idiom">磁碟管理 idiom</h3>
<p>定期（每月或磁碟剩 &lt; 20% 時）做：</p>
<ol>
<li><code>du -sh ~/.ollama ~/Projects/ComfyUI/models</code> 看當前累積</li>
<li><code>ollama list</code> 看哪些 model 沒在用（看 <code>MODIFIED</code> 欄、太舊的考慮刪）</li>
<li>刪實驗用的 model、保留 daily-driver</li>
<li>ComfyUI checkpoints 同樣 review</li>
</ol>
<h2 id="port--process-排錯">Port / Process 排錯</h2>
<h3 id="啟動報address-already-in-use">啟動報「address already in use」</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 找誰占</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">lsof -i :11434
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># COMMAND  PID  USER   ...   NAME</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># ollama   xxx  ...    ...   TCP localhost:11434 (LISTEN)</span>
</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 class="c1"># 看是不是 zombie process</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">ps aux <span class="p">|</span> grep <span class="k">$(</span>lsof -ti :11434 <span class="p">|</span> head -1<span class="k">)</span>
</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"><span class="c1"># 清掉</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nb">kill</span> -9 <span class="k">$(</span>lsof -ti :11434<span class="k">)</span>
</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"><span class="c1"># 或重啟 service（會自動清舊 instance）</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">brew services restart ollama</span></span></code></pre></div><h3 id="ollama-daemon-掛了不知道">Ollama daemon 掛了不知道</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 健康檢查</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">curl -s http://localhost:11434/api/version
</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 class="c1"># 沒回應、看 service 狀態</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">brew services list <span class="p">|</span> grep ollama
</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="c1"># 沒在跑、重啟</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">brew services start ollama
</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 class="c1"># 看 log</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">tail -50 /opt/homebrew/var/log/ollama.log</span></span></code></pre></div><h3 id="comfyui-看似跑著但-queue-不動">ComfyUI 看似跑著但 Queue 不動</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 看 stdout / stderr log</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">tail -30 /tmp/comfyui.log  <span class="c1"># 如果啟動時 redirect 到 log</span>
</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 class="c1"># 看是不是 GPU / Metal stuck（極少見、但 SDXL 大量並發可能踩到）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 解法：kill + 重啟</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">pkill -9 -f <span class="s2">&#34;ComfyUI/main.py&#34;</span></span></span></code></pre></div><p>完整排錯流程跟「先確認哪一層壞」見 <a href="/blog/llm/01-local-llm-services/troubleshooting/" data-link-title="1.7 排錯方法論：用三層架構做故障定位" data-link-desc="故障定位的分層思考、症狀到層級的對應反射、log 在三層的角色差異、最小可重現的縮減策略">1.7 排錯方法論</a>。</p>
<h2 id="觀察記憶體佔用實測對照">觀察記憶體佔用：實測對照</h2>
<p>跑這幾步紀錄 baseline → load model → kill 的 RAM 變化：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Baseline</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">vm_stat <span class="p">|</span> grep <span class="s2">&#34;Pages free&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># Pages free:                              1090076.   ← ~17 GB free</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="c1"># 啟動 Ollama + load 4B model</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">brew services start ollama
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">ollama run gemma3:4b <span class="s2">&#34;hello&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">ollama ps
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># NAME       SIZE     PROCESSOR    UNTIL</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># gemma3:4b  5.5 GB   100% Metal   4 minutes from now</span>
</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">vm_stat <span class="p">|</span> grep <span class="s2">&#34;Pages free&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># Pages free:                               750000.   ← 跌 ~5 GB（model 載入）</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># 額外啟動 ComfyUI + load SDXL</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">nohup python main.py &gt; /tmp/comfyui.log 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"># 在 GUI 上 Queue Prompt 跑一次 SDXL generation</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">vm_stat <span class="p">|</span> grep <span class="s2">&#34;Pages free&#34;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># Pages free:                               280000.   ← 再跌 ~7.5 GB（SDXL 載入 + Python venv）</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"># kill 全部</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">brew services stop ollama
</span></span><span class="line"><span class="ln">23</span><span class="cl">pkill -9 -f <span class="s2">&#34;ComfyUI/main.py&#34;</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">sleep <span class="m">3</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">vm_stat <span class="p">|</span> grep <span class="s2">&#34;Pages free&#34;</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="c1"># Pages free:                              1090000.   ← 回到 baseline</span></span></span></code></pre></div><p>每 page 16 KB、所以 free pages 數字 × 16 KB = 實際 free RAM bytes。</p>
<h2 id="自動化釋放launchd--shell-alias">自動化釋放：launchd / shell alias</h2>
<p>寫個 shell function 一鍵 cleanup：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 加進 ~/.zshrc</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">llm-cleanup<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;[*] Stopping Ollama...&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  brew services stop ollama 2&gt;/dev/null
</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 class="nb">echo</span> <span class="s2">&#34;[*] Killing ComfyUI...&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  pkill -INT -f <span class="s2">&#34;ComfyUI/main.py&#34;</span> 2&gt;/dev/null
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  sleep <span class="m">3</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  pkill -KILL -f <span class="s2">&#34;ComfyUI/main.py&#34;</span> 2&gt;/dev/null
</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 class="nb">echo</span> <span class="s2">&#34;[*] Killing other model servers...&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  pkill -KILL -f <span class="s2">&#34;llama-server&#34;</span> 2&gt;/dev/null
</span></span><span class="line"><span class="ln">13</span><span class="cl">  pkill -KILL -f <span class="s2">&#34;lm-studio-server&#34;</span> 2&gt;/dev/null
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;[*] Verifying ports...&#34;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="k">for</span> p in <span class="m">11434</span> <span class="m">1234</span> <span class="m">8080</span> <span class="m">8188</span> 8000<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    lsof -i :<span class="nv">$p</span> 2&gt;/dev/null <span class="p">|</span> head -2
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="k">done</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;[*] Free RAM:&#34;</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  vm_stat <span class="p">|</span> grep <span class="s2">&#34;Pages free&#34;</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="o">}</span></span></span></code></pre></div><p>完事打 <code>llm-cleanup</code> 一鍵釋放、不用記每個 process 怎麼 kill。</p>
<h2 id="何時這篇會過時">何時這篇會過時</h2>
<p><strong>不會過時的部分</strong>：</p>
<ul>
<li>RAM / 磁碟 / port 三個 dimension 是長期 invariant、用什麼 LLM server 都成立。</li>
<li>「Mac 是 shared resource、需要主動管理」這個 framing。</li>
<li>Ollama 跟 ComfyUI 兩種典型 lifecycle 對比（auto-unload vs persistent）。</li>
<li>觀察工具（<code>vm_stat</code>、<code>lsof</code>、<code>ps</code>、<code>du</code>、Activity Monitor）是 macOS 系統 API、不會 deprecate。</li>
<li>標準釋放程序、自動化 shell function 模式。</li>
</ul>
<p><strong>會變的部分</strong>：</p>
<ul>
<li>具體 model size / RAM 占用數字（隨模型架構演化）。</li>
<li><code>OLLAMA_KEEP_ALIVE</code> 等具體環境變數名（Ollama API 演化）。</li>
<li>ComfyUI 可能加 auto-unload feature（社群有 issue 在討論）。</li>
</ul>
<p>讀的時候若指令跑不過、先 <code>--help</code> 看當前版本 flag；釋放 RAM 的「kill process」這個機制本身永遠成立。</p>
<h2 id="跟其他-hands-on-章節的關係">跟其他 hands-on 章節的關係</h2>
<ul>
<li><a href="/blog/llm/01-local-llm-services/hands-on/ollama-setup/" data-link-title="Hands-on：安裝 Ollama &#43; 拉第一個 Gemma 模型" data-link-desc="brew install ollama、launchd service、ollama pull、curl 驗證 OpenAI 相容 API">Ollama 安裝</a>：介紹 <code>brew services start/stop</code>、本篇延伸 lifecycle 細節</li>
<li><a href="/blog/llm/01-local-llm-services/hands-on/comfyui-setup/" data-link-title="Hands-on：安裝 ComfyUI &#43; SDXL base" data-link-desc="git clone、venv、pip install requirements、SDXL safetensors 放哪、--listen 啟動 server、瀏覽器 workflow 驗證">ComfyUI 安裝</a>：介紹 ComfyUI 啟動、本篇延伸 RAM 占用 + 釋放</li>
<li><a href="/blog/llm/01-local-llm-services/troubleshooting/" data-link-title="1.7 排錯方法論：用三層架構做故障定位" data-link-desc="故障定位的分層思考、症狀到層級的對應反射、log 在三層的角色差異、最小可重現的縮減策略">1.7 排錯方法論</a>：用三層架構定位故障、本篇是 lifecycle 視角的補完</li>
<li><a href="/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">0.7 隱私資料流原理</a>：「每個 hop 都要 audit」延伸到資源層</li>
</ul>
<p>整體心法：本地 LLM 工作流跟雲端不一樣、要主動管理 lifecycle、不能裝完就忘。</p>
]]></content:encoded></item><item><title>Startup Probe</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/startup-probe/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/startup-probe/</guid><description>&lt;p>Startup probe 的核心概念是「在服務啟動期間持續探測、確認初始化完成後再交棒給 liveness 與 readiness &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe&lt;/a>」。它保護啟動時間長的服務（JVM warmup、大量依賴連線建立）不被 liveness 在初始化期間判定失敗而反覆重啟。可先對照 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">Probe&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Startup probe 位在 container 啟動與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> / liveness 之間。startup probe 成功前，liveness 和 readiness 不會啟動。startup probe 一旦成功就永久停用，由 liveness 和 readiness 接手。可先對照 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">Graceful Shutdown&lt;/a>。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 startup probe 的訊號是「服務啟動時間超過 liveness 的預設容忍窗口」。典型場景：JVM 服務 warmup 需 30-60 秒、依賴多的服務需要等資料庫連線池和 cache 連線建立。沒有 startup probe 時，liveness 會在初始化期間把健康的服務判定為壞掉，觸發 restart loop。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>startup probe 的總容忍時間 = &lt;code>failureThreshold × periodSeconds&lt;/code>。設計時先量測服務在最差情境下的啟動時間（冷啟動 + image pull + 依賴連線），再加 headroom。startup probe 跟 &lt;code>initialDelaySeconds&lt;/code> 解決同一個問題，但 startup probe 在啟動期間持續探測（能偵測啟動失敗），&lt;code>initialDelaySeconds&lt;/code> 是盲等（無法觀測啟動進度）。&lt;/p></description><content:encoded><![CDATA[<p>Startup probe 的核心概念是「在服務啟動期間持續探測、確認初始化完成後再交棒給 liveness 與 readiness <a href="/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe</a>」。它保護啟動時間長的服務（JVM warmup、大量依賴連線建立）不被 liveness 在初始化期間判定失敗而反覆重啟。可先對照 <a href="/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">Probe</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>Startup probe 位在 container 啟動與 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> / liveness 之間。startup probe 成功前，liveness 和 readiness 不會啟動。startup probe 一旦成功就永久停用，由 liveness 和 readiness 接手。可先對照 <a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">Graceful Shutdown</a>。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 startup probe 的訊號是「服務啟動時間超過 liveness 的預設容忍窗口」。典型場景：JVM 服務 warmup 需 30-60 秒、依賴多的服務需要等資料庫連線池和 cache 連線建立。沒有 startup probe 時，liveness 會在初始化期間把健康的服務判定為壞掉，觸發 restart loop。</p>
<h2 id="設計責任">設計責任</h2>
<p>startup probe 的總容忍時間 = <code>failureThreshold × periodSeconds</code>。設計時先量測服務在最差情境下的啟動時間（冷啟動 + image pull + 依賴連線），再加 headroom。startup probe 跟 <code>initialDelaySeconds</code> 解決同一個問題，但 startup probe 在啟動期間持續探測（能偵測啟動失敗），<code>initialDelaySeconds</code> 是盲等（無法觀測啟動進度）。</p>
]]></content:encoded></item><item><title>7.B5 Detection Engineering Lifecycle</title><link>https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/</link><pubDate>Thu, 30 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/detection-engineering-lifecycle/</guid><description>&lt;p>本篇的責任是建立偵測規則生命週期。讀者讀完後，能把一條 detection rule 從來源定義、驗證、調校、上線、退場整理成可維護流程。&lt;/p>
&lt;h2 id="核心論點">核心論點&lt;/h2>
&lt;p>Detection engineering lifecycle 的核心概念是把規則當資產管理。規則資產包含來源、邏輯、測試、誤報處理、owner、驗收門檻與退場條件。&lt;/p>
&lt;h2 id="讀者入口">讀者入口&lt;/h2>
&lt;p>本篇適合銜接 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/detection-to-response-routing/" data-link-title="7.B2 從偵測到回應的路由" data-link-desc="建立資安偵測訊號如何轉成 triage、severity、升級與 incident workflow 的大綱">7.B2 從偵測到回應的路由&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/professional-sources/sigma-detection-rule-lifecycle/" data-link-title="Sigma：偵測規則生命週期素材" data-link-desc="把 Sigma detection format 轉成偵測規則欄位、誤報治理與維護流程素材">Sigma：偵測規則生命週期素材&lt;/a>。&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>Rule source&lt;/td>
 &lt;td>描述規則來自哪個威脅假設與資料源&lt;/td>
 &lt;td>Sigma、事件復盤、演練結果&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Detection logic&lt;/td>
 &lt;td>定義條件、例外、聚合方式&lt;/td>
 &lt;td>rule repository、query package&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Validation evidence&lt;/td>
 &lt;td>證明規則可命中目標情境&lt;/td>
 &lt;td>測試事件、回放資料、對照 log&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tuning decision&lt;/td>
 &lt;td>收斂誤報與漏報&lt;/td>
 &lt;td>triage 結果、分析註記、例外記錄&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Release condition&lt;/td>
 &lt;td>定義規則上線條件&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate&lt;/a>、變更審查&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retirement condition&lt;/td>
 &lt;td>定義規則退場條件&lt;/td>
 &lt;td>覆蓋重疊、威脅變化、資料源變動&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>生命周期欄位的核心是讓規則維護可以追溯。每次規則更新都能回查它解哪個風險、用哪個證據驗證、為何做這次調整。&lt;/p>
&lt;h2 id="規則來源治理">規則來源治理&lt;/h2>
&lt;p>規則來源治理的責任是讓規則與威脅假設對齊。來源可來自公開框架、事件教訓、演練情境與稽核要求，並需要在建立時寫清楚 threat hypothesis 與 data dependency。&lt;/p>
&lt;h2 id="驗證節奏">驗證節奏&lt;/h2>
&lt;p>驗證節奏的責任是確保規則在上線前後都保持有效。建議至少建立三層驗證：&lt;/p>
&lt;ol>
&lt;li>邏輯驗證：條件可讀、可測、可重現。&lt;/li>
&lt;li>資料驗證：log schema 與欄位品質可支撐判讀。&lt;/li>
&lt;li>情境驗證：在事件回放或 game day 中能命中目標行為。&lt;/li>
&lt;/ol>
&lt;h2 id="調校策略">調校策略&lt;/h2>
&lt;p>調校策略的責任是把 alert 噪音轉成可判讀訊號。調校時同步記錄 false positive 情境、排除條件、影響範圍與回退方式，並和 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity&lt;/a> 對齊分級節奏。&lt;/p>
&lt;h2 id="上線與退場">上線與退場&lt;/h2>
&lt;p>上線與退場的責任是讓規則變更進入受控流程。上線前需確認 evidence、owner 與回退路徑；退場時要確認替代規則、覆蓋遷移與歷史證據保留。&lt;/p>
&lt;h2 id="與事故流程的交接">與事故流程的交接&lt;/h2>
&lt;p>與事故流程交接的責任是把規則命中轉成回應路由。規則命中後應直接輸出 triage 問題、owner、升級條件與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 路由，讓 08 模組可以快速接手。&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>規則持續觸發但分析結論分散&lt;/td>
 &lt;td>需要調校紀錄與 triage 問題&lt;/td>
 &lt;td>7.B5 → 7.B2&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>規則上線後缺少驗證證據&lt;/td>
 &lt;td>需要補 validation evidence&lt;/td>
 &lt;td>7.B5 → 7.B3&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>相同風險出現多條重複規則&lt;/td>
 &lt;td>需要整理來源與退場條件&lt;/td>
 &lt;td>7.B5 → 7.B1&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>規則變更未進入放行流程&lt;/td>
 &lt;td>需要 release condition&lt;/td>
 &lt;td>7.B5 → 05&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事故後規則未更新&lt;/td>
 &lt;td>需要 write-back 閉環&lt;/td>
 &lt;td>7.B5 → 7.24&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>判讀表格的作用是把規則問題轉成維護任務。每一列都能直接對應到 owner 與下一步交接章節。&lt;/p>
&lt;h2 id="必連章節">必連章節&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/detection-to-response-routing/" data-link-title="7.B2 從偵測到回應的路由" data-link-desc="建立資安偵測訊號如何轉成 triage、severity、升級與 incident workflow 的大綱">7.B2 從偵測到回應的路由&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/security-control-validation/" data-link-title="7.B3 資安控制驗證" data-link-desc="建立資安控制面如何用證據、演練與 release gate 驗證的大綱">7.B3 資安控制驗證&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/blue-team/materials/professional-sources/" data-link-title="7.BM1 藍隊專業來源卡" data-link-desc="整理藍隊可引用的專業來源，明確標示可支撐論點與引用限制">7.BM1 藍隊專業來源卡&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="完稿判準">完稿判準&lt;/h2>
&lt;p>完稿時要讓讀者能為一條偵測規則設計完整生命週期。輸出至少包含來源、邏輯、驗證證據、調校策略、上線條件、退場條件與回寫位置。&lt;/p></description><content:encoded><![CDATA[<p>本篇的責任是建立偵測規則生命週期。讀者讀完後，能把一條 detection rule 從來源定義、驗證、調校、上線、退場整理成可維護流程。</p>
<h2 id="核心論點">核心論點</h2>
<p>Detection engineering lifecycle 的核心概念是把規則當資產管理。規則資產包含來源、邏輯、測試、誤報處理、owner、驗收門檻與退場條件。</p>
<h2 id="讀者入口">讀者入口</h2>
<p>本篇適合銜接 <a href="/blog/backend/07-security-data-protection/blue-team/detection-to-response-routing/" data-link-title="7.B2 從偵測到回應的路由" data-link-desc="建立資安偵測訊號如何轉成 triage、severity、升級與 incident workflow 的大綱">7.B2 從偵測到回應的路由</a>、<a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a> 與 <a href="/blog/backend/07-security-data-protection/blue-team/materials/professional-sources/sigma-detection-rule-lifecycle/" data-link-title="Sigma：偵測規則生命週期素材" data-link-desc="把 Sigma detection format 轉成偵測規則欄位、誤報治理與維護流程素材">Sigma：偵測規則生命週期素材</a>。</p>
<h2 id="生命周期欄位">生命周期欄位</h2>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>責任</th>
          <th>常見來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Rule source</td>
          <td>描述規則來自哪個威脅假設與資料源</td>
          <td>Sigma、事件復盤、演練結果</td>
      </tr>
      <tr>
          <td>Detection logic</td>
          <td>定義條件、例外、聚合方式</td>
          <td>rule repository、query package</td>
      </tr>
      <tr>
          <td>Validation evidence</td>
          <td>證明規則可命中目標情境</td>
          <td>測試事件、回放資料、對照 log</td>
      </tr>
      <tr>
          <td>Tuning decision</td>
          <td>收斂誤報與漏報</td>
          <td>triage 結果、分析註記、例外記錄</td>
      </tr>
      <tr>
          <td>Release condition</td>
          <td>定義規則上線條件</td>
          <td><a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate</a>、變更審查</td>
      </tr>
      <tr>
          <td>Retirement condition</td>
          <td>定義規則退場條件</td>
          <td>覆蓋重疊、威脅變化、資料源變動</td>
      </tr>
  </tbody>
</table>
<p>生命周期欄位的核心是讓規則維護可以追溯。每次規則更新都能回查它解哪個風險、用哪個證據驗證、為何做這次調整。</p>
<h2 id="規則來源治理">規則來源治理</h2>
<p>規則來源治理的責任是讓規則與威脅假設對齊。來源可來自公開框架、事件教訓、演練情境與稽核要求，並需要在建立時寫清楚 threat hypothesis 與 data dependency。</p>
<h2 id="驗證節奏">驗證節奏</h2>
<p>驗證節奏的責任是確保規則在上線前後都保持有效。建議至少建立三層驗證：</p>
<ol>
<li>邏輯驗證：條件可讀、可測、可重現。</li>
<li>資料驗證：log schema 與欄位品質可支撐判讀。</li>
<li>情境驗證：在事件回放或 game day 中能命中目標行為。</li>
</ol>
<h2 id="調校策略">調校策略</h2>
<p>調校策略的責任是把 alert 噪音轉成可判讀訊號。調校時同步記錄 false positive 情境、排除條件、影響範圍與回退方式，並和 <a href="/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity</a> 對齊分級節奏。</p>
<h2 id="上線與退場">上線與退場</h2>
<p>上線與退場的責任是讓規則變更進入受控流程。上線前需確認 evidence、owner 與回退路徑；退場時要確認替代規則、覆蓋遷移與歷史證據保留。</p>
<h2 id="與事故流程的交接">與事故流程的交接</h2>
<p>與事故流程交接的責任是把規則命中轉成回應路由。規則命中後應直接輸出 triage 問題、owner、升級條件與 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 路由，讓 08 模組可以快速接手。</p>
<h2 id="判讀訊號與路由">判讀訊號與路由</h2>
<table>
  <thead>
      <tr>
          <th>判讀訊號</th>
          <th>代表需求</th>
          <th>下一步路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>規則持續觸發但分析結論分散</td>
          <td>需要調校紀錄與 triage 問題</td>
          <td>7.B5 → 7.B2</td>
      </tr>
      <tr>
          <td>規則上線後缺少驗證證據</td>
          <td>需要補 validation evidence</td>
          <td>7.B5 → 7.B3</td>
      </tr>
      <tr>
          <td>相同風險出現多條重複規則</td>
          <td>需要整理來源與退場條件</td>
          <td>7.B5 → 7.B1</td>
      </tr>
      <tr>
          <td>規則變更未進入放行流程</td>
          <td>需要 release condition</td>
          <td>7.B5 → 05</td>
      </tr>
      <tr>
          <td>事故後規則未更新</td>
          <td>需要 write-back 閉環</td>
          <td>7.B5 → 7.24</td>
      </tr>
  </tbody>
</table>
<p>判讀表格的作用是把規則問題轉成維護任務。每一列都能直接對應到 owner 與下一步交接章節。</p>
<h2 id="必連章節">必連章節</h2>
<ul>
<li><a href="/blog/backend/07-security-data-protection/blue-team/detection-to-response-routing/" data-link-title="7.B2 從偵測到回應的路由" data-link-desc="建立資安偵測訊號如何轉成 triage、severity、升級與 incident workflow 的大綱">7.B2 從偵測到回應的路由</a></li>
<li><a href="/blog/backend/07-security-data-protection/blue-team/security-control-validation/" data-link-title="7.B3 資安控制驗證" data-link-desc="建立資安控制面如何用證據、演練與 release gate 驗證的大綱">7.B3 資安控制驗證</a></li>
<li><a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">7.13 偵測覆蓋率與訊號治理</a></li>
<li><a href="/blog/backend/07-security-data-protection/blue-team/materials/professional-sources/" data-link-title="7.BM1 藍隊專業來源卡" data-link-desc="整理藍隊可引用的專業來源，明確標示可支撐論點與引用限制">7.BM1 藍隊專業來源卡</a></li>
</ul>
<h2 id="完稿判準">完稿判準</h2>
<p>完稿時要讓讀者能為一條偵測規則設計完整生命週期。輸出至少包含來源、邏輯、驗證證據、調校策略、上線條件、退場條件與回寫位置。</p>
]]></content:encoded></item></channel></rss>