<?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>Platform on Tarragon</title><link>https://tarrragon.github.io/blog/tags/platform/</link><description>Recent content in Platform on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 26 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/platform/index.xml" rel="self" type="application/rss+xml"/><item><title>環境與系統升級：帶電施工的遷移操作</title><link>https://tarrragon.github.io/blog/infra/upgrade/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/upgrade/</guid><description>&lt;p>環境與系統升級跟從零建置的差別在於：從零建置時可以先建好再上線，升級時系統已經在服務客戶，每一步操作都要在不中斷（或可控中斷）的前提下完成。這個約束決定了升級的操作模式——不是「拆掉重建」，而是「在旁邊建一個新的、驗證通過後切過去、確認沒問題再拆舊的」。&lt;/p>
&lt;p>這個模組處理的是升級的操作框架與各類型的專屬風險，跟&lt;a href="https://tarrragon.github.io/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">成熟度階梯&lt;/a>平行而非串行——升級可能發生在任何成熟度階段。跟&lt;a href="https://tarrragon.github.io/blog/infra/takeover/" data-link-title="接手維運：別人建的環境怎麼接管" data-link-desc="接手前人的專案時，怎麼在不搞壞東西的前提下盤點現況、建立維運能力、逐步正規化 — 從無 SSH 的 FTP 環境到有半套 IaC 的雲端環境都適用">接手維運&lt;/a>的關係是：接手後的下一步常常就是升級（接手一個 PHP 5.6 的站台，穩定維運後第一個任務就是升 PHP 版本）。&lt;/p>
&lt;h2 id="章節文章">章節文章&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/upgrade/upgrade-framework/" data-link-title="升級的共通操作框架" data-link-desc="任何環境或系統升級的四階段模型：差異評估、平行環境驗證、分批切換、退役舊環境，以及貫穿全程的升級紀律">升級的共通操作框架&lt;/a>&lt;/td>
 &lt;td>評估差異、建平行環境、分批切換、退役舊環境的四階段模型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/upgrade/runtime-version-upgrade/" data-link-title="Runtime 版本升級" data-link-desc="PHP / Node.js / Python 大版本升級的相容性評估、本地驗證、分批部署策略與常見陷阱">Runtime 版本升級&lt;/a>&lt;/td>
 &lt;td>PHP / Node / Python 大版本升級的相容性評估、測試策略、分批部署&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/upgrade/platform-migration/" data-link-title="平台遷移" data-link-desc="FTP 面板主機到 VPS、VPS 到雲端、地端到雲端的遷移路徑 — 資料同步策略、DNS 切換、驗證與回退">平台遷移&lt;/a>&lt;/td>
 &lt;td>FTP 面板主機 → VPS → 雲端的遷移路徑、DNS 切換、資料同步&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/upgrade/database-major-upgrade/" data-link-title="資料庫大版本升級" data-link-desc="MySQL 5.7→8.0、PostgreSQL 13→16 等大版本升級的相容性評估、備份保險、平行驗證、切換策略與升級後監控">資料庫大版本升級&lt;/a>&lt;/td>
 &lt;td>MySQL / PostgreSQL 大版本升級的相容性、備份、平行驗證、切換策略&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/upgrade/os-base-software-upgrade/" data-link-title="OS 與基礎軟體更換" data-link-desc="EOL 作業系統的遷移評估、目標 OS 選型、原地升級 vs 平行建置的取捨、應用層遷移清單，以及 Apache → nginx 等基礎軟體切換的操作要點">OS 與基礎軟體更換&lt;/a>&lt;/td>
 &lt;td>EOL OS 的遷移、套件相容性、服務重新部署&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/infra/takeover/" data-link-title="接手維運：別人建的環境怎麼接管" data-link-desc="接手前人的專案時，怎麼在不搞壞東西的前提下盤點現況、建立維運能力、逐步正規化 — 從無 SSH 的 FTP 環境到有半套 IaC 的雲端環境都適用">接手維運&lt;/a>：接手後穩定維運的下一步常是升級&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/before-infra/" data-link-title="模組負一：還沒有 infra 的環境怎麼盡量做好" data-link-desc="手動點起家的環境怎麼守底線、降低未來納管成本、辨識何時該開始導入 IaC — 給還沒有能力上 IaC 的真實起點">模組負一：還沒有 infra 的環境&lt;/a>：升級過程中建立的操作紀律可以對齊這裡&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一：最小可行 IaC&lt;/a>：升級是導入 IaC 的好時機——新環境用 IaC 建、舊環境手動退役&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/05-core-services/" data-link-title="模組五：核心服務上 IaC" data-link-desc="資料庫、運算、儲存、load balancer 怎麼寫進基礎設施程式碼，以及上線順序">模組五：核心服務上 IaC&lt;/a>：資料庫和運算平台的升級涉及 stateful 資源的特殊處理&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>環境與系統升級跟從零建置的差別在於：從零建置時可以先建好再上線，升級時系統已經在服務客戶，每一步操作都要在不中斷（或可控中斷）的前提下完成。這個約束決定了升級的操作模式——不是「拆掉重建」，而是「在旁邊建一個新的、驗證通過後切過去、確認沒問題再拆舊的」。</p>
<p>這個模組處理的是升級的操作框架與各類型的專屬風險，跟<a href="/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">成熟度階梯</a>平行而非串行——升級可能發生在任何成熟度階段。跟<a href="/blog/infra/takeover/" data-link-title="接手維運：別人建的環境怎麼接管" data-link-desc="接手前人的專案時，怎麼在不搞壞東西的前提下盤點現況、建立維運能力、逐步正規化 — 從無 SSH 的 FTP 環境到有半套 IaC 的雲端環境都適用">接手維運</a>的關係是：接手後的下一步常常就是升級（接手一個 PHP 5.6 的站台，穩定維運後第一個任務就是升 PHP 版本）。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/infra/upgrade/upgrade-framework/" data-link-title="升級的共通操作框架" data-link-desc="任何環境或系統升級的四階段模型：差異評估、平行環境驗證、分批切換、退役舊環境，以及貫穿全程的升級紀律">升級的共通操作框架</a></td>
          <td>評估差異、建平行環境、分批切換、退役舊環境的四階段模型</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/upgrade/runtime-version-upgrade/" data-link-title="Runtime 版本升級" data-link-desc="PHP / Node.js / Python 大版本升級的相容性評估、本地驗證、分批部署策略與常見陷阱">Runtime 版本升級</a></td>
          <td>PHP / Node / Python 大版本升級的相容性評估、測試策略、分批部署</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/upgrade/platform-migration/" data-link-title="平台遷移" data-link-desc="FTP 面板主機到 VPS、VPS 到雲端、地端到雲端的遷移路徑 — 資料同步策略、DNS 切換、驗證與回退">平台遷移</a></td>
          <td>FTP 面板主機 → VPS → 雲端的遷移路徑、DNS 切換、資料同步</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/upgrade/database-major-upgrade/" data-link-title="資料庫大版本升級" data-link-desc="MySQL 5.7→8.0、PostgreSQL 13→16 等大版本升級的相容性評估、備份保險、平行驗證、切換策略與升級後監控">資料庫大版本升級</a></td>
          <td>MySQL / PostgreSQL 大版本升級的相容性、備份、平行驗證、切換策略</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/upgrade/os-base-software-upgrade/" data-link-title="OS 與基礎軟體更換" data-link-desc="EOL 作業系統的遷移評估、目標 OS 選型、原地升級 vs 平行建置的取捨、應用層遷移清單，以及 Apache → nginx 等基礎軟體切換的操作要點">OS 與基礎軟體更換</a></td>
          <td>EOL OS 的遷移、套件相容性、服務重新部署</td>
      </tr>
  </tbody>
</table>
<h2 id="跟其他模組的關係">跟其他模組的關係</h2>
<ul>
<li>→ <a href="/blog/infra/takeover/" data-link-title="接手維運：別人建的環境怎麼接管" data-link-desc="接手前人的專案時，怎麼在不搞壞東西的前提下盤點現況、建立維運能力、逐步正規化 — 從無 SSH 的 FTP 環境到有半套 IaC 的雲端環境都適用">接手維運</a>：接手後穩定維運的下一步常是升級</li>
<li>→ <a href="/blog/infra/before-infra/" data-link-title="模組負一：還沒有 infra 的環境怎麼盡量做好" data-link-desc="手動點起家的環境怎麼守底線、降低未來納管成本、辨識何時該開始導入 IaC — 給還沒有能力上 IaC 的真實起點">模組負一：還沒有 infra 的環境</a>：升級過程中建立的操作紀律可以對齊這裡</li>
<li>→ <a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一：最小可行 IaC</a>：升級是導入 IaC 的好時機——新環境用 IaC 建、舊環境手動退役</li>
<li>→ <a href="/blog/infra/05-core-services/" data-link-title="模組五：核心服務上 IaC" data-link-desc="資料庫、運算、儲存、load balancer 怎麼寫進基礎設施程式碼，以及上線順序">模組五：核心服務上 IaC</a>：資料庫和運算平台的升級涉及 stateful 資源的特殊處理</li>
</ul>
]]></content:encoded></item><item><title>JS/TS 平台適配</title><link>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/js-ts-platform/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/js-ts-platform/</guid><description>&lt;p>瀏覽器環境中的監控 SDK 面臨三個平台特有的限制：跨域請求被 CORS 攔截、Service Worker 可以攔截和修改請求、SPA 的路由變換不觸發頁面載入事件。每個限制需要 SDK 在設計層面做適配。&lt;/p>
&lt;h2 id="cors-限制">CORS 限制&lt;/h2>
&lt;p>瀏覽器的同源政策限制網頁向不同 origin 發送請求。SDK 的 HTTP POST 送到 collector endpoint 時，如果 collector 和網頁不在同一個 origin（protocol + domain + port 都相同），瀏覽器會先發送 preflight OPTIONS 請求確認 server 允許跨域存取。&lt;/p>
&lt;p>SDK 端的適配：&lt;/p>
&lt;p>使用 &lt;code>navigator.sendBeacon(url, data)&lt;/code> 代替 &lt;code>fetch&lt;/code> / &lt;code>XMLHttpRequest&lt;/code>。sendBeacon 不受 CORS 限制（瀏覽器對 beacon 請求不做 preflight），且在頁面 unload 時仍能可靠送出 — 適合 close flush 場景。&lt;/p>
&lt;p>sendBeacon 的限制：payload 大小有上限（通常 64KB），不能自訂 Content-Type header（固定為 &lt;code>text/plain&lt;/code> 或 &lt;code>application/x-www-form-urlencoded&lt;/code>），沒有回應 — 送出後無法知道 server 是否收到。&lt;/p>
&lt;p>如果需要 fetch（例如需要讀取回應或送出大 payload），collector 端需要設定 CORS header：&lt;code>Access-Control-Allow-Origin&lt;/code>、&lt;code>Access-Control-Allow-Methods: POST&lt;/code>、&lt;code>Access-Control-Allow-Headers: Content-Type&lt;/code>。&lt;/p>
&lt;h2 id="service-worker-攔截">Service Worker 攔截&lt;/h2>
&lt;p>Service Worker 可以攔截頁面發出的所有 HTTP 請求（包括 SDK 的 POST 請求到 collector）。如果應用程式的 Service Worker 有 cache 策略（cache-first、network-first），SDK 的監控請求可能被快取而非送到 collector。&lt;/p>
&lt;p>SDK 端的適配：&lt;/p>
&lt;p>在 fetch 請求中加 &lt;code>cache: 'no-store'&lt;/code> 防止 Service Worker 快取監控請求。或在請求 URL 加唯一的 query parameter（&lt;code>?_t=timestamp&lt;/code>）讓每次請求的 URL 都不同，繞過 cache 比對。&lt;/p>
&lt;p>如果 SDK 本身提供 Service Worker 模組（在 Service Worker 內攔截 error），需要注意 Service Worker 的生命週期和頁面不同 — Service Worker 可能在頁面關閉後仍在執行，也可能在空閒時被瀏覽器終止。&lt;/p>
&lt;h2 id="spa-路由變換偵測">SPA 路由變換偵測&lt;/h2>
&lt;p>Single Page Application 的路由變換（React Router、Vue Router、Angular Router）不觸發頁面重新載入。從監控角度看，使用者在不同「頁面」之間切換，但 &lt;code>window.onload&lt;/code> 只在首次載入時觸發一次。&lt;/p>
&lt;p>SDK 需要偵測 SPA 路由變換來記錄 &lt;code>lifecycle.view.change&lt;/code> 事件。偵測方式：&lt;/p>
&lt;p>&lt;code>History API&lt;/code> 攔截：monkey-patch &lt;code>history.pushState&lt;/code> 和 &lt;code>history.replaceState&lt;/code>，在呼叫前後記錄路由變換。同時監聽 &lt;code>popstate&lt;/code> 事件處理瀏覽器的上一頁/下一頁。&lt;/p>
&lt;p>&lt;code>MutationObserver&lt;/code>：監聽 DOM 變化偵測頁面內容更新。但 MutationObserver 觸發頻率高，需要 debounce 並搭配 URL 變化檢查，避免把 DOM 微調誤判為路由變換。&lt;/p>
&lt;p>框架特定的 hook：如果 SDK 提供框架整合套件（React / Vue / Angular plugin），可以用框架的 router 事件（&lt;code>useNavigate&lt;/code> hook、&lt;code>router.afterEach&lt;/code> guard）直接取得路由變換資訊，比 monkey-patch History API 更可靠。&lt;/p>
&lt;p>JS/TS 的平台限制理解後，其他平台各有各的挑戰 — &lt;a href="https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/flutter-platform/" data-link-title="Flutter 平台適配" data-link-desc="Isolate 安全、Platform channel 攔截、app lifecycle 事件 — Flutter SDK 的平台特殊考量">Flutter 平台適配&lt;/a>處理 isolate 和 platform channel 的問題。所有平台共同面對的 &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>問題（時區、精度、clock drift）在獨立章節中展開。SDK 的跨平台公開 API 設計見&lt;a href="https://tarrragon.github.io/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&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>瀏覽器環境中的監控 SDK 面臨三個平台特有的限制：跨域請求被 CORS 攔截、Service Worker 可以攔截和修改請求、SPA 的路由變換不觸發頁面載入事件。每個限制需要 SDK 在設計層面做適配。</p>
<h2 id="cors-限制">CORS 限制</h2>
<p>瀏覽器的同源政策限制網頁向不同 origin 發送請求。SDK 的 HTTP POST 送到 collector endpoint 時，如果 collector 和網頁不在同一個 origin（protocol + domain + port 都相同），瀏覽器會先發送 preflight OPTIONS 請求確認 server 允許跨域存取。</p>
<p>SDK 端的適配：</p>
<p>使用 <code>navigator.sendBeacon(url, data)</code> 代替 <code>fetch</code> / <code>XMLHttpRequest</code>。sendBeacon 不受 CORS 限制（瀏覽器對 beacon 請求不做 preflight），且在頁面 unload 時仍能可靠送出 — 適合 close flush 場景。</p>
<p>sendBeacon 的限制：payload 大小有上限（通常 64KB），不能自訂 Content-Type header（固定為 <code>text/plain</code> 或 <code>application/x-www-form-urlencoded</code>），沒有回應 — 送出後無法知道 server 是否收到。</p>
<p>如果需要 fetch（例如需要讀取回應或送出大 payload），collector 端需要設定 CORS header：<code>Access-Control-Allow-Origin</code>、<code>Access-Control-Allow-Methods: POST</code>、<code>Access-Control-Allow-Headers: Content-Type</code>。</p>
<h2 id="service-worker-攔截">Service Worker 攔截</h2>
<p>Service Worker 可以攔截頁面發出的所有 HTTP 請求（包括 SDK 的 POST 請求到 collector）。如果應用程式的 Service Worker 有 cache 策略（cache-first、network-first），SDK 的監控請求可能被快取而非送到 collector。</p>
<p>SDK 端的適配：</p>
<p>在 fetch 請求中加 <code>cache: 'no-store'</code> 防止 Service Worker 快取監控請求。或在請求 URL 加唯一的 query parameter（<code>?_t=timestamp</code>）讓每次請求的 URL 都不同，繞過 cache 比對。</p>
<p>如果 SDK 本身提供 Service Worker 模組（在 Service Worker 內攔截 error），需要注意 Service Worker 的生命週期和頁面不同 — Service Worker 可能在頁面關閉後仍在執行，也可能在空閒時被瀏覽器終止。</p>
<h2 id="spa-路由變換偵測">SPA 路由變換偵測</h2>
<p>Single Page Application 的路由變換（React Router、Vue Router、Angular Router）不觸發頁面重新載入。從監控角度看，使用者在不同「頁面」之間切換，但 <code>window.onload</code> 只在首次載入時觸發一次。</p>
<p>SDK 需要偵測 SPA 路由變換來記錄 <code>lifecycle.view.change</code> 事件。偵測方式：</p>
<p><code>History API</code> 攔截：monkey-patch <code>history.pushState</code> 和 <code>history.replaceState</code>，在呼叫前後記錄路由變換。同時監聽 <code>popstate</code> 事件處理瀏覽器的上一頁/下一頁。</p>
<p><code>MutationObserver</code>：監聽 DOM 變化偵測頁面內容更新。但 MutationObserver 觸發頻率高，需要 debounce 並搭配 URL 變化檢查，避免把 DOM 微調誤判為路由變換。</p>
<p>框架特定的 hook：如果 SDK 提供框架整合套件（React / Vue / Angular plugin），可以用框架的 router 事件（<code>useNavigate</code> hook、<code>router.afterEach</code> guard）直接取得路由變換資訊，比 monkey-patch History API 更可靠。</p>
<p>JS/TS 的平台限制理解後，其他平台各有各的挑戰 — <a href="/blog/monitoring/05-platform-adaptation/flutter-platform/" data-link-title="Flutter 平台適配" data-link-desc="Isolate 安全、Platform channel 攔截、app lifecycle 事件 — Flutter SDK 的平台特殊考量">Flutter 平台適配</a>處理 isolate 和 platform channel 的問題。所有平台共同面對的 <a href="/blog/monitoring/05-platform-adaptation/cross-platform-timestamp/" data-link-title="跨平台 timestamp 一致性" data-link-desc="時區、精度、clock drift — 不同平台產生的 timestamp 在 collector 端需要能正確比對和排序">timestamp 一致性</a>問題（時區、精度、clock drift）在獨立章節中展開。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>。</p>
]]></content:encoded></item><item><title>IaC plan、apply、drift 與 recovery 流程</title><link>https://tarrragon.github.io/blog/ci/iac-platform-deploy/plan-apply-drift-recovery-flow/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/iac-platform-deploy/plan-apply-drift-recovery-flow/</guid><description>&lt;p>IaC 發布流程的核心責任是把基礎設施變更變成可審查、可套用、可追溯的狀態轉移。Terraform、Pulumi、Helm 或平台自動化會改變網路、權限、資料庫、節點、DNS 與部署平台，因此 CI/CD 要把 plan、review、apply、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift&lt;/a> 與 recovery 分成明確 gate。&lt;/p>
&lt;h2 id="流程定位">流程定位&lt;/h2>
&lt;p>IaC 的風險集中在共享狀態與不可逆資源。應用部署失敗常可回退 artifact；基礎設施變更可能刪除資料、替換節點、改掉 IAM 權限或讓 state 與真實環境分叉。發布流程應讓 reviewer 在 apply 前看到「將要改什麼」，並讓 apply 後能確認「環境是否真的符合宣告」。&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>Plan&lt;/td>
 &lt;td>預覽資源差異與風險&lt;/td>
 &lt;td>create / update / replace / destroy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Review&lt;/td>
 &lt;td>審核變更意圖、權限與影響面&lt;/td>
 &lt;td>高風險資源、跨環境、資料資源&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Apply&lt;/td>
 &lt;td>在鎖定狀態下套用變更&lt;/td>
 &lt;td>state lock、timeout、partial apply&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Verify&lt;/td>
 &lt;td>確認環境符合預期&lt;/td>
 &lt;td>health、policy、smoke、connectivity&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift&lt;/a>&lt;/td>
 &lt;td>偵測真實環境與宣告分叉&lt;/td>
 &lt;td>手動 hotfix、console edit、外部系統&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery&lt;/td>
 &lt;td>回退、補正或 state repair&lt;/td>
 &lt;td>是否能安全恢復服務與 state&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Plan 階段負責產生可審查差異。Plan 是 reviewer 判斷資源替換、權限擴大、資料刪除與網路暴露的主要材料。CI 應保留 plan artifact，讓 apply 使用同一份輸入與版本。&lt;/p>
&lt;p>Review 階段負責把風險放到正確 owner。平台、資安、資料庫或服務 owner 應依資源類型參與審核；高風險變更需要額外 gate，例如 maintenance window、人工 approval 或雙人審核。&lt;/p>
&lt;p>Apply 階段負責把宣告狀態寫入環境。&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/state-lock/" data-link-title="State Lock" data-link-desc="說明 IaC apply 如何用狀態鎖避免併發變更覆寫基礎設施狀態">State Lock&lt;/a>、credential、workspace 與環境變數都要固定；partial apply 或 timeout 後，要先判斷 state 與真實資源是否一致，再決定下一步。&lt;/p>
&lt;p>Verify 階段負責確認平台可用。Apply 成功只代表 provider API 接受變更；仍需要 connectivity test、policy check、service smoke test、DNS / certificate check 或 cluster health，確認服務真的能跑。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift&lt;/a> 階段負責發現宣告與現況分叉。手動 hotfix、雲端 console 調整、外部 controller 或 provider 預設值都可能造成 drift；drift detection 要定期執行，並把修復責任導回宣告檔。&lt;/p>
&lt;p>Recovery 階段負責處理失敗套用。IaC 回復不一定是 &lt;code>git revert&lt;/code> 後 apply；可能需要 import、state mv、taint / untaint、手動修復資料資源或 forward fix。流程要先保護資料與服務，再修正宣告與 state。&lt;/p>
&lt;h2 id="plan-review-判讀">Plan review 判讀&lt;/h2>
&lt;p>Plan review 的責任是讓變更影響在 apply 前被看見。Reviewer 應依資源語意判斷，讓 diff 行數退居輔助訊號。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Plan 訊號&lt;/th>
 &lt;th>判讀&lt;/th>
 &lt;th>下一步&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>destroy&lt;/code>&lt;/td>
 &lt;td>資源將被刪除&lt;/td>
 &lt;td>確認資料、依賴與備份&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>replace&lt;/code>&lt;/td>
 &lt;td>先刪後建或重建資源&lt;/td>
 &lt;td>檢查 downtime、IP、DNS、資料&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>IAM 權限擴大&lt;/td>
 &lt;td>blast radius 增加&lt;/td>
 &lt;td>資安或平台 owner 審核&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Network 開放&lt;/td>
 &lt;td>暴露面增加&lt;/td>
 &lt;td>檢查 security group / firewall&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>State 大量漂移&lt;/td>
 &lt;td>宣告與現況長期分叉&lt;/td>
 &lt;td>先處理 drift，再進 feature change&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表讓 review 從「有人按 approve」變成風險判讀。IaC review 的價值在於提前看見不可逆或高代價變更。&lt;/p></description><content:encoded><![CDATA[<p>IaC 發布流程的核心責任是把基礎設施變更變成可審查、可套用、可追溯的狀態轉移。Terraform、Pulumi、Helm 或平台自動化會改變網路、權限、資料庫、節點、DNS 與部署平台，因此 CI/CD 要把 plan、review、apply、<a href="/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift</a> 與 recovery 分成明確 gate。</p>
<h2 id="流程定位">流程定位</h2>
<p>IaC 的風險集中在共享狀態與不可逆資源。應用部署失敗常可回退 artifact；基礎設施變更可能刪除資料、替換節點、改掉 IAM 權限或讓 state 與真實環境分叉。發布流程應讓 reviewer 在 apply 前看到「將要改什麼」，並讓 apply 後能確認「環境是否真的符合宣告」。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Plan</td>
          <td>預覽資源差異與風險</td>
          <td>create / update / replace / destroy</td>
      </tr>
      <tr>
          <td>Review</td>
          <td>審核變更意圖、權限與影響面</td>
          <td>高風險資源、跨環境、資料資源</td>
      </tr>
      <tr>
          <td>Apply</td>
          <td>在鎖定狀態下套用變更</td>
          <td>state lock、timeout、partial apply</td>
      </tr>
      <tr>
          <td>Verify</td>
          <td>確認環境符合預期</td>
          <td>health、policy、smoke、connectivity</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift</a></td>
          <td>偵測真實環境與宣告分叉</td>
          <td>手動 hotfix、console edit、外部系統</td>
      </tr>
      <tr>
          <td>Recovery</td>
          <td>回退、補正或 state repair</td>
          <td>是否能安全恢復服務與 state</td>
      </tr>
  </tbody>
</table>
<p>Plan 階段負責產生可審查差異。Plan 是 reviewer 判斷資源替換、權限擴大、資料刪除與網路暴露的主要材料。CI 應保留 plan artifact，讓 apply 使用同一份輸入與版本。</p>
<p>Review 階段負責把風險放到正確 owner。平台、資安、資料庫或服務 owner 應依資源類型參與審核；高風險變更需要額外 gate，例如 maintenance window、人工 approval 或雙人審核。</p>
<p>Apply 階段負責把宣告狀態寫入環境。<a href="/blog/ci/knowledge-cards/state-lock/" data-link-title="State Lock" data-link-desc="說明 IaC apply 如何用狀態鎖避免併發變更覆寫基礎設施狀態">State Lock</a>、credential、workspace 與環境變數都要固定；partial apply 或 timeout 後，要先判斷 state 與真實資源是否一致，再決定下一步。</p>
<p>Verify 階段負責確認平台可用。Apply 成功只代表 provider API 接受變更；仍需要 connectivity test、policy check、service smoke test、DNS / certificate check 或 cluster health，確認服務真的能跑。</p>
<p><a href="/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift</a> 階段負責發現宣告與現況分叉。手動 hotfix、雲端 console 調整、外部 controller 或 provider 預設值都可能造成 drift；drift detection 要定期執行，並把修復責任導回宣告檔。</p>
<p>Recovery 階段負責處理失敗套用。IaC 回復不一定是 <code>git revert</code> 後 apply；可能需要 import、state mv、taint / untaint、手動修復資料資源或 forward fix。流程要先保護資料與服務，再修正宣告與 state。</p>
<h2 id="plan-review-判讀">Plan review 判讀</h2>
<p>Plan review 的責任是讓變更影響在 apply 前被看見。Reviewer 應依資源語意判斷，讓 diff 行數退居輔助訊號。</p>
<table>
  <thead>
      <tr>
          <th>Plan 訊號</th>
          <th>判讀</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>destroy</code></td>
          <td>資源將被刪除</td>
          <td>確認資料、依賴與備份</td>
      </tr>
      <tr>
          <td><code>replace</code></td>
          <td>先刪後建或重建資源</td>
          <td>檢查 downtime、IP、DNS、資料</td>
      </tr>
      <tr>
          <td>IAM 權限擴大</td>
          <td>blast radius 增加</td>
          <td>資安或平台 owner 審核</td>
      </tr>
      <tr>
          <td>Network 開放</td>
          <td>暴露面增加</td>
          <td>檢查 security group / firewall</td>
      </tr>
      <tr>
          <td>State 大量漂移</td>
          <td>宣告與現況長期分叉</td>
          <td>先處理 drift，再進 feature change</td>
      </tr>
  </tbody>
</table>
<p>這張表讓 review 從「有人按 approve」變成風險判讀。IaC review 的價值在於提前看見不可逆或高代價變更。</p>
<h2 id="drift-處理路由">Drift 處理路由</h2>
<p>Drift 處理的責任是把現況重新帶回可管理狀態。Drift 發現後不應直接 apply 覆蓋，因為 drift 可能是事故 hotfix、外部系統自動調整或宣告檔過期。</p>
<ol>
<li>確認 drift 來源：人工 hotfix、provider 預設、外部 controller 或宣告過期。</li>
<li>判斷 drift 是否仍需要保留：若是真實修復，應回寫到 IaC。</li>
<li>判斷 apply 是否會破壞服務：特別看 replacement、destroy、權限與 network。</li>
<li>修正宣告或 state：必要時使用 import、state mv 或 provider-specific repair。</li>
<li>重新 plan，確認差異收斂到預期。</li>
</ol>
<p>這個路由讓 drift 修復具備審查性。直接在 console 裡補到看起來正常，會讓下一次 CI apply 把修復覆蓋掉。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>反模式的共同問題是把 IaC 降成指令自動化，忽略它承擔的狀態治理責任。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>風險</th>
          <th>替代做法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>plan 與 apply 使用不同輸入</td>
          <td>review 內容與實際套用內容分叉</td>
          <td>保存 plan artifact 或鎖定版本</td>
      </tr>
      <tr>
          <td>沒有 <a href="/blog/ci/knowledge-cards/state-lock/" data-link-title="State Lock" data-link-desc="說明 IaC apply 如何用狀態鎖避免併發變更覆寫基礎設施狀態">State Lock</a></td>
          <td>併發 apply 覆寫狀態</td>
          <td>使用 remote backend 與 locking</td>
      </tr>
      <tr>
          <td>drift 長期忽略</td>
          <td>宣告失去可信度</td>
          <td>定期 drift detection 與 owner 路由</td>
      </tr>
      <tr>
          <td>高風險資源無額外 gate</td>
          <td>資料或網路變更直接進環境</td>
          <td>environment protection / approval</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>IaC 部署總覽：回 <a href="../">IaC / Platform 部署 CI/CD</a>。</li>
<li>環境保護：讀 <a href="/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment Protection</a>。</li>
<li>Gate 原理：讀 <a href="../../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</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>自動攔截機制</title><link>https://tarrragon.github.io/blog/monitoring/03-sdk-design/auto-intercept/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/03-sdk-design/auto-intercept/</guid><description>&lt;p>自動攔截機制讓 SDK 在開發者不寫任何 error 上報程式碼的情況下，自動捕獲未處理的例外並記錄為 error 事件。每個平台有各自的全域錯誤處理器，SDK 在 init 時註冊攔截器，捕獲後轉換為統一的 error 事件格式送出。&lt;/p>
&lt;h2 id="各平台的攔截點">各平台的攔截點&lt;/h2>
&lt;h3 id="javascript--typescript">JavaScript / TypeScript&lt;/h3>
&lt;p>JS 環境有兩個全域錯誤攔截點：&lt;/p>
&lt;p>&lt;code>window.onerror&lt;/code> 捕獲同步程式碼中未處理的例外。回呼函式收到 error message、來源 URL、行號、列號和 Error 物件。&lt;/p>
&lt;p>&lt;code>window.onunhandledrejection&lt;/code> 捕獲未處理的 Promise rejection。回呼函式收到 PromiseRejectionEvent，包含 rejection reason。&lt;/p>
&lt;p>SDK 在 init 時註冊這兩個處理器。註冊前先保存原有的處理器（如果有），攔截後先呼叫原有處理器再執行 SDK 的記錄邏輯 — 避免覆蓋應用程式已有的錯誤處理。&lt;/p>
&lt;p>限制：&lt;code>onerror&lt;/code> 對跨域腳本的錯誤只收到 &lt;code>Script error.&lt;/code> 訊息，沒有 stack trace。需要在 &lt;code>&amp;lt;script&amp;gt;&lt;/code> 標籤加 &lt;code>crossorigin&lt;/code> 屬性，server 端的 CORS header 加 &lt;code>Access-Control-Allow-Origin&lt;/code>。&lt;/p>
&lt;h3 id="flutter">Flutter&lt;/h3>
&lt;p>Flutter 有兩個攔截層：&lt;/p>
&lt;p>&lt;code>FlutterError.onError&lt;/code> 捕獲 widget build / layout / paint 過程中的例外。預設行為是在 console 印出錯誤，SDK 替換為記錄 error 事件後再呼叫預設處理器。&lt;/p>
&lt;p>&lt;code>PlatformDispatcher.instance.onError&lt;/code> 捕獲其他非同步區域的未處理例外（Dart 2.15+）。包含 Isolate 內的未捕獲例外。&lt;/p>
&lt;p>&lt;code>runZonedGuarded&lt;/code> 是另一個選項 — 在指定的 Zone 內捕獲所有未處理例外。SDK 可以用 &lt;code>runZonedGuarded&lt;/code> 包住整個 &lt;code>runApp()&lt;/code>，但這和 &lt;code>PlatformDispatcher.onError&lt;/code> 有重疊，需要避免同一個例外被記錄兩次。&lt;/p>
&lt;p>限制：Flutter 的 release mode 會移除 stack trace 的符號資訊（obfuscation）。需要保留 debug symbols 檔案（&lt;code>.dSYM&lt;/code> / &lt;code>mapping.txt&lt;/code>），在 collector 端做 symbolication。&lt;/p>
&lt;h3 id="python">Python&lt;/h3>
&lt;p>&lt;code>sys.excepthook&lt;/code> 處理主執行緒的未捕獲例外。回呼函式收到 exception type、value 和 traceback。&lt;/p>
&lt;p>&lt;code>threading.excepthook&lt;/code>（Python 3.8+）處理子執行緒的未捕獲例外。&lt;/p>
&lt;p>&lt;code>atexit.register&lt;/code> 用於在 Python 程序退出時 flush 剩餘的 buffer。但 &lt;code>atexit&lt;/code> 在 &lt;code>os._exit()&lt;/code> 或 SIGKILL 時不會執行。&lt;/p>
&lt;p>限制：Python 的 GIL 讓 SDK 的網路操作可能阻塞主執行緒。SDK 的 flush 應該在獨立的 daemon thread 中執行，主執行緒只負責把事件放入 buffer。&lt;/p>
&lt;h2 id="攔截後的統一處理">攔截後的統一處理&lt;/h2>
&lt;p>不同平台的錯誤物件格式不同（JS 的 Error、Flutter 的 FlutterErrorDetails、Python 的 sys.exc_info tuple）。SDK 在攔截後把平台特定的錯誤物件轉換為統一的 error 事件格式：&lt;/p>
&lt;ul>
&lt;li>type: &lt;code>&amp;quot;error&amp;quot;&lt;/code>&lt;/li>
&lt;li>name: 從 error class name 推導（&lt;code>TypeError&lt;/code> → &lt;code>error.TypeError&lt;/code>）&lt;/li>
&lt;li>data: 包含 message、stack trace（字串化）、觸發位置&lt;/li>
&lt;/ul>
&lt;p>轉換層是每個平台 SDK 唯一的平台特定程式碼。轉換完成後，事件進入和手動上報相同的 buffer → flush 管線。&lt;/p>
&lt;h2 id="和手動上報的分工">和手動上報的分工&lt;/h2>
&lt;p>自動攔截處理「開發者沒有預期到的錯誤」— 未捕獲的例外、未處理的 rejection。手動上報（&lt;code>Monitor.error()&lt;/code>）處理「開發者知道可能發生但想記錄的錯誤」— 已捕獲的例外、業務邏輯的異常狀態。&lt;/p>
&lt;p>兩者進入同一個 buffer 和 flush 管線，在 collector 端可以用 data 中的 &lt;code>source: &amp;quot;auto&amp;quot;&lt;/code> / &lt;code>source: &amp;quot;manual&amp;quot;&lt;/code> 欄位區分。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>SDK 公開 API → &lt;a href="https://tarrragon.github.io/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 設計&lt;/a>&lt;/li>
&lt;li>各平台的深入適配問題 → &lt;a href="https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/" data-link-title="模組五：平台適配" data-link-desc="JS CORS / Flutter isolate / Python GIL / Go graceful shutdown — 各平台的特殊考量">模組五 平台適配&lt;/a>&lt;/li>
&lt;li>Buffer 和 flush → &lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/batch-flush/" data-link-title="攢批送出策略" data-link-desc="flush interval / buffer size / flush on close 三個控制點決定事件何時離開 SDK — 平衡即時性和網路效率">攢批送出策略&lt;/a>&lt;/li>
&lt;li>主動感測器設計（和被動攔截互補）→ &lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/frontend-sensor-design/" data-link-title="前端感測器設計" data-link-desc="什麼行為值得埋感測器、每類感測器的實作方式、取樣策略和效能影響 — 和 auto-intercept 的被動攔截互補">前端感測器設計&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>自動攔截機制讓 SDK 在開發者不寫任何 error 上報程式碼的情況下，自動捕獲未處理的例外並記錄為 error 事件。每個平台有各自的全域錯誤處理器，SDK 在 init 時註冊攔截器，捕獲後轉換為統一的 error 事件格式送出。</p>
<h2 id="各平台的攔截點">各平台的攔截點</h2>
<h3 id="javascript--typescript">JavaScript / TypeScript</h3>
<p>JS 環境有兩個全域錯誤攔截點：</p>
<p><code>window.onerror</code> 捕獲同步程式碼中未處理的例外。回呼函式收到 error message、來源 URL、行號、列號和 Error 物件。</p>
<p><code>window.onunhandledrejection</code> 捕獲未處理的 Promise rejection。回呼函式收到 PromiseRejectionEvent，包含 rejection reason。</p>
<p>SDK 在 init 時註冊這兩個處理器。註冊前先保存原有的處理器（如果有），攔截後先呼叫原有處理器再執行 SDK 的記錄邏輯 — 避免覆蓋應用程式已有的錯誤處理。</p>
<p>限制：<code>onerror</code> 對跨域腳本的錯誤只收到 <code>Script error.</code> 訊息，沒有 stack trace。需要在 <code>&lt;script&gt;</code> 標籤加 <code>crossorigin</code> 屬性，server 端的 CORS header 加 <code>Access-Control-Allow-Origin</code>。</p>
<h3 id="flutter">Flutter</h3>
<p>Flutter 有兩個攔截層：</p>
<p><code>FlutterError.onError</code> 捕獲 widget build / layout / paint 過程中的例外。預設行為是在 console 印出錯誤，SDK 替換為記錄 error 事件後再呼叫預設處理器。</p>
<p><code>PlatformDispatcher.instance.onError</code> 捕獲其他非同步區域的未處理例外（Dart 2.15+）。包含 Isolate 內的未捕獲例外。</p>
<p><code>runZonedGuarded</code> 是另一個選項 — 在指定的 Zone 內捕獲所有未處理例外。SDK 可以用 <code>runZonedGuarded</code> 包住整個 <code>runApp()</code>，但這和 <code>PlatformDispatcher.onError</code> 有重疊，需要避免同一個例外被記錄兩次。</p>
<p>限制：Flutter 的 release mode 會移除 stack trace 的符號資訊（obfuscation）。需要保留 debug symbols 檔案（<code>.dSYM</code> / <code>mapping.txt</code>），在 collector 端做 symbolication。</p>
<h3 id="python">Python</h3>
<p><code>sys.excepthook</code> 處理主執行緒的未捕獲例外。回呼函式收到 exception type、value 和 traceback。</p>
<p><code>threading.excepthook</code>（Python 3.8+）處理子執行緒的未捕獲例外。</p>
<p><code>atexit.register</code> 用於在 Python 程序退出時 flush 剩餘的 buffer。但 <code>atexit</code> 在 <code>os._exit()</code> 或 SIGKILL 時不會執行。</p>
<p>限制：Python 的 GIL 讓 SDK 的網路操作可能阻塞主執行緒。SDK 的 flush 應該在獨立的 daemon thread 中執行，主執行緒只負責把事件放入 buffer。</p>
<h2 id="攔截後的統一處理">攔截後的統一處理</h2>
<p>不同平台的錯誤物件格式不同（JS 的 Error、Flutter 的 FlutterErrorDetails、Python 的 sys.exc_info tuple）。SDK 在攔截後把平台特定的錯誤物件轉換為統一的 error 事件格式：</p>
<ul>
<li>type: <code>&quot;error&quot;</code></li>
<li>name: 從 error class name 推導（<code>TypeError</code> → <code>error.TypeError</code>）</li>
<li>data: 包含 message、stack trace（字串化）、觸發位置</li>
</ul>
<p>轉換層是每個平台 SDK 唯一的平台特定程式碼。轉換完成後，事件進入和手動上報相同的 buffer → flush 管線。</p>
<h2 id="和手動上報的分工">和手動上報的分工</h2>
<p>自動攔截處理「開發者沒有預期到的錯誤」— 未捕獲的例外、未處理的 rejection。手動上報（<code>Monitor.error()</code>）處理「開發者知道可能發生但想記錄的錯誤」— 已捕獲的例外、業務邏輯的異常狀態。</p>
<p>兩者進入同一個 buffer 和 flush 管線，在 collector 端可以用 data 中的 <code>source: &quot;auto&quot;</code> / <code>source: &quot;manual&quot;</code> 欄位區分。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<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/05-platform-adaptation/" data-link-title="模組五：平台適配" data-link-desc="JS CORS / Flutter isolate / Python GIL / Go graceful shutdown — 各平台的特殊考量">模組五 平台適配</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>主動感測器設計（和被動攔截互補）→ <a href="/blog/monitoring/03-sdk-design/frontend-sensor-design/" data-link-title="前端感測器設計" data-link-desc="什麼行為值得埋感測器、每類感測器的實作方式、取樣策略和效能影響 — 和 auto-intercept 的被動攔截互補">前端感測器設計</a></li>
</ul>
]]></content:encoded></item><item><title>平台遷移</title><link>https://tarrragon.github.io/blog/infra/upgrade/platform-migration/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/upgrade/platform-migration/</guid><description>&lt;p>平台遷移改變的是系統跑在哪裡，不是系統跑什麼。應用程式碼不動，改變的是網路拓樸、儲存位置、運算環境與存取方式。遷移成功的判準是應用程式在新平台上以等同或更好的效能運作，且舊平台可以被安全退役。&lt;/p>
&lt;p>遷移的核心約束是帶電施工——系統在搬遷過程中要持續服務。這決定了操作模式：在新平台建起平行環境、驗證通過後用 DNS 切換流量、確認沒問題再拆舊環境。每一步都保留回退到舊環境的能力，直到新環境穩定運行一段時間。&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;th>主要變動&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>共享主機 → VPS&lt;/td>
 &lt;td>SSH、cron 彈性、自訂軟體安裝&lt;/td>
 &lt;td>主機商代管的面板、email、自動備份&lt;/td>
 &lt;td>需要自己管 OS、web server、SSL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>VPS → 雲端&lt;/td>
 &lt;td>Auto-scaling、managed DB、IaC、多 AZ&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;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每條路徑的遷移工程量級不同：共享主機 → VPS 是最輕的（應用層搬家）、地端 → 雲端是最重的（整個基礎設施重建）。選擇遷移路徑時先確認商業目標——如果目標是「能裝自訂軟體」，共享主機 → VPS 就夠了，不需要一步跳到雲端。&lt;/p>
&lt;h2 id="共享主機--vps-遷移">共享主機 → VPS 遷移&lt;/h2>
&lt;h3 id="遷移前的記錄">遷移前的記錄&lt;/h3>
&lt;p>把共享主機的所有設定記下來，作為 VPS 上重建的 checklist。需要記錄的項目：&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>PHP 版本與模組&lt;/td>
 &lt;td>&lt;code>phpinfo()&lt;/code> 匯出&lt;/td>
 &lt;td>VPS 上安裝對應版本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cron jobs&lt;/td>
 &lt;td>主機面板截圖或匯出&lt;/td>
 &lt;td>VPS 上重建 crontab&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Email 帳號與轉發規則&lt;/td>
 &lt;td>面板匯出&lt;/td>
 &lt;td>另外處理（見下方）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DNS 記錄（A / CNAME / MX）&lt;/td>
 &lt;td>域名管理介面匯出&lt;/td>
 &lt;td>切換時需要&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SSL 憑證&lt;/td>
 &lt;td>簽發者、到期日&lt;/td>
 &lt;td>VPS 上重新簽發或遷移&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>.htaccess 規則&lt;/td>
 &lt;td>從站台下載&lt;/td>
 &lt;td>轉換成 nginx 設定&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>接手維運模組的&lt;a href="https://tarrragon.github.io/blog/infra/takeover/legacy-ftp-no-ssh/" data-link-title="無 SSH 的 FTP / 面板管理環境接管" data-link-desc="接手一個只有 FTP 和 phpMyAdmin（或 cPanel / Plesk）存取的 PHP 專案：沒有 SSH、沒有 CLI 時，怎麼盤點現況、建立本地開發環境、制定部署與資料庫變更紀律，以及找到升級路徑的切入點">環境設定拍照&lt;/a>有更完整的盤點方法。&lt;/p>
&lt;h3 id="vps-環境建立">VPS 環境建立&lt;/h3>
&lt;p>VPS 上從零安裝 web stack：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># Ubuntu 22.04 為例&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">sudo apt update &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> sudo apt upgrade -y
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1"># Web server&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">sudo apt install nginx -y
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1"># PHP（對齊共享主機的版本）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">sudo apt install php8.1-fpm php8.1-mysql php8.1-curl php8.1-mbstring php8.1-gd php8.1-xml -y
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1"># MySQL&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">sudo apt install mysql-server -y
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1"># SSL（Let&amp;#39;s Encrypt）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">sudo apt install certbot python3-certbot-nginx -y
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">sudo certbot --nginx -d example.com -d www.example.com&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>安裝完成後用 &lt;code>php -m&lt;/code> 比對共享主機的 phpinfo 記錄，確認所有模組都已安裝。缺少的模組用 &lt;code>apt install php8.1-&amp;lt;module&amp;gt;&lt;/code> 補上。&lt;/p>
&lt;h3 id="資料搬移">資料搬移&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 程式碼：從本地 Git repo 部署（不從共享主機直接搬）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">git clone git@github.com:org/site.git /var/www/site
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 資料庫：從備份匯入&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">mysql -u root -p site_db &amp;lt; backup-latest.sql
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 使用者上傳檔案：從共享主機 FTP 下載後 rsync 到 VPS&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">rsync -avz /local/backup/uploads/ user@vps:/var/www/site/uploads/&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="htaccess--nginx-設定轉換">.htaccess → nginx 設定轉換&lt;/h3>
&lt;p>共享主機用 Apache 的 &lt;code>.htaccess&lt;/code>，VPS 如果改用 nginx 需要手動轉換。常見的規則對照：&lt;/p></description><content:encoded><![CDATA[<p>平台遷移改變的是系統跑在哪裡，不是系統跑什麼。應用程式碼不動，改變的是網路拓樸、儲存位置、運算環境與存取方式。遷移成功的判準是應用程式在新平台上以等同或更好的效能運作，且舊平台可以被安全退役。</p>
<p>遷移的核心約束是帶電施工——系統在搬遷過程中要持續服務。這決定了操作模式：在新平台建起平行環境、驗證通過後用 DNS 切換流量、確認沒問題再拆舊環境。每一步都保留回退到舊環境的能力，直到新環境穩定運行一段時間。</p>
<h2 id="遷移路徑的常見組合">遷移路徑的常見組合</h2>
<table>
  <thead>
      <tr>
          <th>路徑</th>
          <th>獲得</th>
          <th>失去</th>
          <th>主要變動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>共享主機 → VPS</td>
          <td>SSH、cron 彈性、自訂軟體安裝</td>
          <td>主機商代管的面板、email、自動備份</td>
          <td>需要自己管 OS、web server、SSL</td>
      </tr>
      <tr>
          <td>VPS → 雲端</td>
          <td>Auto-scaling、managed DB、IaC、多 AZ</td>
          <td>固定月費的簡單計費</td>
          <td>計費模型改按用量、運維複雜度上升</td>
      </tr>
      <tr>
          <td>地端 → 雲端</td>
          <td>彈性擴縮、不管硬體</td>
          <td>對硬體的直接控制</td>
          <td>網路重新設計、合規審查、資料主權確認</td>
      </tr>
  </tbody>
</table>
<p>每條路徑的遷移工程量級不同：共享主機 → VPS 是最輕的（應用層搬家）、地端 → 雲端是最重的（整個基礎設施重建）。選擇遷移路徑時先確認商業目標——如果目標是「能裝自訂軟體」，共享主機 → VPS 就夠了，不需要一步跳到雲端。</p>
<h2 id="共享主機--vps-遷移">共享主機 → VPS 遷移</h2>
<h3 id="遷移前的記錄">遷移前的記錄</h3>
<p>把共享主機的所有設定記下來，作為 VPS 上重建的 checklist。需要記錄的項目：</p>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>記錄方式</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PHP 版本與模組</td>
          <td><code>phpinfo()</code> 匯出</td>
          <td>VPS 上安裝對應版本</td>
      </tr>
      <tr>
          <td>Cron jobs</td>
          <td>主機面板截圖或匯出</td>
          <td>VPS 上重建 crontab</td>
      </tr>
      <tr>
          <td>Email 帳號與轉發規則</td>
          <td>面板匯出</td>
          <td>另外處理（見下方）</td>
      </tr>
      <tr>
          <td>DNS 記錄（A / CNAME / MX）</td>
          <td>域名管理介面匯出</td>
          <td>切換時需要</td>
      </tr>
      <tr>
          <td>SSL 憑證</td>
          <td>簽發者、到期日</td>
          <td>VPS 上重新簽發或遷移</td>
      </tr>
      <tr>
          <td>.htaccess 規則</td>
          <td>從站台下載</td>
          <td>轉換成 nginx 設定</td>
      </tr>
  </tbody>
</table>
<p>接手維運模組的<a href="/blog/infra/takeover/legacy-ftp-no-ssh/" data-link-title="無 SSH 的 FTP / 面板管理環境接管" data-link-desc="接手一個只有 FTP 和 phpMyAdmin（或 cPanel / Plesk）存取的 PHP 專案：沒有 SSH、沒有 CLI 時，怎麼盤點現況、建立本地開發環境、制定部署與資料庫變更紀律，以及找到升級路徑的切入點">環境設定拍照</a>有更完整的盤點方法。</p>
<h3 id="vps-環境建立">VPS 環境建立</h3>
<p>VPS 上從零安裝 web stack：</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"># Ubuntu 22.04 為例</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">sudo apt update <span class="o">&amp;&amp;</span> sudo apt upgrade -y
</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"># Web server</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">sudo apt install nginx -y
</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"># PHP（對齊共享主機的版本）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">sudo apt install php8.1-fpm php8.1-mysql php8.1-curl php8.1-mbstring php8.1-gd php8.1-xml -y
</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"># MySQL</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">sudo apt install mysql-server -y
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># SSL（Let&#39;s Encrypt）</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">sudo apt install certbot python3-certbot-nginx -y
</span></span><span class="line"><span class="ln">15</span><span class="cl">sudo certbot --nginx -d example.com -d www.example.com</span></span></code></pre></div><p>安裝完成後用 <code>php -m</code> 比對共享主機的 phpinfo 記錄，確認所有模組都已安裝。缺少的模組用 <code>apt install php8.1-&lt;module&gt;</code> 補上。</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"># 程式碼：從本地 Git repo 部署（不從共享主機直接搬）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">git clone git@github.com:org/site.git /var/www/site
</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"># 資料庫：從備份匯入</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">mysql -u root -p site_db &lt; backup-latest.sql
</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"># 使用者上傳檔案：從共享主機 FTP 下載後 rsync 到 VPS</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">rsync -avz /local/backup/uploads/ user@vps:/var/www/site/uploads/</span></span></code></pre></div><h3 id="htaccess--nginx-設定轉換">.htaccess → nginx 設定轉換</h3>
<p>共享主機用 Apache 的 <code>.htaccess</code>，VPS 如果改用 nginx 需要手動轉換。常見的規則對照：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># .htaccess: RewriteEngine On / RewriteRule ^(.*)$ index.php/$1
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># nginx 等價：
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="k">location</span> <span class="s">/</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="nv">$uri/</span> <span class="s">/index.php?</span><span class="nv">$query_string</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># .htaccess: Options -Indexes
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># nginx 等價：
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="k">autoindex</span> <span class="no">off</span><span class="p">;</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"># .htaccess: deny from all (某目錄)
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># nginx 等價：
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span><span class="k">location</span> <span class="p">~</span> <span class="sr">/\.env</span> <span class="p">{</span> <span class="kn">deny</span> <span class="s">all</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>轉換後在本地或 staging 驗證每條規則的行為是否一致。WordPress、Laravel 等框架有現成的 nginx 設定範例可參考。</p>
<h3 id="email-處理">Email 處理</h3>
<p>共享主機通常附帶 email 服務（用主機面板建 email 帳號）。VPS 預設不含 email。三個處理方式：</p>
<ul>
<li>自架 email server（Postfix + Dovecot）：維運成本高、不推薦除非有特殊需求</li>
<li>改用第三方 email 服務（Google Workspace / Zoho Mail）：設定 MX 記錄指向服務商</li>
<li>只轉發（不收信）：應用程式的寄信功能改用 SMTP relay（SendGrid / Mailgun）</li>
</ul>
<p>DNS 的 MX 記錄要在切換前就改好指向新的 email 服務，否則切換後 email 會中斷。</p>
<h3 id="ssl-自動續期">SSL 自動續期</h3>
<p>共享主機的 SSL 通常由主機商代管續期。VPS 上用 Let&rsquo;s Encrypt 的 certbot 會自動設定 systemd timer 或 cron 做續期，但要驗證它確實在跑：</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"># 確認 certbot 的自動續期排程存在</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo systemctl list-timers <span class="p">|</span> grep certbot
</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"># 模擬續期測試（不實際續期）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">sudo certbot renew --dry-run</span></span></code></pre></div><h2 id="vps--雲端遷移">VPS → 雲端遷移</h2>
<h3 id="服務盤點與雲端對照">服務盤點與雲端對照</h3>
<p>VPS 上的每個 process 都需要對應到雲端的服務：</p>
<table>
  <thead>
      <tr>
          <th>VPS 上的角色</th>
          <th>雲端對應</th>
          <th>備註</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>nginx + PHP-FPM</td>
          <td>ECS Fargate / EC2 + ALB</td>
          <td>容器化或直接搬</td>
      </tr>
      <tr>
          <td>MySQL</td>
          <td>RDS</td>
          <td>managed DB、自動備份</td>
      </tr>
      <tr>
          <td>cron jobs</td>
          <td>EventBridge + Lambda / ECS task</td>
          <td>排程觸發的獨立 task</td>
      </tr>
      <tr>
          <td>背景 worker</td>
          <td>ECS service / SQS + Lambda</td>
          <td>依工作模式選型</td>
      </tr>
      <tr>
          <td>檔案儲存</td>
          <td>S3 + CloudFront</td>
          <td>上傳檔案搬到物件儲存</td>
      </tr>
  </tbody>
</table>
<h3 id="自動化遷移工具">自動化遷移工具</h3>
<p>AWS Application Migration Service（MGN）可以自動化 VM workload 的搬遷——把現有 server 的 block-level data 持續複製到 AWS、切換時啟動 EC2 instance。適合大量 VM 的 lift-and-shift，但不處理應用層的重構（nginx config、cron 轉 EventBridge 等仍需手動）。單台 VM 的遷移用 MGN 反而比手動 dump/restore 多一層設定成本，適用場景是同時搬 5 台以上。</p>
<h3 id="iac-的導入時機">IaC 的導入時機</h3>
<p>VPS → 雲端是導入 IaC 的最佳時機——新環境從零建起，沒有歷史包袱。用 Terraform 描述 VPC、subnet、RDS、ECS、ALB 等資源，讓新環境可重現（見<a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一：最小可行 IaC</a>）。遷移完成後，這套 IaC 直接成為持續維運的基礎。</p>
<h3 id="資料庫遷移">資料庫遷移</h3>
<p>小型資料庫（&lt; 10GB）：mysqldump + 匯入 RDS，遷移期間短暫唯讀即可。</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"># 從 VPS dump</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">mysqldump -u user -p --single-transaction site_db <span class="p">|</span> gzip &gt; site_db.sql.gz
</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"># 匯入 RDS</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">gunzip -c site_db.sql.gz <span class="p">|</span> mysql -h rds-endpoint.region.rds.amazonaws.com -u admin -p site_db</span></span></code></pre></div><p>大型資料庫（&gt; 10GB 或需要零停機）：使用 AWS DMS（Database Migration Service）做持續複寫，VPS 上的 MySQL 作為 source、RDS 作為 target，DMS 做初始全量複製後持續同步增量，切換時把應用指向 RDS 端點。</p>
<h3 id="網路設計">網路設計</h3>
<p>雲端環境的網路要在遷移前規劃好。VPC、subnet、security group 的設計見<a href="/blog/infra/03-network-foundation/" data-link-title="模組三：網路地基 — VPC 與分層" data-link-desc="VPC、public / private subnet 切分、route table、NAT、security group 設計">模組三：網路地基</a>。VPS 上的 iptables 規則要映射成 security group 規則——iptables 的每條 accept 對應一條 SG ingress rule，但 SG 不支援 deny（用「不開就是 deny」的白名單模式）。</p>
<h2 id="資料同步策略">資料同步策略</h2>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>停機時間</th>
          <th>複雜度</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一次性 dump + restore</td>
          <td>分鐘到小時級</td>
          <td>低</td>
          <td>資料 &lt; 10GB、可接受維護窗口</td>
      </tr>
      <tr>
          <td>持續複寫（DMS / 邏輯複寫）</td>
          <td>秒級（切換瞬間）</td>
          <td>高</td>
          <td>資料大、不允許停機</td>
      </tr>
      <tr>
          <td>檔案 rsync 增量同步</td>
          <td>取決於差異量</td>
          <td>低</td>
          <td>靜態檔案、上傳內容</td>
      </tr>
  </tbody>
</table>
<p>選擇策略時先問兩個問題：資料量多大（決定 dump 時間）、業務能接受多長的唯讀或停機窗口（決定要不要持續複寫）。</p>
<p>對於上傳檔案（圖片、文件），遷移到雲端時通常從本地檔案系統搬到 S3：</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"># 從 VPS 同步上傳目錄到 S3</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws s3 sync /var/www/site/uploads/ s3://site-uploads/ --delete</span></span></code></pre></div><p>應用程式碼裡的檔案路徑要改成 S3 URL 或用 CDN 代理。</p>
<h2 id="dns-切換與驗證">DNS 切換與驗證</h2>
<h3 id="切換前準備">切換前準備</h3>
<p>遷移前 48 小時，降低 DNS TTL 到 300 秒（5 分鐘）。正常的 TTL 通常是 3600 秒（1 小時）或更長——如果切換出問題需要回退，短 TTL 讓 DNS 傳播更快。</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"># 確認當前 TTL</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">dig example.com +short +ttlid</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"># 更新 A record 指向新平台的 IP / ALB endpoint</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 如果用 Route 53：</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">aws route53 change-resource-record-sets --hosted-zone-id Z123 --change-batch <span class="s1">&#39;{
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s1">  &#34;Changes&#34;: [{&#34;Action&#34;: &#34;UPSERT&#34;, &#34;ResourceRecordSet&#34;: {
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s1">    &#34;Name&#34;: &#34;example.com&#34;, &#34;Type&#34;: &#34;A&#34;,
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s1">    &#34;AliasTarget&#34;: {&#34;HostedZoneId&#34;: &#34;Z456&#34;, &#34;DNSName&#34;: &#34;alb-xxx.region.elb.amazonaws.com&#34;, &#34;EvaluateTargetHealth&#34;: true}
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="s1">  }}]
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="s1">}&#39;</span></span></span></code></pre></div><h3 id="切換後監控">切換後監控</h3>
<p>切換後的驗證窗口至少等 2 倍 TTL（短 TTL 設 300 秒的話，至少等 10 分鐘）。在這段時間內：</p>
<ul>
<li>新平台：監控 HTTP 狀態碼、回應時間、錯誤率</li>
<li>舊平台：觀察流量是否遞減到零（仍有流量代表 DNS 還沒完全傳播）</li>
<li>功能驗證：跑一次關鍵流程（登入、查詢、交易）</li>
</ul>
<h3 id="回退">回退</h3>
<p>如果新平台出問題，回退方式是把 DNS 切回舊平台的 IP。回退的生效時間等於當前的 TTL——這正是切換前降低 TTL 的理由。舊平台在 DNS 切換後要保留至少 72 小時（全球 DNS 快取最慢的清除時間），確認完全沒有流量後再退役。</p>
<h3 id="切換後收尾">切換後收尾</h3>
<p>穩定運行 1-2 週後：</p>
<ul>
<li>把 DNS TTL 恢復到正常值（3600 秒）</li>
<li>退役舊平台（關機 → 保留快照 → 一個月後刪除）</li>
<li>更新文件：新環境的存取方式、部署流程、監控端點</li>
</ul>
<h2 id="時程與管理層溝通">時程與管理層溝通</h2>
<table>
  <thead>
      <tr>
          <th>遷移路徑</th>
          <th>典型時程</th>
          <th>主要風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>共享主機 → VPS</td>
          <td>1-2 週</td>
          <td>.htaccess 轉換、email 處理、SSL 續期</td>
      </tr>
      <tr>
          <td>VPS → 雲端</td>
          <td>2-4 週</td>
          <td>資料庫遷移、網路設計、IaC 建立</td>
      </tr>
      <tr>
          <td>地端 → 雲端</td>
          <td>4-8 週</td>
          <td>網路重建、合規審查、資料主權</td>
      </tr>
  </tbody>
</table>
<p>向管理層溝通時的關鍵訊息：「應用程式碼不變、改的是運行環境。風險集中在資料搬移和 DNS 切換這兩個步驟，兩者都有回退路徑。」</p>
<p>成本變化也要提前說明：共享主機 → VPS 的月費通常持平或略增（$5-30/月）；VPS → 雲端的月費取決於資源用量，初期可能增加 50-200%（換到的是彈性和 managed 服務），但可以透過 reserved instance 和 rightsizing 後續優化。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/upgrade/upgrade-framework/" data-link-title="升級的共通操作框架" data-link-desc="任何環境或系統升級的四階段模型：差異評估、平行環境驗證、分批切換、退役舊環境，以及貫穿全程的升級紀律">升級的共通操作框架</a>：評估差異 → 平行環境 → 切換 → 退役的四階段模型</li>
<li>→ <a href="/blog/infra/takeover/legacy-ftp-no-ssh/" data-link-title="無 SSH 的 FTP / 面板管理環境接管" data-link-desc="接手一個只有 FTP 和 phpMyAdmin（或 cPanel / Plesk）存取的 PHP 專案：沒有 SSH、沒有 CLI 時，怎麼盤點現況、建立本地開發環境、制定部署與資料庫變更紀律，以及找到升級路徑的切入點">接手維運：無 SSH 的 FTP 環境</a>：遷移前的環境盤點方法</li>
<li>→ <a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一：最小可行 IaC</a>：雲端遷移是導入 IaC 的最佳時機</li>
<li>→ <a href="/blog/infra/03-network-foundation/" data-link-title="模組三：網路地基 — VPC 與分層" data-link-desc="VPC、public / private subnet 切分、route table、NAT、security group 設計">模組三：網路地基</a>：雲端環境的 VPC / subnet 設計</li>
</ul>
]]></content:encoded></item><item><title>Python 平台適配</title><link>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/python-platform/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/python-platform/</guid><description>&lt;p>Python 的執行模型（GIL 限制並行、atexit 不保證執行、subprocess 獨立 process）讓監控 SDK 在 Python 環境中需要特別處理 flush 的執行方式、程序退出時的事件保存和子程序的監控。&lt;/p>
&lt;h2 id="gil-與-threading">GIL 與 threading&lt;/h2>
&lt;p>Python 的 Global Interpreter Lock（GIL）讓同一時間只有一個 thread 執行 Python bytecode。SDK 的 flush 操作（HTTP POST 到 collector）如果在主 thread 執行，會阻塞主程式的其他工作。&lt;/p>
&lt;p>SDK 端的適配：&lt;/p>
&lt;p>在 daemon thread 中執行 flush。Daemon thread 在主 thread 結束時自動終止，不需要手動 join。SDK 的 flush 計時器在 daemon thread 中運行，buffer 的存取用 threading.Lock 保護。&lt;/p>
&lt;p>GIL 對 SDK 的影響比想像的小：HTTP 請求是 I/O bound 操作，CPython 在等待 I/O 時釋放 GIL。所以 flush 的 HTTP POST 在 daemon thread 中執行時，主 thread 可以繼續工作。GIL 只在 CPU-bound 的操作上造成瓶頸 — SDK 的 buffer 操作和事件序列化是 CPU-bound 但耗時極短（微秒級），影響可忽略。&lt;/p>
&lt;h3 id="asyncio-環境">asyncio 環境&lt;/h3>
&lt;p>Python 的 asyncio 程式（FastAPI、aiohttp）使用事件迴圈而非 threading。SDK 在 asyncio 環境中應該用 &lt;code>asyncio.create_task&lt;/code> 而非 threading 執行 flush，避免在事件迴圈中阻塞。&lt;/p>
&lt;p>SDK 可以在 init 時自動偵測是否在 asyncio 環境中（檢查 &lt;code>asyncio.get_running_loop()&lt;/code> 是否存在），自動切換 flush 的執行方式。&lt;/p>
&lt;h2 id="atexit-可靠性">atexit 可靠性&lt;/h2>
&lt;p>&lt;code>atexit.register&lt;/code> 在 Python 程序正常退出時執行註冊的清理函式。SDK 在 init 時註冊 atexit handler 做最後一次 flush。&lt;/p>
&lt;p>atexit 不執行的場景：&lt;/p>
&lt;ul>
&lt;li>&lt;code>os._exit()&lt;/code> 直接終止 process，跳過所有清理&lt;/li>
&lt;li>SIGKILL（&lt;code>kill -9&lt;/code>）強制終止，作業系統直接回收 process&lt;/li>
&lt;li>未處理的 fatal signal（SIGSEGV、SIGABRT）導致 crash&lt;/li>
&lt;/ul>
&lt;p>對於 SIGTERM 和 SIGINT，Python 預設會執行 atexit handler（前提是 signal handler 沒有被覆蓋）。SDK 可以額外註冊 &lt;code>signal.signal(signal.SIGTERM, handler)&lt;/code> 確保在收到 SIGTERM 時觸發 flush。&lt;/p>
&lt;p>實務影響：&lt;code>os._exit()&lt;/code> 和 SIGKILL 導致的事件遺失無法避免。使用本地 persistence（&lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/offline-buffer/" data-link-title="離線 buffer 與重試" data-link-desc="網路不可用時的事件保存策略 — FIFO 丟棄、本地 persistence、恢復後補發的取捨">離線 buffer&lt;/a>）可以降低影響 — 事件在寫入本地檔案後，即使 process 被強制終止，下次啟動時仍可補發。&lt;/p>
&lt;h2 id="短生命週期腳本">短生命週期腳本&lt;/h2>
&lt;p>SDK 的預設設計假設長期運行的 app — flush interval 定期觸發、daemon thread 持續運行、atexit 是最後防線。但 Python SDK 的一個重要場景是短命腳本（CI/CD hook、pre-commit hook、CLI 工具的子命令），生命週期可能 &amp;lt; 1 秒。這個場景下 SDK 的行為和長期 app 完全不同。&lt;/p>
&lt;h3 id="什麼會壞">什麼會壞&lt;/h3>
&lt;p>&lt;strong>flush interval 來不及觸發&lt;/strong>。預設 30 秒的 flush interval，但腳本在 200ms 內結束。計時器還沒觸發，buffer 中的事件從未送出。&lt;/p></description><content:encoded><![CDATA[<p>Python 的執行模型（GIL 限制並行、atexit 不保證執行、subprocess 獨立 process）讓監控 SDK 在 Python 環境中需要特別處理 flush 的執行方式、程序退出時的事件保存和子程序的監控。</p>
<h2 id="gil-與-threading">GIL 與 threading</h2>
<p>Python 的 Global Interpreter Lock（GIL）讓同一時間只有一個 thread 執行 Python bytecode。SDK 的 flush 操作（HTTP POST 到 collector）如果在主 thread 執行，會阻塞主程式的其他工作。</p>
<p>SDK 端的適配：</p>
<p>在 daemon thread 中執行 flush。Daemon thread 在主 thread 結束時自動終止，不需要手動 join。SDK 的 flush 計時器在 daemon thread 中運行，buffer 的存取用 threading.Lock 保護。</p>
<p>GIL 對 SDK 的影響比想像的小：HTTP 請求是 I/O bound 操作，CPython 在等待 I/O 時釋放 GIL。所以 flush 的 HTTP POST 在 daemon thread 中執行時，主 thread 可以繼續工作。GIL 只在 CPU-bound 的操作上造成瓶頸 — SDK 的 buffer 操作和事件序列化是 CPU-bound 但耗時極短（微秒級），影響可忽略。</p>
<h3 id="asyncio-環境">asyncio 環境</h3>
<p>Python 的 asyncio 程式（FastAPI、aiohttp）使用事件迴圈而非 threading。SDK 在 asyncio 環境中應該用 <code>asyncio.create_task</code> 而非 threading 執行 flush，避免在事件迴圈中阻塞。</p>
<p>SDK 可以在 init 時自動偵測是否在 asyncio 環境中（檢查 <code>asyncio.get_running_loop()</code> 是否存在），自動切換 flush 的執行方式。</p>
<h2 id="atexit-可靠性">atexit 可靠性</h2>
<p><code>atexit.register</code> 在 Python 程序正常退出時執行註冊的清理函式。SDK 在 init 時註冊 atexit handler 做最後一次 flush。</p>
<p>atexit 不執行的場景：</p>
<ul>
<li><code>os._exit()</code> 直接終止 process，跳過所有清理</li>
<li>SIGKILL（<code>kill -9</code>）強制終止，作業系統直接回收 process</li>
<li>未處理的 fatal signal（SIGSEGV、SIGABRT）導致 crash</li>
</ul>
<p>對於 SIGTERM 和 SIGINT，Python 預設會執行 atexit handler（前提是 signal handler 沒有被覆蓋）。SDK 可以額外註冊 <code>signal.signal(signal.SIGTERM, handler)</code> 確保在收到 SIGTERM 時觸發 flush。</p>
<p>實務影響：<code>os._exit()</code> 和 SIGKILL 導致的事件遺失無法避免。使用本地 persistence（<a href="/blog/monitoring/03-sdk-design/offline-buffer/" data-link-title="離線 buffer 與重試" data-link-desc="網路不可用時的事件保存策略 — FIFO 丟棄、本地 persistence、恢復後補發的取捨">離線 buffer</a>）可以降低影響 — 事件在寫入本地檔案後，即使 process 被強制終止，下次啟動時仍可補發。</p>
<h2 id="短生命週期腳本">短生命週期腳本</h2>
<p>SDK 的預設設計假設長期運行的 app — flush interval 定期觸發、daemon thread 持續運行、atexit 是最後防線。但 Python SDK 的一個重要場景是短命腳本（CI/CD hook、pre-commit hook、CLI 工具的子命令），生命週期可能 &lt; 1 秒。這個場景下 SDK 的行為和長期 app 完全不同。</p>
<h3 id="什麼會壞">什麼會壞</h3>
<p><strong>flush interval 來不及觸發</strong>。預設 30 秒的 flush interval，但腳本在 200ms 內結束。計時器還沒觸發，buffer 中的事件從未送出。</p>
<p><strong>daemon thread 隨主 thread 結束</strong>。SDK 用 daemon thread 執行 flush 計時器。Python 的 daemon thread 在最後一個非 daemon thread 結束時被殺 — 不會等待 daemon thread 完成當前工作。如果 flush 正在進行中（HTTP POST 送到一半），daemon thread 被殺，HTTP 請求中斷，事件丟失。</p>
<p><strong>atexit 的執行順序不確定</strong>。atexit handler 在 daemon thread 被殺之後執行。如果 SDK 的 atexit handler 嘗試在 daemon thread 中 flush，會失敗（thread 已死）。atexit handler 必須在主 thread 中同步 flush。</p>
<h3 id="正確的短命腳本模式">正確的短命腳本模式</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="kn">from</span> <span class="nn">monitor</span> <span class="kn">import</span> <span class="n">Monitor</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">Monitor</span><span class="o">.</span><span class="n">init</span><span class="p">(</span><span class="n">endpoint</span><span class="o">=</span><span class="s2">&#34;http://localhost:9090/v1/events&#34;</span><span class="p">,</span> <span class="n">app</span><span class="o">=</span><span class="s2">&#34;my-hook&#34;</span><span class="p">)</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"># 做事...</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">Monitor</span><span class="o">.</span><span class="n">event</span><span class="p">(</span><span class="s2">&#34;hook.run&#34;</span><span class="p">,</span> <span class="p">{</span><span class="s2">&#34;hook&#34;</span><span class="p">:</span> <span class="s2">&#34;branch-check&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># 結束前必須呼叫 close</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="n">Monitor</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>  <span class="c1"># close 內同步 flush，不依賴 daemon thread</span></span></span></code></pre></div><p><code>close()</code> 是唯一可靠的 flush 時機。<code>close()</code> 的實作在短命腳本場景下必須：</p>
<ol>
<li><strong>同步執行 HTTP POST</strong>，不委託給 daemon thread — 主 thread 呼叫 <code>close()</code> 時直接在當前 thread 送出</li>
<li><strong>設 HTTP timeout</strong> — 短命腳本不能等太久，3 秒的 timeout 是合理的</li>
<li><strong>flush 失敗時靜默放棄</strong> — 短命腳本的主要職責不是監控，SDK 失敗不應影響腳本的 exit code</li>
</ol>
<p><code>atexit</code> 仍然註冊，作為開發者忘記呼叫 <code>close()</code> 的備份。但 atexit 是 best-effort — 在 <code>os._exit()</code> 和 SIGKILL 下不執行。</p>
<h3 id="flush-interval-在短命腳本中的角色">flush interval 在短命腳本中的角色</h3>
<p>flush interval 對短命腳本無意義 — 腳本在第一次 interval 觸發前就結束了。SDK 可以偵測「init 到 close 的間隔 &lt; flush interval」的模式，在 debug log 中提示開發者考慮降低 interval 或直接依賴 <code>close()</code> flush。</p>
<p>但不建議把 flush interval 設為 0（停用）— 同一個 SDK 設定可能同時用於長期 app 和短命腳本，interval 對長期 app 仍然有用。</p>
<h2 id="subprocess-監控">Subprocess 監控</h2>
<p>Python 程式中的 <code>subprocess.Popen</code> 啟動的子程序是獨立的 process，不共享 SDK 的 buffer 和網路連線。子程序的錯誤和事件需要獨立的監控機制。</p>
<p>兩種方式：</p>
<p><strong>子程序獨立初始化 SDK</strong>：子程序的 Python 腳本自己呼叫 <code>Monitor.init()</code>，獨立送事件到 collector。適合子程序是長時間運行的 Python 程式。</p>
<p><strong>父程序代理</strong>：父程序讀取子程序的 stdout/stderr，從輸出中解析事件（子程序用約定格式印出事件），父程序的 SDK 代理送出。適合子程序是短命的腳本或非 Python 程式。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Go 平台的適配 → <a href="/blog/monitoring/05-platform-adaptation/go-platform/" data-link-title="Go 平台適配" data-link-desc="Graceful shutdown、signal handling、HTTP server 自身監控 — Go SDK 和 collector 端共同面對的平台問題">Go 平台適配</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>離線 buffer 策略 → <a href="/blog/monitoring/03-sdk-design/offline-buffer/" data-link-title="離線 buffer 與重試" data-link-desc="網路不可用時的事件保存策略 — FIFO 丟棄、本地 persistence、恢復後補發的取捨">模組三 離線 buffer 與重試</a></li>
</ul>
]]></content:encoded></item><item><title>Go 平台適配</title><link>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/go-platform/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/go-platform/</guid><description>&lt;p>Go 的 monitoring SDK 和其他平台 SDK 的定位不同。JS / Flutter / Python SDK 是 client-side 的事件上報工具，Go SDK 更常用在 server-side — 包括 collector 本身的自身監控。Go 的 goroutine 並行模型、signal handling 機制和 HTTP server 的 graceful shutdown 是 Go 環境中的三個核心適配問題。&lt;/p>
&lt;h2 id="graceful-shutdown">Graceful shutdown&lt;/h2>
&lt;p>Go 程式收到 SIGTERM 或 SIGINT 時需要在退出前完成清理：flush 剩餘的 buffer、關閉網路連線、寫入最後的 lifecycle 事件。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">stop&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">signal&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NotifyContext&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Background&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="nx">syscall&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SIGTERM&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">syscall&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SIGINT&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">defer&lt;/span> &lt;span class="nf">stop&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Done&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">// signal received, start graceful shutdown&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="nx">monitor&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Close&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WithTimeout&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Background&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&lt;/span>&lt;span class="p">))&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>graceful shutdown 的時間窗口由部署環境決定。Kubernetes 的預設 terminationGracePeriodSeconds 是 30 秒，Docker 的 stop timeout 是 10 秒。SDK 的 Close 方法接受 context 讓呼叫端控制超時。&lt;/p>
&lt;h3 id="http-server-的-shutdown-順序">HTTP server 的 shutdown 順序&lt;/h3>
&lt;p>如果 Go 程式同時是 HTTP server 和 monitoring SDK 的使用者，shutdown 順序需要正確：&lt;/p>
&lt;ol>
&lt;li>停止接受新連線（&lt;code>server.Shutdown(ctx)&lt;/code>）&lt;/li>
&lt;li>等待進行中的請求完成&lt;/li>
&lt;li>flush 監控 buffer（&lt;code>monitor.Close(ctx)&lt;/code>）&lt;/li>
&lt;li>關閉 log 和其他資源&lt;/li>
&lt;/ol>
&lt;p>如果先 close monitor 再 shutdown server，進行中的請求產生的事件會在 monitor 已關閉後嘗試送出，被靜默丟棄。&lt;/p>
&lt;h2 id="signal-handling">Signal handling&lt;/h2>
&lt;p>Go 的 &lt;code>signal.Notify&lt;/code> 和 &lt;code>signal.NotifyContext&lt;/code> 是接收 OS signal 的標準方式。SDK 在 init 時不應該自己註冊 signal handler — 這會和應用程式的 signal handling 衝突（Go 的 signal handler 是先到先得，後註冊的覆蓋先註冊的）。&lt;/p>
&lt;p>SDK 端的適配方式是提供 &lt;code>Close&lt;/code> 方法讓應用程式在自己的 signal handler 中呼叫，而非 SDK 內部攔截 signal。應用程式控制 shutdown 流程，SDK 只負責在被告知關閉時 flush 和清理。&lt;/p>
&lt;h3 id="panic-recovery">panic recovery&lt;/h3>
&lt;p>Go 的 panic 會終止當前 goroutine。如果 panic 發生在 main goroutine 且沒有 recover，程式直接退出，SDK 的 buffer 中的事件遺失。&lt;/p>
&lt;p>SDK 可以提供 &lt;code>monitor.RecoverAndReport()&lt;/code> 讓開發者在 goroutine 的入口用 &lt;code>defer monitor.RecoverAndReport()&lt;/code> 攔截 panic，記錄 error 事件後再 re-panic（保持原有的 crash 行為）。&lt;/p>
&lt;p>HTTP handler 的 panic 可以用 middleware 攔截：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">monitorMiddleware&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">next&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Handler&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Handler&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">HandlerFunc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ResponseWriter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Request&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">monitor&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RecoverAndReport&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">next&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">ServeHTTP&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="http-server-自身監控">HTTP server 自身監控&lt;/h2>
&lt;p>Go 常用來寫 collector 本身。Collector 需要監控自己的健康狀態 — 請求處理速率、錯誤率、goroutine 數量、記憶體使用量。&lt;/p></description><content:encoded><![CDATA[<p>Go 的 monitoring SDK 和其他平台 SDK 的定位不同。JS / Flutter / Python SDK 是 client-side 的事件上報工具，Go SDK 更常用在 server-side — 包括 collector 本身的自身監控。Go 的 goroutine 並行模型、signal handling 機制和 HTTP server 的 graceful shutdown 是 Go 環境中的三個核心適配問題。</p>
<h2 id="graceful-shutdown">Graceful shutdown</h2>
<p>Go 程式收到 SIGTERM 或 SIGINT 時需要在退出前完成清理：flush 剩餘的 buffer、關閉網路連線、寫入最後的 lifecycle 事件。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">ctx</span><span class="p">,</span> <span class="nx">stop</span> <span class="o">:=</span> <span class="nx">signal</span><span class="p">.</span><span class="nf">NotifyContext</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="nx">syscall</span><span class="p">.</span><span class="nx">SIGTERM</span><span class="p">,</span> <span class="nx">syscall</span><span class="p">.</span><span class="nx">SIGINT</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">defer</span> <span class="nf">stop</span><span class="p">()</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="o">&lt;-</span><span class="nx">ctx</span><span class="p">.</span><span class="nf">Done</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// signal received, start graceful shutdown</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">monitor</span><span class="p">.</span><span class="nf">Close</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">WithTimeout</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="mi">5</span><span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">))</span></span></span></code></pre></div><p>graceful shutdown 的時間窗口由部署環境決定。Kubernetes 的預設 terminationGracePeriodSeconds 是 30 秒，Docker 的 stop timeout 是 10 秒。SDK 的 Close 方法接受 context 讓呼叫端控制超時。</p>
<h3 id="http-server-的-shutdown-順序">HTTP server 的 shutdown 順序</h3>
<p>如果 Go 程式同時是 HTTP server 和 monitoring SDK 的使用者，shutdown 順序需要正確：</p>
<ol>
<li>停止接受新連線（<code>server.Shutdown(ctx)</code>）</li>
<li>等待進行中的請求完成</li>
<li>flush 監控 buffer（<code>monitor.Close(ctx)</code>）</li>
<li>關閉 log 和其他資源</li>
</ol>
<p>如果先 close monitor 再 shutdown server，進行中的請求產生的事件會在 monitor 已關閉後嘗試送出，被靜默丟棄。</p>
<h2 id="signal-handling">Signal handling</h2>
<p>Go 的 <code>signal.Notify</code> 和 <code>signal.NotifyContext</code> 是接收 OS signal 的標準方式。SDK 在 init 時不應該自己註冊 signal handler — 這會和應用程式的 signal handling 衝突（Go 的 signal handler 是先到先得，後註冊的覆蓋先註冊的）。</p>
<p>SDK 端的適配方式是提供 <code>Close</code> 方法讓應用程式在自己的 signal handler 中呼叫，而非 SDK 內部攔截 signal。應用程式控制 shutdown 流程，SDK 只負責在被告知關閉時 flush 和清理。</p>
<h3 id="panic-recovery">panic recovery</h3>
<p>Go 的 panic 會終止當前 goroutine。如果 panic 發生在 main goroutine 且沒有 recover，程式直接退出，SDK 的 buffer 中的事件遺失。</p>
<p>SDK 可以提供 <code>monitor.RecoverAndReport()</code> 讓開發者在 goroutine 的入口用 <code>defer monitor.RecoverAndReport()</code> 攔截 panic，記錄 error 事件後再 re-panic（保持原有的 crash 行為）。</p>
<p>HTTP handler 的 panic 可以用 middleware 攔截：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">monitorMiddleware</span><span class="p">(</span><span class="nx">next</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Handler</span><span class="p">)</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Handler</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">return</span> <span class="nx">http</span><span class="p">.</span><span class="nf">HandlerFunc</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="k">defer</span> <span class="nx">monitor</span><span class="p">.</span><span class="nf">RecoverAndReport</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="nx">next</span><span class="p">.</span><span class="nf">ServeHTTP</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h2 id="http-server-自身監控">HTTP server 自身監控</h2>
<p>Go 常用來寫 collector 本身。Collector 需要監控自己的健康狀態 — 請求處理速率、錯誤率、goroutine 數量、記憶體使用量。</p>
<p>Collector 的自身監控和接收外部事件是兩個獨立的管線。自身監控的 metric 可以寫入獨立的 JSONL 檔案（和外部事件分開），或透過 Go 的 <code>expvar</code> / <code>runtime.ReadMemStats</code> 暴露為 HTTP endpoint。</p>
<p>自身監控的關鍵指標：</p>
<ul>
<li><code>collector.events.received</code>：每秒收到的事件數</li>
<li><code>collector.events.invalid</code>：schema 驗證失敗的事件數</li>
<li><code>collector.storage.write_duration_ms</code>：寫入 JSONL 的耗時</li>
<li><code>collector.goroutines</code>：goroutine 數量（洩漏偵測）</li>
<li><code>collector.memory.alloc_mb</code>：記憶體使用量</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<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>Collector 的架構設計 → <a href="/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">模組四 Collector 設計</a></li>
<li>SDK 公開 API 的 Close 方法 → <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>
</ul>
]]></content:encoded></item><item><title>跨平台 timestamp 一致性</title><link>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/cross-platform-timestamp/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/cross-platform-timestamp/</guid><description>&lt;p>跨平台的監控系統收到來自不同平台（JS / Flutter / Python / Go）的事件，每個平台的 timestamp 格式、精度和時鐘來源不同。Collector 需要對這些 timestamp 做排序、分組和時間範圍查詢，一致性問題會導致事件順序錯亂和分析結果偏差。&lt;/p>
&lt;h2 id="統一格式iso-8601--時區偏移">統一格式：ISO 8601 + 時區偏移&lt;/h2>
&lt;p>所有平台的 SDK 統一使用 ISO 8601 格式，包含毫秒精度和時區偏移：&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">2026-06-19T14:30:00.123+08:00&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>避免使用 Unix timestamp（秒或毫秒）作為僅有的時間表示 — Unix timestamp 沒有時區資訊，如果 SDK 端和 collector 端在不同時區，需要額外的 metadata 才能正確轉換。&lt;/p>
&lt;p>避免使用「本地時間不帶時區」的格式（&lt;code>2026-06-19T14:30:00&lt;/code>）— 無法區分 UTC+8 的 14:30 和 UTC+0 的 14:30。&lt;/p>
&lt;h2 id="各平台的-timestamp-來源">各平台的 timestamp 來源&lt;/h2>
&lt;h3 id="javascript">JavaScript&lt;/h3>
&lt;p>&lt;code>Date.now()&lt;/code> 回傳毫秒精度的 Unix timestamp。&lt;code>new Date().toISOString()&lt;/code> 回傳 UTC 時間的 ISO 8601 字串。&lt;/p>
&lt;p>SDK 應該用 &lt;code>Intl.DateTimeFormat&lt;/code> 或手動計算時區偏移，產生帶本地時區的 ISO 8601 字串 — collector 端需要知道事件的本地時間，以便做使用者時區的分析。&lt;/p>
&lt;p>&lt;code>performance.now()&lt;/code> 提供微秒精度的高解析度時間，但起點是頁面載入時間，無法用來產生絕對 timestamp。用於計算 duration（兩個時間點的差值），不用於記錄事件時間。&lt;/p>
&lt;h3 id="flutter--dart">Flutter / Dart&lt;/h3>
&lt;p>&lt;code>DateTime.now()&lt;/code> 回傳本地時間的 DateTime 物件。&lt;code>DateTime.now().toUtc()&lt;/code> 轉成 UTC。&lt;code>DateTime.now().toIso8601String()&lt;/code> 產生 ISO 8601 字串，但不包含時區偏移（Dart 的 ISO 8601 格式不包含 offset）。&lt;/p>
&lt;p>SDK 需要手動附加時區偏移：&lt;code>DateTime.now().timeZoneOffset&lt;/code> 取得偏移量，手動格式化為 &lt;code>+08:00&lt;/code> 格式附加到 ISO 8601 字串後面。&lt;/p>
&lt;h3 id="python">Python&lt;/h3>
&lt;p>&lt;code>datetime.now(timezone.utc)&lt;/code> 取得 UTC 時間。&lt;code>datetime.now().astimezone()&lt;/code> 取得本地時間帶時區。&lt;code>.isoformat()&lt;/code> 產生帶時區偏移的 ISO 8601 字串。&lt;/p>
&lt;p>Python 3.2+ 的 &lt;code>datetime&lt;/code> 原生支援 timezone-aware 的 ISO 8601 輸出，是各平台中最完整的。&lt;/p>
&lt;h3 id="go">Go&lt;/h3>
&lt;p>&lt;code>time.Now()&lt;/code> 回傳帶時區的 Time 值。&lt;code>time.Now().Format(time.RFC3339Milli)&lt;/code> 產生帶毫秒和時區偏移的字串。&lt;/p>
&lt;p>Go 的 &lt;code>time.RFC3339Nano&lt;/code> 提供奈秒精度，但監控事件不需要這個精度 — 毫秒足夠。&lt;/p>
&lt;h2 id="clock-drift">Clock drift&lt;/h2>
&lt;p>不同裝置的系統時鐘可能有偏差（clock drift）。使用者手機的時鐘比 collector server 快 5 分鐘，SDK 產生的 timestamp 會比 collector 收到時間早 5 分鐘。&lt;/p>
&lt;p>Clock drift 的影響：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>排序錯亂&lt;/strong>：裝置 A（時鐘快）和裝置 B（時鐘慢）的事件混合排序時，時間順序可能和真實發生順序不一致&lt;/li>
&lt;li>&lt;strong>告警延遲計算錯誤&lt;/strong>：collector 用「事件 timestamp 到收到時間的差值」計算延遲，clock drift 讓延遲值不準確&lt;/li>
&lt;/ul>
&lt;p>處理策略：&lt;/p>
&lt;p>&lt;strong>Collector 記錄 receive_timestamp&lt;/strong>：每筆事件除了 SDK 端的 timestamp，collector 在收到時附加 &lt;code>receive_timestamp&lt;/code>。兩者的差值用於估算 clock drift 和網路延遲。&lt;/p>
&lt;p>&lt;strong>容忍而非修正&lt;/strong>：在數秒到數分鐘級的 drift 範圍內，容忍 drift 帶來的排序不精確。跨裝置的事件排序本身就不需要毫秒精度 — 分析的粒度通常是秒或分鐘。&lt;/p>
&lt;p>&lt;strong>異常值偵測&lt;/strong>：timestamp 比 receive_timestamp 早超過 1 小時，或晚超過 5 分鐘，標記為可疑的 clock drift — 可能是使用者手動調整了系統時鐘。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>JS 平台適配 → &lt;a href="https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/js-ts-platform/" data-link-title="JS/TS 平台適配" data-link-desc="CORS 限制、Service Worker 攔截、SPA 路由變換偵測 — 瀏覽器環境中 SDK 需要處理的平台特殊問題">JS/TS 平台適配&lt;/a>&lt;/li>
&lt;li>Flutter 平台適配 → &lt;a href="https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/flutter-platform/" data-link-title="Flutter 平台適配" data-link-desc="Isolate 安全、Platform channel 攔截、app lifecycle 事件 — Flutter SDK 的平台特殊考量">Flutter 平台適配&lt;/a>&lt;/li>
&lt;li>Log schema 中的 timestamp 欄位 → &lt;a href="https://tarrragon.github.io/blog/monitoring/02-log-schema/event-schema-fields/" data-link-title="event.schema.json 完整欄位解說" data-link-desc="監控事件的 JSON Schema 定義 — 每個欄位的語意、必填/選填、資料型別和設計理由">模組二 event.schema.json 欄位解說&lt;/a>&lt;/li>
&lt;li>各平台的 error 攔截差異影響 test 設計 → &lt;a href="https://tarrragon.github.io/blog/testing/05-test-design-judgment/" data-link-title="模組五：測試設計判斷" data-link-desc="Mock 邊界判斷、assertion 設計、test data 代表性、flaky test 診斷">testing 模組五 測試設計判斷&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>跨平台的監控系統收到來自不同平台（JS / Flutter / Python / Go）的事件，每個平台的 timestamp 格式、精度和時鐘來源不同。Collector 需要對這些 timestamp 做排序、分組和時間範圍查詢，一致性問題會導致事件順序錯亂和分析結果偏差。</p>
<h2 id="統一格式iso-8601--時區偏移">統一格式：ISO 8601 + 時區偏移</h2>
<p>所有平台的 SDK 統一使用 ISO 8601 格式，包含毫秒精度和時區偏移：</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">2026-06-19T14:30:00.123+08:00</span></span></code></pre></div><p>避免使用 Unix timestamp（秒或毫秒）作為僅有的時間表示 — Unix timestamp 沒有時區資訊，如果 SDK 端和 collector 端在不同時區，需要額外的 metadata 才能正確轉換。</p>
<p>避免使用「本地時間不帶時區」的格式（<code>2026-06-19T14:30:00</code>）— 無法區分 UTC+8 的 14:30 和 UTC+0 的 14:30。</p>
<h2 id="各平台的-timestamp-來源">各平台的 timestamp 來源</h2>
<h3 id="javascript">JavaScript</h3>
<p><code>Date.now()</code> 回傳毫秒精度的 Unix timestamp。<code>new Date().toISOString()</code> 回傳 UTC 時間的 ISO 8601 字串。</p>
<p>SDK 應該用 <code>Intl.DateTimeFormat</code> 或手動計算時區偏移，產生帶本地時區的 ISO 8601 字串 — collector 端需要知道事件的本地時間，以便做使用者時區的分析。</p>
<p><code>performance.now()</code> 提供微秒精度的高解析度時間，但起點是頁面載入時間，無法用來產生絕對 timestamp。用於計算 duration（兩個時間點的差值），不用於記錄事件時間。</p>
<h3 id="flutter--dart">Flutter / Dart</h3>
<p><code>DateTime.now()</code> 回傳本地時間的 DateTime 物件。<code>DateTime.now().toUtc()</code> 轉成 UTC。<code>DateTime.now().toIso8601String()</code> 產生 ISO 8601 字串，但不包含時區偏移（Dart 的 ISO 8601 格式不包含 offset）。</p>
<p>SDK 需要手動附加時區偏移：<code>DateTime.now().timeZoneOffset</code> 取得偏移量，手動格式化為 <code>+08:00</code> 格式附加到 ISO 8601 字串後面。</p>
<h3 id="python">Python</h3>
<p><code>datetime.now(timezone.utc)</code> 取得 UTC 時間。<code>datetime.now().astimezone()</code> 取得本地時間帶時區。<code>.isoformat()</code> 產生帶時區偏移的 ISO 8601 字串。</p>
<p>Python 3.2+ 的 <code>datetime</code> 原生支援 timezone-aware 的 ISO 8601 輸出，是各平台中最完整的。</p>
<h3 id="go">Go</h3>
<p><code>time.Now()</code> 回傳帶時區的 Time 值。<code>time.Now().Format(time.RFC3339Milli)</code> 產生帶毫秒和時區偏移的字串。</p>
<p>Go 的 <code>time.RFC3339Nano</code> 提供奈秒精度，但監控事件不需要這個精度 — 毫秒足夠。</p>
<h2 id="clock-drift">Clock drift</h2>
<p>不同裝置的系統時鐘可能有偏差（clock drift）。使用者手機的時鐘比 collector server 快 5 分鐘，SDK 產生的 timestamp 會比 collector 收到時間早 5 分鐘。</p>
<p>Clock drift 的影響：</p>
<ul>
<li><strong>排序錯亂</strong>：裝置 A（時鐘快）和裝置 B（時鐘慢）的事件混合排序時，時間順序可能和真實發生順序不一致</li>
<li><strong>告警延遲計算錯誤</strong>：collector 用「事件 timestamp 到收到時間的差值」計算延遲，clock drift 讓延遲值不準確</li>
</ul>
<p>處理策略：</p>
<p><strong>Collector 記錄 receive_timestamp</strong>：每筆事件除了 SDK 端的 timestamp，collector 在收到時附加 <code>receive_timestamp</code>。兩者的差值用於估算 clock drift 和網路延遲。</p>
<p><strong>容忍而非修正</strong>：在數秒到數分鐘級的 drift 範圍內，容忍 drift 帶來的排序不精確。跨裝置的事件排序本身就不需要毫秒精度 — 分析的粒度通常是秒或分鐘。</p>
<p><strong>異常值偵測</strong>：timestamp 比 receive_timestamp 早超過 1 小時，或晚超過 5 分鐘，標記為可疑的 clock drift — 可能是使用者手動調整了系統時鐘。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>JS 平台適配 → <a href="/blog/monitoring/05-platform-adaptation/js-ts-platform/" data-link-title="JS/TS 平台適配" data-link-desc="CORS 限制、Service Worker 攔截、SPA 路由變換偵測 — 瀏覽器環境中 SDK 需要處理的平台特殊問題">JS/TS 平台適配</a></li>
<li>Flutter 平台適配 → <a href="/blog/monitoring/05-platform-adaptation/flutter-platform/" data-link-title="Flutter 平台適配" data-link-desc="Isolate 安全、Platform channel 攔截、app lifecycle 事件 — Flutter SDK 的平台特殊考量">Flutter 平台適配</a></li>
<li>Log schema 中的 timestamp 欄位 → <a href="/blog/monitoring/02-log-schema/event-schema-fields/" data-link-title="event.schema.json 完整欄位解說" data-link-desc="監控事件的 JSON Schema 定義 — 每個欄位的語意、必填/選填、資料型別和設計理由">模組二 event.schema.json 欄位解說</a></li>
<li>各平台的 error 攔截差異影響 test 設計 → <a href="/blog/testing/05-test-design-judgment/" data-link-title="模組五：測試設計判斷" data-link-desc="Mock 邊界判斷、assertion 設計、test data 代表性、flaky test 診斷">testing 模組五 測試設計判斷</a></li>
</ul>
]]></content:encoded></item><item><title>模組五：平台適配</title><link>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/</guid><description>&lt;p>回答「各平台有什麼特殊考量」。&lt;/p>
&lt;h2 id="待寫章節">待寫章節&lt;/h2>
&lt;ul>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> JS/TS 平台：CORS 限制、Service Worker 攔截、SPA 路由變換偵測&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Flutter 平台：isolate 安全、Platform channel 攔截、app lifecycle&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Python 平台：GIL 與 threading、atexit 可靠性、subprocess 監控&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Go 平台：graceful shutdown、signal handling、HTTP server 自身監控&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 跨平台 timestamp 一致性（時區、精度、clock drift）&lt;/li>
&lt;/ul>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/testing/05-test-design-judgment/" data-link-title="模組五：測試設計判斷" data-link-desc="Mock 邊界判斷、assertion 設計、test data 代表性、flaky test 診斷">testing 模組五 測試設計判斷&lt;/a>：各平台 error 攔截差異影響 test 設計&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>回答「各平台有什麼特殊考量」。</p>
<h2 id="待寫章節">待寫章節</h2>
<ul>
<li><input checked="" disabled="" type="checkbox"> JS/TS 平台：CORS 限制、Service Worker 攔截、SPA 路由變換偵測</li>
<li><input checked="" disabled="" type="checkbox"> Flutter 平台：isolate 安全、Platform channel 攔截、app lifecycle</li>
<li><input checked="" disabled="" type="checkbox"> Python 平台：GIL 與 threading、atexit 可靠性、subprocess 監控</li>
<li><input checked="" disabled="" type="checkbox"> Go 平台：graceful shutdown、signal handling、HTTP server 自身監控</li>
<li><input checked="" disabled="" type="checkbox"> 跨平台 timestamp 一致性（時區、精度、clock drift）</li>
</ul>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/testing/05-test-design-judgment/" data-link-title="模組五：測試設計判斷" data-link-desc="Mock 邊界判斷、assertion 設計、test data 代表性、flaky test 診斷">testing 模組五 測試設計判斷</a>：各平台 error 攔截差異影響 test 設計</li>
</ul>
]]></content:encoded></item><item><title>模組五：部署平台與網路入口</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/</guid><description>&lt;p>部署平台模組的核心目標是說明服務如何和外部調度、網路入口與資源限制對齊。語言教材會處理 &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>、health / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 檢查與 signal handling；本模組負責平台設定與操作語意。&lt;/p>
&lt;h2 id="vendor--platform-清單">Vendor / Platform 清單&lt;/h2>
&lt;p>實作時的常用選擇見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/" data-link-title="部署平台 Vendor 清單" data-link-desc="規劃 workload runtime、orchestration、traffic、IaC 與 discovery 的服務頁撰寫順序與判準">vendors&lt;/a> — T1 收錄 Kubernetes / Docker / systemd / nginx / Envoy / AWS ELB / Terraform / Traefik / Consul，每個 vendor 有定位、適用場景、取捨與預計實作話題的骨架。&lt;/p>
&lt;p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/" data-link-title="部署平台 Vendor 清單" data-link-desc="規劃 workload runtime、orchestration、traffic、IaC 與 discovery 的服務頁撰寫順序與判準">vendors/&lt;/a> 的「內容覆蓋進度」段。&lt;/p>
&lt;h2 id="暫定分類">暫定分類&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>分類&lt;/th>
 &lt;th>內容方向&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">Container&lt;/a>&lt;/td>
 &lt;td>image build、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">Resource Limit&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Kubernetes&lt;/td>
 &lt;td>deployment、pod lifecycle、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rolling-update/" data-link-title="Rolling Update" data-link-desc="說明逐批替換服務版本的發版策略與風險控制">rolling update&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>systemd&lt;/td>
 &lt;td>service unit、restart policy、signal、journal&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">Load balancer&lt;/a>&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idle-timeout/" data-link-title="Idle Timeout" data-link-desc="說明連線或會話在多久沒有活動後應該被回收">idle timeout&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">health check&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sticky-session/" data-link-title="Sticky Session" data-link-desc="說明同一 client 如何在一段時間內持續命中同一個後端實例">sticky session&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/service-registry/" data-link-title="Service Registry" data-link-desc="說明服務實例如何被註冊、維護與摘除">Service Registry&lt;/a>&lt;/td>
 &lt;td>實例如何註冊、更新與摘除&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">Service discovery&lt;/a>&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint&lt;/a> discovery、DNS&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config rollout&lt;/a>&lt;/td>
 &lt;td>設定如何安全下發到正在運作的服務實例&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config&lt;/a>&lt;/td>
 &lt;td>environment variable、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">Feature Flag&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CDN 與邊緣分發&lt;/td>
 &lt;td>邊緣快取、origin protection、purge 與 invalidation、stale-while-revalidate&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="建議閱讀順序">建議閱讀順序&lt;/h2>
&lt;p>章節編號是主題分類，不是閱讀順序。建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract&lt;/a> 理解 startup / readiness / liveness / shutdown / drain 的責任分類，再按 5.1 → 5.2 → 5.3 → 5.4 進入平台實作層。5.5（威脅建模）和 5.7（boundary 分類）適合讀完 5.1-5.4 後做概念整理。5.8（實作示範）是 5.2 + 5.3 的操作化，適合最後讀。&lt;/p></description><content:encoded><![CDATA[<p>部署平台模組的核心目標是說明服務如何和外部調度、網路入口與資源限制對齊。語言教材會處理 <a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a>、health / <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 檢查與 signal handling；本模組負責平台設定與操作語意。</p>
<h2 id="vendor--platform-清單">Vendor / Platform 清單</h2>
<p>實作時的常用選擇見 <a href="/blog/backend/05-deployment-platform/vendors/" data-link-title="部署平台 Vendor 清單" data-link-desc="規劃 workload runtime、orchestration、traffic、IaC 與 discovery 的服務頁撰寫順序與判準">vendors</a> — T1 收錄 Kubernetes / Docker / systemd / nginx / Envoy / AWS ELB / Terraform / Traefik / Consul，每個 vendor 有定位、適用場景、取捨與預計實作話題的骨架。</p>
<p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 <a href="/blog/backend/05-deployment-platform/vendors/" data-link-title="部署平台 Vendor 清單" data-link-desc="規劃 workload runtime、orchestration、traffic、IaC 與 discovery 的服務頁撰寫順序與判準">vendors/</a> 的「內容覆蓋進度」段。</p>
<h2 id="暫定分類">暫定分類</h2>
<table>
  <thead>
      <tr>
          <th>分類</th>
          <th>內容方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">Container</a></td>
          <td>image build、<a href="/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config</a>、<a href="/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">Resource Limit</a></td>
      </tr>
      <tr>
          <td>Kubernetes</td>
          <td>deployment、pod lifecycle、<a href="/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe</a>、<a href="/blog/backend/knowledge-cards/rolling-update/" data-link-title="Rolling Update" data-link-desc="說明逐批替換服務版本的發版策略與風險控制">rolling update</a></td>
      </tr>
      <tr>
          <td>systemd</td>
          <td>service unit、restart policy、signal、journal</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">Load balancer</a></td>
          <td><a href="/blog/backend/knowledge-cards/idle-timeout/" data-link-title="Idle Timeout" data-link-desc="說明連線或會話在多久沒有活動後應該被回收">idle timeout</a>、<a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a>、<a href="/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">health check</a>、<a href="/blog/backend/knowledge-cards/sticky-session/" data-link-title="Sticky Session" data-link-desc="說明同一 client 如何在一段時間內持續命中同一個後端實例">sticky session</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/service-registry/" data-link-title="Service Registry" data-link-desc="說明服務實例如何被註冊、維護與摘除">Service Registry</a></td>
          <td>實例如何註冊、更新與摘除</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">Service discovery</a></td>
          <td><a href="/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint</a> discovery、DNS</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config rollout</a></td>
          <td>設定如何安全下發到正在運作的服務實例</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config</a></td>
          <td>environment variable、<a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a>、<a href="/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">Feature Flag</a></td>
      </tr>
      <tr>
          <td>CDN 與邊緣分發</td>
          <td>邊緣快取、origin protection、purge 與 invalidation、stale-while-revalidate</td>
      </tr>
  </tbody>
</table>
<h2 id="建議閱讀順序">建議閱讀順序</h2>
<p>章節編號是主題分類，不是閱讀順序。建議先讀 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a> 理解 startup / readiness / liveness / shutdown / drain 的責任分類，再按 5.1 → 5.2 → 5.3 → 5.4 進入平台實作層。5.5（威脅建模）和 5.7（boundary 分類）適合讀完 5.1-5.4 後做概念整理。5.8（實作示範）是 5.2 + 5.3 的操作化，適合最後讀。</p>
<h2 id="選型入口">選型入口</h2>
<p>部署平台選型的核心判斷是服務如何被啟動、更新、接流量、擴容與停止。當問題集中在 container image、rolling update、health check、<a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a>、<a href="/blog/backend/knowledge-cards/service-registry/" data-link-title="Service Registry" data-link-desc="說明服務實例如何被註冊、維護與摘除">service registry</a>、<a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery</a> 或 <a href="/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config</a> 時，應先評估部署平台能力。</p>
<p>Container 解決服務包裝與 runtime 依賴；Kubernetes 解決多 instance 調度、<a href="/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe</a>、rolling update 與 <a href="/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">resource limit</a>；systemd 適合單機或 VM 上的 service lifecycle；<a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a> 解決流量入口、<a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a>、<a href="/blog/backend/knowledge-cards/idle-timeout/" data-link-title="Idle Timeout" data-link-desc="說明連線或會話在多久沒有活動後應該被回收">idle timeout</a> 與 <a href="/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">health check</a>；<a href="/blog/backend/knowledge-cards/service-registry/" data-link-title="Service Registry" data-link-desc="說明服務實例如何被註冊、維護與摘除">service registry</a> 解決實例狀態維護；<a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery</a> 解決服務彼此如何找到 <a href="/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint</a>；<a href="/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config</a> 解決環境差異、<a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a> 與 <a href="/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">Feature Flag</a>。</p>
<p>接近真實網路服務的例子包括發版時 request 失敗、pod 尚未 ready 就接流量、長連線 shutdown 清理不完整、服務擴容後 <a href="/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint</a> 更新延遲。這些場景的共同問題是程式與平台合約，因此本模組會先處理生命週期、流量入口與平台訊號。</p>
<h2 id="與語言教材的分工">與語言教材的分工</h2>
<p>語言教材處理程式內的生命週期與訊號。Backend deployment 模組處理 Kubernetes、systemd、<a href="/blog/backend/knowledge-cards/load-balancer/" data-link-title="Load Balancer" data-link-desc="說明流量如何分散、排空與導向健康節點">load balancer</a> 與 <a href="/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">container</a> 平台如何觸發、解讀與限制這些訊號。</p>
<h2 id="與資安概念層的交接">與資安概念層的交接</h2>
<p>本模組承接 07 模組的概念判讀，並在服務實體層落地。交接基線如下：</p>
<ul>
<li>來自 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a>：承接入口分級、管理平面分離、修補窗口節奏。</li>
<li>來自 <a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">7.5 傳輸信任與憑證生命週期</a>：承接 TLS/mTLS 與憑證佈署節奏。</li>
<li>來自 <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">7.6 秘密管理與機器憑證治理</a>：承接 runtime secret 與機器憑證交付模型。</li>
</ul>
<p>這個交接讓部署模組聚焦實體配置與平台語意，同時保持與資安判讀一致。</p>
<h2 id="案例驅動讀法">案例驅動讀法</h2>
<p>部署平台案例的核心讀法是先確認切換單位（服務、流量、叢集），再定義可回退邊界。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>先看章節</th>
          <th>回寫目標</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift：self-managed K8s -&gt; EKS</a></td>
          <td><a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2</a>、<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</a></td>
          <td>把零停機遷移拆成分批切流策略</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/conde-nast-platform-modernization-eks/" data-link-title="5.C2 Condé Nast：EKS 平台整併與標準化" data-link-desc="多地區異質 Kubernetes 平台整併為統一控制面的案例。">5.C2 Condé Nast：平台整併</a></td>
          <td><a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2</a></td>
          <td>把多叢集治理收斂成單一控制面</td>
      </tr>
      <tr>
          <td><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></td>
          <td><a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1</a>、<a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4</a></td>
          <td>把平台重置與服務連續性目標綁定</td>
      </tr>
  </tbody>
</table>
<h2 id="跨語言適配評估">跨語言適配評估</h2>
<p>部署平台使用方式會受語言的啟動時間、process model、signal handling、thread/task lifecycle、runtime memory behavior 與 liveness 支援影響。啟動慢的 runtime 要調整 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 與 rollout 節奏；長連線或背景 worker 要支援 <a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a>；使用 GC 的 runtime 要觀察 memory limit 與 pause 行為；多 process 模型要確認 signal、<a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 與 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 如何聚合。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>關鍵收穫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1</a></td>
          <td><a href="/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">container</a> 與 runtime</td>
          <td>規劃 image、資源限制與啟動行為</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2</a></td>
          <td>Kubernetes 部署策略</td>
          <td>了解 deployment、<a href="/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe</a>、rolling update</td>
      </tr>
      <tr>
          <td><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</a></td>
          <td><a href="/blog/backend/knowledge-cards/load-balancer-contract/" data-link-title="Load Balancer Contract" data-link-desc="說明服務與負載平衡器之間的流量與健康檢查約定">Load Balancer Contract</a></td>
          <td>處理 <a href="/blog/backend/knowledge-cards/idle-timeout/" data-link-title="Idle Timeout" data-link-desc="說明連線或會話在多久沒有活動後應該被回收">idle timeout</a>、<a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a> 與 <a href="/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">health check</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4</a></td>
          <td><a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery</a></td>
          <td>讓服務能穩定註冊與發現彼此</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/attacker-view-platform-entry-risks/" data-link-title="5.5 平台與入口威脅建模（Threat Modeling）" data-link-desc="以概念層判讀部署平台弱點，聚焦入口、生命週期、設定與交付節奏">5.5</a></td>
          <td>平台與入口威脅建模（Threat Modeling）</td>
          <td>用隱藏入口、設定漂移與切換風險盤點交付平台</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6</a></td>
          <td>Platform Lifecycle Contract</td>
          <td>分辨 startup、readiness、liveness、shutdown 與 drain 的責任</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7</a></td>
          <td>Traffic、Config 與 Control Plane Boundary</td>
          <td>拆分流量、設定、secret、service discovery 與管理面邊界</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.8</a></td>
          <td>Deployment Rollout with Drain and Rollback 實作示範</td>
          <td>以 checkout service 示範 canary evidence、drain signal 與 rollback decision</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9</a></td>
          <td>邊緣分發與靜態資源（CDN / Origin Protection）</td>
          <td>把 CDN 視為網路入口層，理解三層快取分工、origin protection、purge 操作模型</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10</a></td>
          <td>Outbound Tunnel 入口與生命週期（cloudflared / Tailscale）</td>
          <td>把反向隧道視為一種入口形態、理解就緒對齊、network 層故障與認證疊法</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/05-deployment-platform/cases/" data-link-title="模組五案例正文" data-link-desc="部署平台轉換案例入口。">5.C</a></td>
          <td>轉換案例正文</td>
          <td>把平台遷移、整併與流量切換做成可回寫案例</td>
      </tr>
  </tbody>
</table>
<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 反例</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>。</p>
<p>回退判讀寫法見 <a href="/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/#%e5%9b%9e%e9%80%80%e5%88%a4%e8%ae%80%e5%af%ab%e6%b3%95" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">0.C4 回退判讀寫法</a>，部署案例要優先保留切流批次、draining、連線生命週期與回退時間。</p>
<h2 id="觀念網路補完方向">觀念網路補完方向</h2>
<p>部署平台章節下一輪的核心責任是把平台能力寫成服務契約。現有章節已經有 container、Kubernetes、load balancer 與 service discovery，但還需要補上 runtime contract、lifecycle contract、traffic contract、rollout contract 與 control-plane contract 的關係，讓讀者知道部署是一組流量、連線、設定、資源與回退條件的連續切換。</p>
<table>
  <thead>
      <tr>
          <th>補完方向</th>
          <th>需要回答的問題</th>
          <th>主要路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Runtime contract</td>
          <td>image、entrypoint、runtime config 與 resource limit 是否可預期</td>
          <td><a href="/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">container</a>、<a href="/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">runtime config</a></td>
      </tr>
      <tr>
          <td>Lifecycle contract</td>
          <td>startup、readiness、liveness、shutdown 與 drain 是否對齊</td>
          <td><a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a>、<a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a></td>
      </tr>
      <tr>
          <td>Traffic contract</td>
          <td>load balancer、timeout、sticky session 與 routing 是否有明確邊界</td>
          <td><a href="/blog/backend/knowledge-cards/load-balancer-contract/" data-link-title="Load Balancer Contract" data-link-desc="說明服務與負載平衡器之間的流量與健康檢查約定">load balancer contract</a>、<a href="/blog/backend/knowledge-cards/request-routing/" data-link-title="Request Routing" data-link-desc="說明入口流量如何依規則被導向不同服務或處理路徑">request routing</a></td>
      </tr>
      <tr>
          <td>Rollout contract</td>
          <td>canary、rolling update、config rollout 與 rollback 是否可分批</td>
          <td><a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">config rollout</a>、<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a></td>
      </tr>
      <tr>
          <td>Control-plane contract</td>
          <td>service discovery、secret delivery 與管理面是否被保護</td>
          <td><a href="/blog/backend/knowledge-cards/management-plane/" data-link-title="Management Plane" data-link-desc="說明管理平面如何與業務流量平面分離，避免高權限入口擴散">management plane</a>、<a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3</a></td>
      </tr>
  </tbody>
</table>
<p>這些方向要用部署平台自己的服務壓力展開。短 request API、長連線服務、背景 worker、<a href="/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane</a> config push 與多租戶平台的生命週期不同，寫作時要分別處理它們的 rollout 與 drain 條件。</p>
<h2 id="知識卡補強方向">知識卡補強方向</h2>
<p>部署模組的 knowledge card 缺口集中在「平台契約」與「切換完成訊號」。已有 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a>、<a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a>、<a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">config rollout</a> 與 <a href="/blog/backend/knowledge-cards/rollback-strategy/" data-link-title="Rollback Strategy" data-link-desc="說明事故期間如何判斷回滾、回切與暫停變更">rollback strategy</a> 可以作為第一批錨點。</p>
<p>下一批候選卡片包括 startup probe、drain completion、rollout batch、<a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a>、config freeze、environment protection 與 deployment contract。這些卡片要讓讀者能分辨「服務已啟動」和「服務可安全接流量」分屬不同責任。</p>
<h2 id="實作探討入口">實作探討入口</h2>
<p>部署平台的第一條實作路徑是 <a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.8 Deployment Rollout with Drain and Rollback（實作示範）</a>。這篇以 checkout service rollout 為例，說明 rollout plan、canary evidence、drain signal、rollback condition 與 incident decision route 如何一起成立。</p>
<p>這條路徑的前置引用應該是 5.2 Kubernetes deployment、5.3 load balancer contract、<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>、<a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a> 與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。完成後可依 <a href="/blog/backend/#%e5%ad%b8%e7%bf%92%e8%b7%af%e7%b7%9a" data-link-title="Backend 服務實務指南" data-link-desc="用跨語言教學路線整理資料庫、快取、訊息佇列、觀測、部署、可靠性、資安、事故與容量等後端服務能力">Backend 學習路線</a> 進入下一條服務路徑。</p>
<p>部署路徑的 artifact 對齊重點是「每一批切換都能被觀測、被放行、被回退」。對 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a> 要交 <code>Source/Time range/Query link/Owner/Data quality</code>，並覆蓋 per-version error rate、latency、drain completion 與 reconnect 訊號；對 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a> 要交 <code>Gate decision/Checks/Stop condition/Rollback window/Owner</code>，呈現 canary 批次與停損規則；對 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a> 要交 <code>Timestamp/Decision/Context/Evidence/Owner/Expected effect/Rollback condition</code>，記錄 freeze、回退與重啟切流的決策條件與時間序列。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/05-core-services/" data-link-title="模組五：核心服務上 IaC" data-link-desc="資料庫、運算、儲存、load balancer 怎麼寫進基礎設施程式碼，以及上線順序">infra 模組五：核心服務上 IaC</a>：ECS / EKS 的 IaC 描述（subnet 接線、IAM task role、映像版本解耦）是部署平台的地基層</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>IaC / Platform 部署 CI/CD</title><link>https://tarrragon.github.io/blog/ci/iac-platform-deploy/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/iac-platform-deploy/</guid><description>&lt;p>IaC / Platform 部署 CI/CD 的核心責任是把基礎設施變更轉成可審查、可追溯、可回復的流程。它和應用部署不同，主要風險在 state、權限、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift&lt;/a> 與不可逆資源變更。&lt;/p>
&lt;h2 id="場域定位">場域定位&lt;/h2>
&lt;p>IaC 流程通常分成 plan、review、apply 三段，並依環境分層推進。部署成功不只代表指令完成，還代表資源狀態符合預期且未引入漂移。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>IaC 部署常見責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Plan&lt;/td>
 &lt;td>變更差異預覽與風險提示&lt;/td>
 &lt;td>是否包含高風險破壞性變更&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Review&lt;/td>
 &lt;td>審核資源變更與權限範圍&lt;/td>
 &lt;td>是否符合治理規範&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Apply&lt;/td>
 &lt;td>狀態寫入與資源同步&lt;/td>
 &lt;td>state lock / timeout 是否可控&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift&lt;/a>&lt;/td>
 &lt;td>實際環境與宣告差異檢查&lt;/td>
 &lt;td>是否存在未受控手動變更&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Recovery&lt;/td>
 &lt;td>回退或補正策略&lt;/td>
 &lt;td>失敗時是否有安全回復路徑&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見注意事項">常見注意事項&lt;/h2>
&lt;ul>
&lt;li>plan 與 apply 要用同一份輸入與版本，避免結果漂移。&lt;/li>
&lt;li>state backend 要有鎖定與權限隔離，避免併發覆寫。&lt;/li>
&lt;li>高風險資源變更需要額外 gate（人工審核或變更時窗）。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift&lt;/a> 偵測要定期執行，並有修復責任人。&lt;/li>
&lt;/ul>
&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;a href="plan-apply-drift-recovery-flow/">IaC plan、apply、drift 與 recovery 流程&lt;/a>&lt;/td>
 &lt;td>Plan, apply, drift and recovery&lt;/td>
 &lt;td>控制基礎設施變更、漂移與回復&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>IaC 發布主流程：讀 &lt;a href="plan-apply-drift-recovery-flow/">IaC plan、apply、drift 與 recovery 流程&lt;/a>。&lt;/li>
&lt;li>環境保護：讀 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment Protection&lt;/a>。&lt;/li>
&lt;li>部署合約：讀 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/deployment-contract/" data-link-title="Deployment Contract" data-link-desc="說明服務與部署平台之間的生命週期約定">Deployment Contract&lt;/a>。&lt;/li>
&lt;li>變更放行：讀 &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;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>IaC / Platform 部署 CI/CD 的核心責任是把基礎設施變更轉成可審查、可追溯、可回復的流程。它和應用部署不同，主要風險在 state、權限、<a href="/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift</a> 與不可逆資源變更。</p>
<h2 id="場域定位">場域定位</h2>
<p>IaC 流程通常分成 plan、review、apply 三段，並依環境分層推進。部署成功不只代表指令完成，還代表資源狀態符合預期且未引入漂移。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>IaC 部署常見責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Plan</td>
          <td>變更差異預覽與風險提示</td>
          <td>是否包含高風險破壞性變更</td>
      </tr>
      <tr>
          <td>Review</td>
          <td>審核資源變更與權限範圍</td>
          <td>是否符合治理規範</td>
      </tr>
      <tr>
          <td>Apply</td>
          <td>狀態寫入與資源同步</td>
          <td>state lock / timeout 是否可控</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift</a></td>
          <td>實際環境與宣告差異檢查</td>
          <td>是否存在未受控手動變更</td>
      </tr>
      <tr>
          <td>Recovery</td>
          <td>回退或補正策略</td>
          <td>失敗時是否有安全回復路徑</td>
      </tr>
  </tbody>
</table>
<h2 id="常見注意事項">常見注意事項</h2>
<ul>
<li>plan 與 apply 要用同一份輸入與版本，避免結果漂移。</li>
<li>state backend 要有鎖定與權限隔離，避免併發覆寫。</li>
<li>高風險資源變更需要額外 gate（人工審核或變更時窗）。</li>
<li><a href="/blog/ci/knowledge-cards/infrastructure-drift/" data-link-title="Infrastructure Drift" data-link-desc="說明真實基礎設施狀態與 IaC 宣告分叉時的偵測、判讀與修復責任">Infrastructure Drift</a> 偵測要定期執行，並有修復責任人。</li>
</ul>
<h2 id="學習路線">學習路線</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="plan-apply-drift-recovery-flow/">IaC plan、apply、drift 與 recovery 流程</a></td>
          <td>Plan, apply, drift and recovery</td>
          <td>控制基礎設施變更、漂移與回復</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>IaC 發布主流程：讀 <a href="plan-apply-drift-recovery-flow/">IaC plan、apply、drift 與 recovery 流程</a>。</li>
<li>環境保護：讀 <a href="/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment Protection</a>。</li>
<li>部署合約：讀 <a href="/blog/backend/knowledge-cards/deployment-contract/" data-link-title="Deployment Contract" data-link-desc="說明服務與部署平台之間的生命週期約定">Deployment Contract</a>。</li>
<li>變更放行：讀 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">Release Gate</a>。</li>
</ul>
]]></content:encoded></item><item><title>Infrastructure Drift</title><link>https://tarrragon.github.io/blog/ci/knowledge-cards/infrastructure-drift/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/knowledge-cards/infrastructure-drift/</guid><description>&lt;p>Infrastructure Drift 的核心概念是「真實環境狀態與宣告檔分叉」。它會削弱 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment Protection&lt;/a> 與 deployment review 的可信度，並影響下一次 plan / apply 的安全性。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Infrastructure Drift 位在 IaC state、cloud resource、手動 hotfix 與外部 controller 之間，常由 console edit、事故修復、provider 預設值或自動調整造成。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;ul>
&lt;li>plan 顯示大量非預期變更。&lt;/li>
&lt;li>production 資源和 repository 宣告不一致。&lt;/li>
&lt;li>下次 apply 可能覆蓋事故 hotfix。&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實服務的例子">接近真實服務的例子&lt;/h2>
&lt;p>事故中工程師在雲端 console 手動放寬 security group。服務恢復後，IaC plan 顯示 security group 與宣告檔不同；團隊需要判斷這個變更是短期 hotfix 還是應回寫成正式規則。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Infrastructure Drift 要定義偵測頻率、owner、修復路由、state repair 與回寫規則，讓平台狀態重新回到可審查流程。&lt;/p></description><content:encoded><![CDATA[<p>Infrastructure Drift 的核心概念是「真實環境狀態與宣告檔分叉」。它會削弱 <a href="/blog/ci/knowledge-cards/environment-protection/" data-link-title="Environment Protection" data-link-desc="說明目標環境的審核、權限與放行條件如何保護發布">Environment Protection</a> 與 deployment review 的可信度，並影響下一次 plan / apply 的安全性。</p>
<h2 id="概念位置">概念位置</h2>
<p>Infrastructure Drift 位在 IaC state、cloud resource、手動 hotfix 與外部 controller 之間，常由 console edit、事故修復、provider 預設值或自動調整造成。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<ul>
<li>plan 顯示大量非預期變更。</li>
<li>production 資源和 repository 宣告不一致。</li>
<li>下次 apply 可能覆蓋事故 hotfix。</li>
</ul>
<h2 id="接近真實服務的例子">接近真實服務的例子</h2>
<p>事故中工程師在雲端 console 手動放寬 security group。服務恢復後，IaC plan 顯示 security group 與宣告檔不同；團隊需要判斷這個變更是短期 hotfix 還是應回寫成正式規則。</p>
<h2 id="設計責任">設計責任</h2>
<p>Infrastructure Drift 要定義偵測頻率、owner、修復路由、state repair 與回寫規則，讓平台狀態重新回到可審查流程。</p>
]]></content:encoded></item></channel></rss>