<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>模組五：部署平台與網路入口 on Tarragon</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/</link><description>Recent content in 模組五：部署平台與網路入口 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 22 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/backend/05-deployment-platform/index.xml" rel="self" type="application/rss+xml"/><item><title>5.1 container 與 runtime</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/</guid><description>&lt;p>容器執行環境（container runtime）的核心責任是把應用執行環境做成可重現、可限制、可觀測的交付單位。它是部署可靠性的起點——後續的 probe、canary、rollback 都假設 runtime 產物行為可預測。&lt;/p>
&lt;h2 id="image-與建置責任">image 與建置責任&lt;/h2>
&lt;p>image 的責任是固定依賴、執行入口與檔案結構，讓同一版本在不同環境行為一致。建置流程要回答三件事：基底映像是否可維護、建置產物是否可追溯、敏感資訊是否被隔離。&lt;/p>
&lt;p>映像層數、套件來源、編譯參數都會影響啟動時間與安全邊界。部署策略在後面才有效，前提是 runtime 產物本身可預測。&lt;/p>
&lt;h3 id="基底映像選擇">基底映像選擇&lt;/h3>
&lt;p>基底映像（base image）決定 image 的安全維護基線與啟動時體積。選擇的核心取捨是體積 / 啟動速度與除錯便利性：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>語言官方映像&lt;/strong>（&lt;code>python:3.12&lt;/code>、&lt;code>node:20&lt;/code>）：套件齊全、除錯方便，但體積大（通常 800MB+）、攻擊面廣。適合開發環境與 CI。&lt;/li>
&lt;li>&lt;strong>slim / alpine 變體&lt;/strong>（&lt;code>python:3.12-slim&lt;/code>、&lt;code>node:20-alpine&lt;/code>）：體積壓到 100-200MB、啟動快、攻擊面小。代價是缺少除錯工具（strace、curl、dig），生產事故時 exec 進容器排查會受限。Alpine 用 musl libc 而非 glibc，某些 C extension 需要額外處理。&lt;/li>
&lt;li>&lt;strong>distroless&lt;/strong>（&lt;code>gcr.io/distroless/base&lt;/code>）：只包含 runtime 必要檔案，無 shell、無套件管理器。攻擊面最小，但除錯只能靠 ephemeral debug container 或外部觀測。適合安全要求高且觀測基礎建設完備的生產環境。&lt;/li>
&lt;li>&lt;strong>自建基底&lt;/strong>：組織統一維護的基底映像，可以固定安全基線、預裝觀測 agent、統一 timezone / locale。代價是基底維護本身是持續工作，版本更新節奏要有明確 owner。&lt;/li>
&lt;/ul>
&lt;p>選完基底後要確認兩件事：upstream 的更新節奏是否可追蹤（CVE 修補從上游到自家 image 的時間），以及團隊是否有能力在基底更新後快速重建並驗證所有服務 image。&lt;/p>
&lt;h3 id="建置可重現性">建置可重現性&lt;/h3>
&lt;p>同一份 source code 在不同時間建置出不同 image，會讓 rollback 的假設失效——「回退到上一版」回退的是哪一版，取決於當時 build 環境的狀態。&lt;/p>
&lt;p>可重現建置的關鍵實踐：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>鎖定依賴版本&lt;/strong>：&lt;code>go.sum&lt;/code>、&lt;code>package-lock.json&lt;/code>、&lt;code>poetry.lock&lt;/code> 要進 git。依賴解析在建置時不從 registry 重新 resolve。&lt;/li>
&lt;li>&lt;strong>Multi-stage build&lt;/strong>：把建置環境（compiler、dev dependencies）和執行環境分開。最終 image 只包含 runtime 必要檔案，體積小且攻擊面收窄。&lt;/li>
&lt;li>&lt;strong>避免 image 中殘留敏感資訊&lt;/strong>：build arg、環境變數、中間層都可能殘留 secret。secret 不進 Dockerfile，用 runtime mount 或 secret manager 注入。&lt;/li>
&lt;li>&lt;strong>image 標記策略&lt;/strong>：&lt;code>latest&lt;/code> tag 不可重現——同一個 tag 指向的 image 會隨時間改變。用 git commit SHA 或語意版本號標記，讓每個 tag 指向唯一 image。&lt;/li>
&lt;/ol>
&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>：揭露「跨平台遷移本質是能力遷移」。遷移到新平台時，CI/CD pipeline 可能換了 runner 環境、換了 registry——建置可重現性的前提是依賴鎖定與 multi-stage build 本身不依賴特定 CI 環境。&lt;/p>
&lt;h2 id="entrypoint-與啟動行為">entrypoint 與啟動行為&lt;/h2>
&lt;p>entrypoint/command 的責任是定義容器如何啟動與退出。啟動流程應顯式處理初始化步驟、配置載入、依賴檢查與失敗退出。退出流程應處理信號中斷、在途請求收斂與資源釋放。&lt;/p>
&lt;p>若啟動行為隱藏在 shell script 且無可觀測訊號，部署平台很難判斷 readiness 與失敗原因。&lt;/p>
&lt;h3 id="pid-1-與信號處理">PID 1 與信號處理&lt;/h3>
&lt;p>容器內 PID 1 有特殊語意：它是 init process，負責接收平台送來的 SIGTERM / SIGINT 並轉發給子進程。PID 1 的問題出在三種情境：&lt;/p>
&lt;p>&lt;strong>Shell 作為 PID 1&lt;/strong>：&lt;code>ENTRYPOINT [&amp;quot;sh&amp;quot;, &amp;quot;-c&amp;quot;, &amp;quot;java -jar app.jar&amp;quot;]&lt;/code> 讓 sh 成為 PID 1。SIGTERM 送到 sh、sh 預設不轉發、java 進程收不到信號、等到 terminationGracePeriodSeconds 到期後被 SIGKILL 強殺。修法是用 &lt;code>exec&lt;/code> 或直接用 exec form：&lt;code>ENTRYPOINT [&amp;quot;java&amp;quot;, &amp;quot;-jar&amp;quot;, &amp;quot;app.jar&amp;quot;]&lt;/code>。&lt;/p>
&lt;p>&lt;strong>多進程容器&lt;/strong>：一個容器跑多個進程時，PID 1 要負責信號轉發與子進程回收（zombie reaping）。如果 PID 1 不做 wait()，結束的子進程會變成 zombie。解法是用 tini 或 dumb-init 作為輕量 init，或在 Kubernetes 設 &lt;code>shareProcessNamespace: true&lt;/code> 讓 kubelet 處理。&lt;/p></description><content:encoded><![CDATA[<p>容器執行環境（container runtime）的核心責任是把應用執行環境做成可重現、可限制、可觀測的交付單位。它是部署可靠性的起點——後續的 probe、canary、rollback 都假設 runtime 產物行為可預測。</p>
<h2 id="image-與建置責任">image 與建置責任</h2>
<p>image 的責任是固定依賴、執行入口與檔案結構，讓同一版本在不同環境行為一致。建置流程要回答三件事：基底映像是否可維護、建置產物是否可追溯、敏感資訊是否被隔離。</p>
<p>映像層數、套件來源、編譯參數都會影響啟動時間與安全邊界。部署策略在後面才有效，前提是 runtime 產物本身可預測。</p>
<h3 id="基底映像選擇">基底映像選擇</h3>
<p>基底映像（base image）決定 image 的安全維護基線與啟動時體積。選擇的核心取捨是體積 / 啟動速度與除錯便利性：</p>
<ul>
<li><strong>語言官方映像</strong>（<code>python:3.12</code>、<code>node:20</code>）：套件齊全、除錯方便，但體積大（通常 800MB+）、攻擊面廣。適合開發環境與 CI。</li>
<li><strong>slim / alpine 變體</strong>（<code>python:3.12-slim</code>、<code>node:20-alpine</code>）：體積壓到 100-200MB、啟動快、攻擊面小。代價是缺少除錯工具（strace、curl、dig），生產事故時 exec 進容器排查會受限。Alpine 用 musl libc 而非 glibc，某些 C extension 需要額外處理。</li>
<li><strong>distroless</strong>（<code>gcr.io/distroless/base</code>）：只包含 runtime 必要檔案，無 shell、無套件管理器。攻擊面最小，但除錯只能靠 ephemeral debug container 或外部觀測。適合安全要求高且觀測基礎建設完備的生產環境。</li>
<li><strong>自建基底</strong>：組織統一維護的基底映像，可以固定安全基線、預裝觀測 agent、統一 timezone / locale。代價是基底維護本身是持續工作，版本更新節奏要有明確 owner。</li>
</ul>
<p>選完基底後要確認兩件事：upstream 的更新節奏是否可追蹤（CVE 修補從上游到自家 image 的時間），以及團隊是否有能力在基底更新後快速重建並驗證所有服務 image。</p>
<h3 id="建置可重現性">建置可重現性</h3>
<p>同一份 source code 在不同時間建置出不同 image，會讓 rollback 的假設失效——「回退到上一版」回退的是哪一版，取決於當時 build 環境的狀態。</p>
<p>可重現建置的關鍵實踐：</p>
<ol>
<li><strong>鎖定依賴版本</strong>：<code>go.sum</code>、<code>package-lock.json</code>、<code>poetry.lock</code> 要進 git。依賴解析在建置時不從 registry 重新 resolve。</li>
<li><strong>Multi-stage build</strong>：把建置環境（compiler、dev dependencies）和執行環境分開。最終 image 只包含 runtime 必要檔案，體積小且攻擊面收窄。</li>
<li><strong>避免 image 中殘留敏感資訊</strong>：build arg、環境變數、中間層都可能殘留 secret。secret 不進 Dockerfile，用 runtime mount 或 secret manager 注入。</li>
<li><strong>image 標記策略</strong>：<code>latest</code> tag 不可重現——同一個 tag 指向的 image 會隨時間改變。用 git commit SHA 或語意版本號標記，讓每個 tag 指向唯一 image。</li>
</ol>
<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>：揭露「跨平台遷移本質是能力遷移」。遷移到新平台時，CI/CD pipeline 可能換了 runner 環境、換了 registry——建置可重現性的前提是依賴鎖定與 multi-stage build 本身不依賴特定 CI 環境。</p>
<h2 id="entrypoint-與啟動行為">entrypoint 與啟動行為</h2>
<p>entrypoint/command 的責任是定義容器如何啟動與退出。啟動流程應顯式處理初始化步驟、配置載入、依賴檢查與失敗退出。退出流程應處理信號中斷、在途請求收斂與資源釋放。</p>
<p>若啟動行為隱藏在 shell script 且無可觀測訊號，部署平台很難判斷 readiness 與失敗原因。</p>
<h3 id="pid-1-與信號處理">PID 1 與信號處理</h3>
<p>容器內 PID 1 有特殊語意：它是 init process，負責接收平台送來的 SIGTERM / SIGINT 並轉發給子進程。PID 1 的問題出在三種情境：</p>
<p><strong>Shell 作為 PID 1</strong>：<code>ENTRYPOINT [&quot;sh&quot;, &quot;-c&quot;, &quot;java -jar app.jar&quot;]</code> 讓 sh 成為 PID 1。SIGTERM 送到 sh、sh 預設不轉發、java 進程收不到信號、等到 terminationGracePeriodSeconds 到期後被 SIGKILL 強殺。修法是用 <code>exec</code> 或直接用 exec form：<code>ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]</code>。</p>
<p><strong>多進程容器</strong>：一個容器跑多個進程時，PID 1 要負責信號轉發與子進程回收（zombie reaping）。如果 PID 1 不做 wait()，結束的子進程會變成 zombie。解法是用 tini 或 dumb-init 作為輕量 init，或在 Kubernetes 設 <code>shareProcessNamespace: true</code> 讓 kubelet 處理。</p>
<p><strong>啟動腳本的信號遮蔽</strong>：entrypoint script 在初始化階段（下載 config、等依賴就緒）捕捉 SIGTERM 做清理，但如果清理邏輯卡住，整個 shutdown 會被阻塞。啟動腳本的 trap handler 要有 timeout，避免把 graceful shutdown 變成 ungraceful hang。</p>
<h3 id="啟動時間對部署策略的影響">啟動時間對部署策略的影響</h3>
<p>啟動時間直接影響 rollout 的最短觀察窗。一個啟動需 60 秒的服務，rollout 每批至少要等 60 秒 + 觀察窗口才能確認新版本穩定。啟動時間的組成與壓縮策略見 <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>。</p>
<p>image 體積也影響啟動時間——image pull 在冷啟動（節點上沒有這個 image 的快取）時占啟動時間的顯著比例。1GB image 在 100Mbps 網路下需要 ~80 秒 pull。壓縮 image 體積同時改善啟動速度與節省 registry 頻寬。</p>
<h2 id="resource-limit">resource limit</h2>
<p>CPU/memory <a href="/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">Resource Limit</a> 隔離資源競爭並保護叢集穩態。限制過低會導致頻繁節流與重啟，過高會壓縮同節點容量並放大鄰近工作負載風險。</p>
<p>限制設計要依服務流量型態與 GC/執行時特性調整，並與 autoscaling、rollout 批次策略一起評估。</p>
<h3 id="cpu-request-與-limit-的設定策略">CPU request 與 limit 的設定策略</h3>
<p>CPU 限制有兩個參數：request（排程保證）與 limit（硬上限）。兩者的關係決定服務在負載變動下的行為：</p>
<ul>
<li><strong>request = limit</strong>（guaranteed QoS）：CPU 用量穩定可預測，不會被 throttle 也不會超用。代價是無法在閒時借用節點剩餘 CPU。適合延遲敏感的 API 服務。</li>
<li><strong>request &lt; limit</strong>（burstable QoS）：平時用 request 保證的份額，高峰時可用到 limit。代價是當節點 CPU 競爭激烈時，所有 burstable pod 同時被 throttle，延遲會一起劣化。適合批次處理或對延遲要求不高的服務。</li>
<li><strong>不設 limit</strong>（只設 request）：服務可用到節點全部剩餘 CPU。Kubernetes 社群近年傾向這個做法——CPU throttle 常比 CPU contention 更難排查。代價是需要良好的觀測來偵測 noisy neighbor。</li>
</ul>
<h3 id="memory-limit-與-oom-的判讀">Memory limit 與 OOM 的判讀</h3>
<p>memory limit 是硬邊界——超過就 OOM kill，不走 graceful shutdown。OOM kill 的判讀分兩種情境：</p>
<p><strong>真正的 memory leak</strong>：記憶體使用量隨時間單調上升，GC 無法回收。修法在程式碼層。memory limit 只是延後問題爆發，不是解法。</p>
<p><strong>memory limit 設太低</strong>：服務在高峰流量下的正常記憶體使用超過 limit。常見於 JVM 服務——JVM heap + metaspace + native memory + thread stack 的總和超出 container memory limit。設 limit 時要用「峰值實際使用 + headroom」而非「平均使用」。</p>
<p>GC-based runtime（JVM、.NET、Go）要注意 container-aware memory 設定。早期 JVM 不認 cgroup memory limit，會按宿主機記憶體計算 heap 大小，導致 heap 配置超過 container limit。現代 JVM（Java 10+）預設啟用 container awareness（<code>-XX:+UseContainerSupport</code>），Go runtime 1.19+ 支援 <code>GOMEMLIMIT</code>。</p>
<h3 id="資源設定與-autoscaling-的協同">資源設定與 autoscaling 的協同</h3>
<p>resource request 同時決定 HPA（Horizontal Pod Autoscaler）的觸發基線。request 設太高時，CPU utilization % 會偏低，HPA 不會觸發擴容，導致服務在真正需要擴容前已經出現延遲。request 設太低時，utilization % 容易衝高，HPA 頻繁擴容，造成 pod 數量抖動。</p>
<p>穩定做法是先在 staging 環境跑負載測試確認服務的實際資源消耗曲線，再以 p90 負載的 CPU / memory 使用作為 request 基線。</p>
<h2 id="runtime-config">runtime config</h2>
<p>環境差異要顯式化才能追蹤——<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/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout</a> 策略，避免同批大規模變更。</p>
<p>runtime 配置與映像版本要保留相容窗口，讓部署與回退可分步進行。</p>
<h3 id="配置注入方式與取捨">配置注入方式與取捨</h3>
<p>配置注入容器有三條路徑，各自有不同的版本追蹤與更新語意：</p>
<table>
  <thead>
      <tr>
          <th>注入方式</th>
          <th>版本追蹤</th>
          <th>更新行為</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>環境變數</td>
          <td>跟 deployment spec 一起版控</td>
          <td>需要 pod restart 才生效</td>
          <td>啟動時固定的設定（DB URL、port）</td>
      </tr>
      <tr>
          <td>ConfigMap mount</td>
          <td>ConfigMap 版本</td>
          <td>自動更新（kubelet sync period 內）</td>
          <td>需要動態更新的非敏感設定</td>
      </tr>
      <tr>
          <td>Secret mount</td>
          <td>Secret 版本</td>
          <td>自動更新（同 ConfigMap）</td>
          <td>credential、cert、API key</td>
      </tr>
      <tr>
          <td>外部 config store</td>
          <td>config store 內版本</td>
          <td>應用主動拉取或 sidecar push</td>
          <td>feature flag、複雜設定邏輯</td>
      </tr>
  </tbody>
</table>
<p>環境變數最簡單但更新需要 restart。ConfigMap mount 可以動態更新但應用要能偵測檔案變化並 reload。外部 config store（Consul KV、AWS AppConfig、Feature Flag service）最靈活但引入了額外依賴。</p>
<p>設定變更跟 image 變更走不同路徑時，要確保兩者的版本可以交叉相容。版本 v2 的 image 搭版本 A 的 config 能跑、版本 v1 的 image 搭版本 B 的 config 也能跑——rollback image 但 config 沒回退、或 rollback config 但 image 沒回退的情境下、服務不應崩潰。這個相容窗口的設計責任見 <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 Config Boundary</a>。</p>
<h2 id="遷移期的-runtime-穩定性">遷移期的 Runtime 穩定性</h2>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5 Miro managed EKS 遷移</a>：揭露「平台託管化的價值在讓團隊把心力從底層維護轉到交付效率與可靠性策略」。遷移到 managed 平台後，runtime 層面的變化包含 container runtime 版本（containerd vs Docker shim）、node OS、storage driver、network plugin。這些變化可能改變 image pull 速度、filesystem 行為、DNS 解析路徑。</p>
<p>遷移前後的 runtime 驗證應包含：</p>
<ol>
<li><strong>image pull 時間比較</strong>：新 registry / 新 node 的 pull 速度是否在 startup timeout 內。</li>
<li><strong>filesystem 行為</strong>：log 寫入路徑、tmp 目錄、volume mount 行為在新 runtime 下是否一致。</li>
<li><strong>DNS 解析</strong>：新叢集的 CoreDNS / node-local DNS 設定是否影響服務的依賴連線建立速度。</li>
<li><strong>resource 行為</strong>：新 node type 的 CPU 架構（x86 vs ARM）、memory page size 是否影響服務性能特性。</li>
</ol>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>新版本容器啟動時間顯著增加</td>
          <td>image 體積或初始化步驟膨脹</td>
          <td>優化映像層、拆分初始化流程</td>
      </tr>
      <tr>
          <td>rollout 初期出現 OOM/CPU throttle</td>
          <td>resource limit 與實際負載不匹配</td>
          <td>重設 request/limit、調整併發與批次</td>
      </tr>
      <tr>
          <td>配置變更後特定環境異常</td>
          <td>runtime config 管理不一致</td>
          <td>統一配置來源、補版本追蹤與差異檢查</td>
      </tr>
      <tr>
          <td>容器停止時請求中斷率上升</td>
          <td>signal/drain 協調不足</td>
          <td>補 shutdown hook、對齊 termination 流程</td>
      </tr>
      <tr>
          <td>同版本在不同節點行為差異大</td>
          <td>runtime 依賴未固定或環境漂移</td>
          <td>收斂基底映像、鎖定依賴與建置流程</td>
      </tr>
      <tr>
          <td>JVM 服務 OOM 但 heap 未用滿</td>
          <td>native memory / metaspace 超出 limit</td>
          <td>調整 MaxMetaspaceSize、限制 thread 數</td>
      </tr>
      <tr>
          <td>冷啟動節點上服務啟動超慢</td>
          <td>image pull 時間在啟動時間中占比高</td>
          <td>壓縮 image 體積、啟用 image cache</td>
      </tr>
      <tr>
          <td>rollback 後行為跟上次部署不同</td>
          <td>建置不可重現、tag 覆蓋</td>
          <td>改用 commit SHA 標記、鎖定依賴版本</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>Container 常被簡化成「打包完就好」的步驟，結果是部署風險被後移到 rollout 階段。runtime 產物穩定性不足時，後續 probe、canary、rollback 都只能被動補救。</p>
<p>把資源限制設成平台預設值，也常造成高峰期不穩。限制應反映服務真實耗用模式，不應只追求表面資源利用率。</p>
<p>把 <code>latest</code> tag 當成版本標記，會讓 rollback 指向無法預測的 image。image tag 在 registry 上是 mutable——同一個 tag 可以被覆蓋指向新 image。用 immutable tag（commit SHA、content digest）才能保證 rollback 的確定性。</p>
<p>把所有配置都用環境變數注入，會讓設定變更跟 image 部署綁在一起。需要動態更新的設定（feature flag、rate limit 閾值）應該用 ConfigMap mount 或外部 config store，讓設定變更不需要 pod restart。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>runtime 穩定性可用 <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> 回寫。先看遷移期內啟動行為與資源限制如何影響切流，再對照本章檢查 image、entrypoint、limit 與 config 相容窗口。這個案例主要支撐的是「執行環境可重現性」判讀——遷移到新叢集時，image 不變但 runtime 環境變了（node OS、container runtime 版本、network plugin），runtime 穩定性的前提是 image 本身不依賴特定宿主環境的行為。</p>
<p><a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5 Miro managed EKS 遷移</a> 從另一個角度支撐：managed 平台接管 runtime 基礎設施後，container runtime 版本升級由平台控制，團隊要能驗證自家 image 在新 runtime 版本下行為一致。</p>
<p>若同版容器在不同節點出現分歧行為，先追建置來源與 runtime config 版本鏈，確認是依賴漂移還是環境漂移，再把關鍵證據收斂到 <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 Observability Evidence Package</a>。不直接支撐 service discovery TTL 或 queue replay 邏輯；若根因在定位鏈路或重播流程，應轉到 5.4 或 3.4。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 5.2 的交接：部署批次與探針策略回到 <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 的交接：流量進出與連線收斂回到 <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.6 的交接：startup / readiness / drain 的生命週期定義回到 <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 如何組成平台生命週期合約。">Platform Lifecycle Contract</a>。</li>
<li>與 4.20 的交接：啟動與資源證據回到 <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 的交接：放行與回退條件回到 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a>。</li>
<li>與 7.3 的交接：image 安全基線與攻擊面回到 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把 runtime 行為接到部署收斂，接著讀 <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>。要看切流與退場條件，接著讀 <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/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>。</p>
]]></content:encoded></item><item><title>5.2 Kubernetes 部署策略</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/</guid><description>&lt;p>Kubernetes 部署策略（Kubernetes deployment strategy）的核心責任是把服務版本切換做成可預測流程。Deployment 把副本數、健康訊號、流量承接、設定變更與回退條件組成同一條交付路徑。&lt;/p>
&lt;h2 id="deploymentreplica-與-rollout">deployment、replica 與 rollout&lt;/h2>
&lt;p>Deployment 的責任是宣告目標狀態：期望副本數、版本、更新策略。rollout 的責任是把現況收斂到目標狀態，並在過程中維持可服務能力。這兩者分開理解後，才能在異常時判斷是目標設定問題，還是收斂過程問題。&lt;/p>
&lt;p>rolling update 常用來降低單次切換風險。rolling update 的判讀重點是批次大小與節奏：每批新增多少新副本、每批回收多少舊副本、每批觀察多長時間。這些參數以服務容量曲線與回退時間目標校準、名稱本身只是工具標籤、不是判讀條件。&lt;/p>
&lt;h2 id="probe-對齊服務生命週期">probe 對齊服務生命週期&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe&lt;/a> 要對齊服務生命週期，不同 probe 有不同責任：&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/startup-probe/" data-link-title="Startup Probe" data-link-desc="保護慢啟動服務不被 liveness probe 過早重啟的探針">startup probe&lt;/a>：確認服務啟動完成，避免慢啟動服務被過早重啟。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> probe：確認服務可安全接流量。&lt;/li>
&lt;li>liveness probe：確認服務仍可維持基本運作，必要時觸發重建。&lt;/li>
&lt;/ol>
&lt;p>probe 設計若只回傳固定成功，rollout 期間會出現「容器在線但服務未就緒」的流量抖動。穩定做法是讓 readiness 反映依賴就緒條件，例如資料庫連線池、必要配置、關鍵背景任務狀態。&lt;/p>
&lt;h3 id="startup-probe-設計注意事項">Startup probe 設計注意事項&lt;/h3>
&lt;p>startup probe 跟 &lt;code>initialDelaySeconds&lt;/code> 解決同一個問題（避免慢啟動服務被 liveness 殺掉），但機制不同。&lt;code>initialDelaySeconds&lt;/code> 是 liveness / readiness probe 的延遲啟動——在等待期間 probe 完全不跑，無法觀測啟動進度。startup probe 在啟動期間持續探測，一旦成功就交棒給 liveness / readiness，啟動失敗時能更快偵測到。&lt;/p>
&lt;p>startup probe 的總容忍時間 = &lt;code>failureThreshold × periodSeconds&lt;/code>。例如 &lt;code>failureThreshold: 30, periodSeconds: 10&lt;/code> 給服務 300 秒啟動窗口。設計時先量測服務在最差情境下的啟動時間（冷啟動 + image pull + 依賴連線建立），再加 20-30% headroom 作為總容忍時間。&lt;/p>
&lt;h3 id="readiness-probe-的深度選擇">Readiness probe 的深度選擇&lt;/h3>
&lt;p>readiness probe 的檢查深度決定它能攔截多少「可啟動但不可服務」的狀態。三個常見層級：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Port check&lt;/strong>（TCP probe）：確認進程在監聽。最淺，無法偵測依賴未就緒。適合依賴簡單、啟動快的服務。&lt;/li>
&lt;li>&lt;strong>Dependency check&lt;/strong>（HTTP endpoint 檢查必要依賴）：確認資料庫連線池、cache 連線可用。涵蓋多數「啟動完但依賴不通」的場景。常用做法是在 &lt;code>/ready&lt;/code> endpoint 內驗證必要依賴的連線狀態。&lt;/li>
&lt;li>&lt;strong>Deep health&lt;/strong>（業務路徑驗證）：執行一次簡化的業務查詢確認端到端通路。最深但代價最高——probe 本身消耗資源，且可能被下游延遲拖慢導致 readiness 抖動。&lt;/li>
&lt;/ol>
&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 Readiness 設計的核心取捨&lt;/a>。&lt;/p>
&lt;h2 id="config-rollout-與版本相容">config rollout 與版本相容&lt;/h2>
&lt;p>&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> 需要和應用版本一起治理。設定先行、版本後行，或版本先行、設定後行，都要保留相容窗口。相容窗口存在時，才有漸進 rollout 與快速回退空間。&lt;/p>
&lt;p>跨版本配置遷移要先定義停止條件：錯誤率上升、延遲尖峰、關鍵路徑失敗或下游壓力超標。停止條件明確後，部署決策才能一致。&lt;/p>
&lt;h3 id="n-1-相容與-feature-flag-gating">N-1 相容與 Feature Flag Gating&lt;/h3>
&lt;p>版本相容窗口的操作基線是 N-1 相容：版本 N 的程式碼可以處理版本 N-1 的設定，反之亦然。這讓 rollback 從「版本 + config 必須同時回退」降級成「版本先回退、config 稍後再處理」，回退操作的原子性要求降低。&lt;/p>
&lt;p>N-1 相容的實作通常搭配 feature flag gating：新功能在程式碼中預設關閉，先部署程式碼（版本 N 上線但新功能 off），確認版本穩定後再開啟 feature flag。這讓版本部署跟功能啟用分成兩個獨立決策，rollback 時只需關 flag 而不必回退版本。&lt;/p>
&lt;p>N-1 相容窗口的壽命要有明確終點。長期維護雙版本相容會累積技術債——舊欄位不能刪、舊路徑不能移除。穩定做法是在 rollout 完成 + 觀測確認穩定後設定移除 deadline，把 N-1 相容視為暫時性保護而非永久設計。設定注入方式與版本追蹤見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 配置注入方式與取捨&lt;/a>。&lt;/p>
&lt;h2 id="autoscaling-與部署策略協同">Autoscaling 與部署策略協同&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/autoscaling/" data-link-title="Autoscaling" data-link-desc="說明系統如何依負載自動調整服務實例數量">autoscaling&lt;/a> 在部署期間扮演容量緩衝角色。部署批次若超過服務可承受變動幅度，autoscaling 會被動補償並延長收斂時間。穩定做法是讓 rollout 節奏與容量策略同時設計：先保證服務穩態，再提高切換速度。&lt;/p></description><content:encoded><![CDATA[<p>Kubernetes 部署策略（Kubernetes deployment strategy）的核心責任是把服務版本切換做成可預測流程。Deployment 把副本數、健康訊號、流量承接、設定變更與回退條件組成同一條交付路徑。</p>
<h2 id="deploymentreplica-與-rollout">deployment、replica 與 rollout</h2>
<p>Deployment 的責任是宣告目標狀態：期望副本數、版本、更新策略。rollout 的責任是把現況收斂到目標狀態，並在過程中維持可服務能力。這兩者分開理解後，才能在異常時判斷是目標設定問題，還是收斂過程問題。</p>
<p>rolling update 常用來降低單次切換風險。rolling update 的判讀重點是批次大小與節奏：每批新增多少新副本、每批回收多少舊副本、每批觀察多長時間。這些參數以服務容量曲線與回退時間目標校準、名稱本身只是工具標籤、不是判讀條件。</p>
<h2 id="probe-對齊服務生命週期">probe 對齊服務生命週期</h2>
<p><a href="/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe</a> 要對齊服務生命週期，不同 probe 有不同責任：</p>
<ol>
<li><a href="/blog/backend/knowledge-cards/startup-probe/" data-link-title="Startup Probe" data-link-desc="保護慢啟動服務不被 liveness probe 過早重啟的探針">startup probe</a>：確認服務啟動完成，避免慢啟動服務被過早重啟。</li>
<li><a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> probe：確認服務可安全接流量。</li>
<li>liveness probe：確認服務仍可維持基本運作，必要時觸發重建。</li>
</ol>
<p>probe 設計若只回傳固定成功，rollout 期間會出現「容器在線但服務未就緒」的流量抖動。穩定做法是讓 readiness 反映依賴就緒條件，例如資料庫連線池、必要配置、關鍵背景任務狀態。</p>
<h3 id="startup-probe-設計注意事項">Startup probe 設計注意事項</h3>
<p>startup probe 跟 <code>initialDelaySeconds</code> 解決同一個問題（避免慢啟動服務被 liveness 殺掉），但機制不同。<code>initialDelaySeconds</code> 是 liveness / readiness probe 的延遲啟動——在等待期間 probe 完全不跑，無法觀測啟動進度。startup probe 在啟動期間持續探測，一旦成功就交棒給 liveness / readiness，啟動失敗時能更快偵測到。</p>
<p>startup probe 的總容忍時間 = <code>failureThreshold × periodSeconds</code>。例如 <code>failureThreshold: 30, periodSeconds: 10</code> 給服務 300 秒啟動窗口。設計時先量測服務在最差情境下的啟動時間（冷啟動 + image pull + 依賴連線建立），再加 20-30% headroom 作為總容忍時間。</p>
<h3 id="readiness-probe-的深度選擇">Readiness probe 的深度選擇</h3>
<p>readiness probe 的檢查深度決定它能攔截多少「可啟動但不可服務」的狀態。三個常見層級：</p>
<ol>
<li><strong>Port check</strong>（TCP probe）：確認進程在監聽。最淺，無法偵測依賴未就緒。適合依賴簡單、啟動快的服務。</li>
<li><strong>Dependency check</strong>（HTTP endpoint 檢查必要依賴）：確認資料庫連線池、cache 連線可用。涵蓋多數「啟動完但依賴不通」的場景。常用做法是在 <code>/ready</code> endpoint 內驗證必要依賴的連線狀態。</li>
<li><strong>Deep health</strong>（業務路徑驗證）：執行一次簡化的業務查詢確認端到端通路。最深但代價最高——probe 本身消耗資源，且可能被下游延遲拖慢導致 readiness 抖動。</li>
</ol>
<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 Readiness 設計的核心取捨</a>。</p>
<h2 id="config-rollout-與版本相容">config rollout 與版本相容</h2>
<p><a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout</a> 需要和應用版本一起治理。設定先行、版本後行，或版本先行、設定後行，都要保留相容窗口。相容窗口存在時，才有漸進 rollout 與快速回退空間。</p>
<p>跨版本配置遷移要先定義停止條件：錯誤率上升、延遲尖峰、關鍵路徑失敗或下游壓力超標。停止條件明確後，部署決策才能一致。</p>
<h3 id="n-1-相容與-feature-flag-gating">N-1 相容與 Feature Flag Gating</h3>
<p>版本相容窗口的操作基線是 N-1 相容：版本 N 的程式碼可以處理版本 N-1 的設定，反之亦然。這讓 rollback 從「版本 + config 必須同時回退」降級成「版本先回退、config 稍後再處理」，回退操作的原子性要求降低。</p>
<p>N-1 相容的實作通常搭配 feature flag gating：新功能在程式碼中預設關閉，先部署程式碼（版本 N 上線但新功能 off），確認版本穩定後再開啟 feature flag。這讓版本部署跟功能啟用分成兩個獨立決策，rollback 時只需關 flag 而不必回退版本。</p>
<p>N-1 相容窗口的壽命要有明確終點。長期維護雙版本相容會累積技術債——舊欄位不能刪、舊路徑不能移除。穩定做法是在 rollout 完成 + 觀測確認穩定後設定移除 deadline，把 N-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 配置注入方式與取捨</a>。</p>
<h2 id="autoscaling-與部署策略協同">Autoscaling 與部署策略協同</h2>
<p><a href="/blog/backend/knowledge-cards/autoscaling/" data-link-title="Autoscaling" data-link-desc="說明系統如何依負載自動調整服務實例數量">autoscaling</a> 在部署期間扮演容量緩衝角色。部署批次若超過服務可承受變動幅度，autoscaling 會被動補償並延長收斂時間。穩定做法是讓 rollout 節奏與容量策略同時設計：先保證服務穩態，再提高切換速度。</p>
<p>長連線服務或有大量背景任務的 workload，通常需要比 stateless API 更保守的 rollout 策略，並額外搭配 drain 與 reconnect 設計。</p>
<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 K8s 叢集擴縮演進</a>：揭露「擴縮策略版本化跟可回放」「不同 workload 區分擴縮政策」「容量治理跟事故指標綁定」三個方向。以下基於通用工程知識展開。</p>
<p>可重複套用的做法：</p>
<ol>
<li><strong>擴縮策略進 IaC</strong>：HPA / VPA / Karpenter / Cluster Autoscaler 的配置都進 git、變更走 release flow、避免手動調整在事故後被遺忘。IaC + 自動化的 ownership 邊界見 [5.7 <a href="/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane</a> boundary](/backend/05-deployment-platform/traffic-config-control-plane-boundary/)。</li>
<li><strong>workload 分群擴縮</strong>：stateless API、長連線服務、batch job、background worker 對擴縮的需求不同。把不同 workload 用不同 namespace + 不同 autoscaler policy 隔離，避免一套規則套全部。</li>
<li><strong>擴縮事件接事故指標</strong>：HPA 觸發、scale-up 延遲、scale-down 過快、cluster autoscaler 加 node 失敗，都該在事故 timeline 上可見。回到 <a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13 service topology</a> 的擴縮事件 vs 事故區分。</li>
</ol>
<h2 id="分階段平台遷移">分階段平台遷移</h2>
<p>平台遷移的本質是流量跟依賴的分段切換。遷移期內新舊叢集同時存在，rollout 策略要把跨叢集流量切換納入批次節奏、視為連續多批決策。本段聚焦流量 / 依賴切換時序；遷移期的團隊職責邊界重訂見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#managed-%e5%b9%b3%e5%8f%b0%e8%b7%9f%e5%9c%98%e9%9a%8a%e8%81%b7%e8%b2%ac%e9%82%8a%e7%95%8c" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Managed 平台跟團隊職責邊界</a>。</p>
<p>對應 <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 → EKS</a>：揭露「零停機遷移要把切換做成分段策略」「難點通常在跨叢集服務依賴跟流量切換、不在 Kubernetes API 本身」。對應 <a href="/blog/backend/05-deployment-platform/cases/mobileye-workloads-to-eks/" data-link-title="5.C4 Mobileye：Workloads 遷移到 EKS" data-link-desc="大規模工作負載遷移到 managed Kubernetes 的分段治理案例。">5.C4 Mobileye workloads 遷移</a>：揭露「分批遷移 workload、保留觀測對照」「明確切換 / 回退條件」「新平台先驗證容量跟恢復節奏」。以下基於通用工程知識展開。</p>
<p>可重複套用的分階段做法：</p>
<ol>
<li><strong>新叢集 + 共通配置基線</strong>：先在新叢集上建立跟舊叢集對等的配置基線（namespace、ResourceQuota、NetworkPolicy、Ingress class、storage class），讓 workload 可以無縫部署。</li>
<li><strong>小流量先導服務</strong>：選擇影響面小、依賴單純的服務作為先導，先在新叢集跑完整 deployment cycle（rollout、drain、rollback 驗證）、累積信心後再擴大。</li>
<li><strong>可控流量分批切換</strong>：用 DNS 加權、service mesh 流量切分或 LB 規則把流量分批從舊叢集導到新叢集。每批切換後驗證 SLI 偏差、再進下一批。</li>
<li><strong>每批保留回退路徑</strong>：舊叢集服務不立即下線，保留作為回退目標。回退條件先驗證（rollback script、流量切回 DNS / LB 規則），再開始下一批切換。</li>
</ol>
<p>延伸 5.C1 揭露的「跨叢集服務依賴是難點」、5.C10 中型組織判讀「服務本身切過去了、但資料面、認證面、觀測面還沒同步」也指向同類問題。跨叢集遷移最容易出的事故是「服務切過去了、依賴沒切過去」。Database、cache、message queue、observability pipeline、auth service 的切換時機要分別規劃，避免應用層在新叢集但仍跨網路打舊叢集的依賴，造成隱性 latency 或單點失效。規模差異下的同類問題見 <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>
<h2 id="大規模-k8s-的設計取捨">大規模 K8s 的設計取捨</h2>
<p>K8s 在不同規模下的設計取捨會明顯分歧。小規模叢集追求簡單跟低運維成本，大規模叢集追求隔離跟自動化治理。同一套部署策略放到不同規模會在某個量級開始失效。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games：246 個 EKS cluster</a>：揭露架構決策從 multi-tenant cluster 改成 single-tenant per game、Karpenter + Terraform 的 cluster 級自動化、35ms 延遲門檻 + Local Zones / Outposts 區域部署（case 中「35ms 反推 region 部署」屬作者判讀層、本章引用此推論）。對應 <a href="/blog/backend/09-performance-capacity/cases/gcp-130k-node-gke-cluster/" data-link-title="9.C34 GCP：130,000-node GKE cluster 的工程極限" data-link-desc="Google 用單一 GKE control plane 跑 13 萬個 node、AI workload &#43; 1000 Pods/sec 創建吞吐">9.C34 GCP 130,000-node GKE cluster</a>：揭露 control plane 極限取決於 storage backend（GCP 用 Spanner 替代 etcd）、AI workload 跟 web workload 容量規劃差異。對應 <a href="/blog/backend/09-performance-capacity/cases/maersk-bosch-azure-aks/" data-link-title="9.C33 Maersk &#43; Bosch：傳統產業在 Azure AKS 上的微服務治理" data-link-desc="全球海運 Maersk 跟 Bosch 智慧建築把 AKS 當微服務治理基礎、釋放工程資源做業務功能">9.C33 Maersk + Bosch AKS</a>：揭露 Maersk 工程訴求引語「focus on things that makes the most business impact」、傳統產業上 K8s 動機是治理一致性（作者判讀）、適合 single-cluster-multi-namespace。</p>
<p>可重複套用的取捨判讀：</p>
<ol>
<li><strong>single-tenant per workload vs single-cluster multi-namespace</strong>：高隔離需求（每個 workload 失效不能影響其他）、高延遲敏感度（需 region cluster）→ 多 cluster；治理一致性訴求（統一 release flow、合規邊界）→ 單一 cluster 多 namespace。</li>
<li><strong>Cluster 容量極限取決於 control plane</strong>：data plane（worker nodes）擴容容易、control plane（API server、etcd / storage）擴容難、瓶頸通常在 control plane。etcd 撐 5K-10K node 後吃力、需要替換 storage backend（Spanner / PostgreSQL / 自家 KV）才能撐萬級節點（見 <a href="/blog/backend/09-performance-capacity/cases/gcp-130k-node-gke-cluster/" data-link-title="9.C34 GCP：130,000-node GKE cluster 的工程極限" data-link-desc="Google 用單一 GKE control plane 跑 13 萬個 node、AI workload &#43; 1000 Pods/sec 創建吞吐">9.C34</a>）。control plane 的 ownership 邊界由 <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 control plane boundary</a> 處理。</li>
<li><strong>Multi-cluster 治理需要 IaC + 自動化</strong>：Terraform / Crossplane / Cluster API + Karpenter / Cluster Autoscaler 是基本工具。手動管理超過數十個 cluster 不可行。</li>
<li><strong>AI workload 跟 web workload 容量規劃完全不同</strong>：AI workload 短時間爆量創建 Pods（萬級 / 秒）、preempt 頻繁；web workload 節點生命週期長、變動緩。把 web 經驗套到 AI workload 容量規劃會嚴重低估壓力。</li>
</ol>
<p>關鍵判讀是「先決定 cluster 是隔離單位還是治理單位」。Riot Games 把 cluster 當隔離單位（246 個獨立 cluster），Maersk / Bosch 把 cluster 當治理單位（單 cluster 多 namespace）。同一個工具兩種用法、決定整體運維模型。</p>
<p>對應 <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：EKS 平台整併與標準化</a>：揭露多叢集整併到單一控制面的場景、跟 Maersk-Bosch 同屬「治理一致性」取捨方向（治理單位優先於隔離單位）。Condé Nast 的整併路徑是「盤點既有叢集差異 → 建立統一平台基線 → 藍綠或漸進切換業務流量」、對應前面「分階段平台遷移」段的批次節奏。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>rollout 卡在中段且新副本反覆重啟</td>
          <td>probe 與啟動路徑不匹配</td>
          <td>校正 startup/readiness 探針與超時參數</td>
      </tr>
      <tr>
          <td>rollout 完成後延遲與錯誤率短期上升</td>
          <td>批次切換過快或下游未對齊</td>
          <td>降低批次、延長觀察窗口、回退再重試</td>
      </tr>
      <tr>
          <td>config 變更後特定路徑失敗率飆升</td>
          <td>設定與版本相容窗口不足</td>
          <td>啟動回退配置、補雙軌相容</td>
      </tr>
      <tr>
          <td>autoscaling 在部署期間頻繁抖動</td>
          <td>容量閾值與 rollout 節奏衝突</td>
          <td>分離部署窗口與擴縮窗口、調整資源策略</td>
      </tr>
      <tr>
          <td>長連線服務切版後 reconnect storm</td>
          <td>drain 與連線生命週期控制不足</td>
          <td>拉長 drain、分批切流、校正 timeout</td>
      </tr>
      <tr>
          <td>跨叢集遷移後特定路徑 latency 升高</td>
          <td>應用切過去但依賴未切、跨網路</td>
          <td>規劃依賴切換時機、分批一致</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 Kubernetes 部署看成 YAML 套版，會忽略服務語意差異。相同 deployment 參數在不同服務上，可能代表完全不同風險。</p>
<p>把 probe 當成健康檢查 URL，會讓服務在邊界條件下過早接流量。probe 的工程價值在於反映服務真實可用條件。</p>
<p>把 cluster scale-up 想成「加 node 就好」也是常見誤判。當 cluster 規模超過 control plane 預設邊界，etcd / API server 會先撐不住，加 node 反而加重 control plane 負擔。</p>
<h2 id="案例回寫">案例回寫</h2>
<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> 做回寫。先看事件中的失敗是在 rollout 批次、probe 判斷、還是 drain 時序，再對照本章的 rollout 節奏與停止條件。</p>
<p>這個案例主要支撐的是「部署批次與切換時序」判讀，不直接支撐資料庫交易切分或 consumer 冪等；若問題落在提交一致性或重播補償，應轉到 1.3 或 3.4。</p>
<p>若版本已切換但錯誤率延遲上升，先回到 probe 與 config 相容窗口，再把證據欄位接到 <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 Observability Evidence Package</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>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>Kubernetes 部署策略要和觀測、驗證、事故流程同時對齊。</p>
<ol>
<li>與 5.6 的交接：startup / readiness / liveness / drain 的生命週期定義回到 <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 如何組成平台生命週期合約。">Platform Lifecycle Contract</a>。</li>
<li>與 5.1 的交接：image、entrypoint、resource limit 的 runtime 層回到 <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.3 的交接：流量承接與退出落在 <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.4 的交接：endpoint 註冊與摘除回到 <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>。</li>
<li>與 5.7 的交接：control plane 跟 data plane 邊界落在 <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 與管理面如何分責任與回退。">Traffic、Config 與 Control Plane Boundary</a>。</li>
<li>與 4.20 的交接：版本切換證據進入 <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 的交接：放行與停損條件進入 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a>。</li>
<li>與 8.19 的交接：部署中止與回退判斷進入 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把部署與流量切換一起治理，接著讀 <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>。要看切換失敗與回退判讀，接著讀 <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>。要看大規模 K8s 容量設計，接著讀 <a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games</a> 跟 <a href="/blog/backend/09-performance-capacity/cases/gcp-130k-node-gke-cluster/" data-link-title="9.C34 GCP：130,000-node GKE cluster 的工程極限" data-link-desc="Google 用單一 GKE control plane 跑 13 萬個 node、AI workload &#43; 1000 Pods/sec 創建吞吐">9.C34 GCP 130K-node</a>。</p>
]]></content:encoded></item><item><title>5.3 load balancer 合約</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/load-balancer-contract/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/load-balancer-contract/</guid><description>&lt;p>流量平衡合約（load balancer contract）的核心責任是定義平台何時把流量交給服務，以及服務何時安全退出流量。這份合約一旦模糊，部署、擴容、回退與事故處理都會出現同型問題。&lt;/p>
&lt;h2 id="contract-組成">contract 組成&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-balancer-contract/" data-link-title="Load Balancer Contract" data-link-desc="說明服務與負載平衡器之間的流量與健康檢查約定">Load Balancer Contract&lt;/a> 可以拆成四個部分：&lt;/p>
&lt;ol>
&lt;li>routing contract：哪些路徑導向哪些服務，如何處理權重與版本。&lt;/li>
&lt;li>health contract：哪些訊號代表可接流量，何時摘除節點。&lt;/li>
&lt;li>connection contract：長短連線的 idle timeout、keepalive、重試規則。&lt;/li>
&lt;li>drain contract：版本切換時如何讓 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight&lt;/a> request 安全收斂。&lt;/li>
&lt;/ol>
&lt;p>這四個部分共同定義 rollout 的穩定性。服務端 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 與平台端健康檢查要對位，否則會出現「服務已啟動但尚未可服務」的切換抖動。&lt;/p>
&lt;h2 id="draining-與-shutdown">draining 與 shutdown&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining&lt;/a> 的責任是讓舊實例在下線前完成現有請求。drain 視窗的 workload 分類詳見 &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>，本段聚焦 LB 如何配合 drain：短請求 API 的 drain 視窗可較短；長連線、串流或 websocket 場景需要更長窗口與明確 reconnect 策略。&lt;/p>
&lt;p>部署流程中，LB 摘流量、服務停止接新請求、服務完成在途請求、實例退出，這四步要有固定順序。順序穩定後，rollback 才能在同一套機制下運作。&lt;/p>
&lt;h2 id="timeout-與-sticky-session">timeout 與 sticky session&lt;/h2>
&lt;p>idle &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 是連線資源與使用者體驗的平衡點。timeout 太短會增加重連與錯誤，太長會占用連線與資源。設定時依請求型態與峰值流量校準、按 SLI 訊號迭代閾值。&lt;/p>
&lt;h3 id="timeout-層級串聯">Timeout 層級串聯&lt;/h3>
&lt;p>一條請求路徑上的 timeout 分佈在多個層級，每層各自有預設值。全路徑的 timeout 設計原則是由外到內遞減：外層（離使用者近）的 timeout 要大於內層（離資料源近），否則外層先放棄，內層還在處理一個已經沒人等的請求。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層級&lt;/th>
 &lt;th>典型 timeout 範圍&lt;/th>
 &lt;th>設定位置&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Client / Browser&lt;/td>
 &lt;td>30-120 秒&lt;/td>
 &lt;td>前端 fetch / axios / SDK 設定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CDN edge&lt;/td>
 &lt;td>5-30 秒&lt;/td>
 &lt;td>CDN vendor 設定（Cloudflare / CloudFront）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Load balancer&lt;/td>
 &lt;td>30-60 秒&lt;/td>
 &lt;td>LB idle timeout / request timeout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application&lt;/td>
 &lt;td>5-30 秒&lt;/td>
 &lt;td>HTTP server read/write timeout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Database / Cache&lt;/td>
 &lt;td>1-5 秒&lt;/td>
 &lt;td>連線池 query timeout / connect timeout&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的每一層 timeout 都要比它的下一層大。如果 LB timeout 30 秒但 application 設了 60 秒，LB 會在 30 秒回 504 給使用者，但 application 仍然持有連線等 DB 回應——佔用連線資源卻無法交付結果。&lt;/p>
&lt;p>timeout 設計的常見失誤是只調 LB 層：團隊看到使用者回報 timeout，直接把 LB timeout 從 30 秒調到 120 秒。結果是慢請求佔用 LB 連線更久、連線池被慢請求填滿、其他正常請求也開始排隊 timeout。穩定做法是先在 application 或 DB 層找出延遲根因，而非放大外層 timeout 來「等更久」。&lt;/p>
&lt;p>&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> 適合需要短期會話一致性的場景，但它會提高特定節點負載不均與失效轉移成本。採用 sticky policy 前要先定義會話狀態落點與失效時的回復路徑。&lt;/p></description><content:encoded><![CDATA[<p>流量平衡合約（load balancer contract）的核心責任是定義平台何時把流量交給服務，以及服務何時安全退出流量。這份合約一旦模糊，部署、擴容、回退與事故處理都會出現同型問題。</p>
<h2 id="contract-組成">contract 組成</h2>
<p><a href="/blog/backend/knowledge-cards/load-balancer-contract/" data-link-title="Load Balancer Contract" data-link-desc="說明服務與負載平衡器之間的流量與健康檢查約定">Load Balancer Contract</a> 可以拆成四個部分：</p>
<ol>
<li>routing contract：哪些路徑導向哪些服務，如何處理權重與版本。</li>
<li>health contract：哪些訊號代表可接流量，何時摘除節點。</li>
<li>connection contract：長短連線的 idle timeout、keepalive、重試規則。</li>
<li>drain contract：版本切換時如何讓 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a> request 安全收斂。</li>
</ol>
<p>這四個部分共同定義 rollout 的穩定性。服務端 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 與平台端健康檢查要對位，否則會出現「服務已啟動但尚未可服務」的切換抖動。</p>
<h2 id="draining-與-shutdown">draining 與 shutdown</h2>
<p><a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a> 的責任是讓舊實例在下線前完成現有請求。drain 視窗的 workload 分類詳見 <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>，本段聚焦 LB 如何配合 drain：短請求 API 的 drain 視窗可較短；長連線、串流或 websocket 場景需要更長窗口與明確 reconnect 策略。</p>
<p>部署流程中，LB 摘流量、服務停止接新請求、服務完成在途請求、實例退出，這四步要有固定順序。順序穩定後，rollback 才能在同一套機制下運作。</p>
<h2 id="timeout-與-sticky-session">timeout 與 sticky session</h2>
<p>idle <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 是連線資源與使用者體驗的平衡點。timeout 太短會增加重連與錯誤，太長會占用連線與資源。設定時依請求型態與峰值流量校準、按 SLI 訊號迭代閾值。</p>
<h3 id="timeout-層級串聯">Timeout 層級串聯</h3>
<p>一條請求路徑上的 timeout 分佈在多個層級，每層各自有預設值。全路徑的 timeout 設計原則是由外到內遞減：外層（離使用者近）的 timeout 要大於內層（離資料源近），否則外層先放棄，內層還在處理一個已經沒人等的請求。</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>典型 timeout 範圍</th>
          <th>設定位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Client / Browser</td>
          <td>30-120 秒</td>
          <td>前端 fetch / axios / SDK 設定</td>
      </tr>
      <tr>
          <td>CDN edge</td>
          <td>5-30 秒</td>
          <td>CDN vendor 設定（Cloudflare / CloudFront）</td>
      </tr>
      <tr>
          <td>Load balancer</td>
          <td>30-60 秒</td>
          <td>LB idle timeout / request timeout</td>
      </tr>
      <tr>
          <td>Application</td>
          <td>5-30 秒</td>
          <td>HTTP server read/write timeout</td>
      </tr>
      <tr>
          <td>Database / Cache</td>
          <td>1-5 秒</td>
          <td>連線池 query timeout / connect timeout</td>
      </tr>
  </tbody>
</table>
<p>這張表的每一層 timeout 都要比它的下一層大。如果 LB timeout 30 秒但 application 設了 60 秒，LB 會在 30 秒回 504 給使用者，但 application 仍然持有連線等 DB 回應——佔用連線資源卻無法交付結果。</p>
<p>timeout 設計的常見失誤是只調 LB 層：團隊看到使用者回報 timeout，直接把 LB timeout 從 30 秒調到 120 秒。結果是慢請求佔用 LB 連線更久、連線池被慢請求填滿、其他正常請求也開始排隊 timeout。穩定做法是先在 application 或 DB 層找出延遲根因，而非放大外層 timeout 來「等更久」。</p>
<p><a href="/blog/backend/knowledge-cards/sticky-session/" data-link-title="Sticky Session" data-link-desc="說明同一 client 如何在一段時間內持續命中同一個後端實例">sticky session</a> 適合需要短期會話一致性的場景，但它會提高特定節點負載不均與失效轉移成本。採用 sticky policy 前要先定義會話狀態落點與失效時的回復路徑。</p>
<h3 id="lb--cdn-連線生命週期協調">LB + CDN 連線生命週期協調</h3>
<p>當 LB 上游有 <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 策略">CDN</a> 時、兩層的 timeout / retry 行為要對齊、否則會出現「使用者已經 timeout 但 origin 還在處理」這類雙層不一致：</p>
<ul>
<li><strong>CDN edge timeout</strong> 通常比 origin LB timeout 短（5-30 秒）— edge 認定 origin 慢就放棄。若 origin LB timeout 是 60 秒、edge 在 30 秒已放棄回 504、origin 還在處理一個沒人在意的 request。應對齊兩邊的 timeout 上限。</li>
<li><strong>CDN retry policy</strong> 在 edge miss 後若拿不到 origin response、預設不會重試（避免雙倍 origin 流量）— LB 端的 idle timeout 設計要假設「只有一次機會」、不依賴上游重試</li>
<li><strong>長連線（WebSocket、SSE、gRPC）通常繞過 CDN</strong> — 直接連到 origin LB。這些連線的 idle timeout 跟一般 HTTP 不同、要單獨配置</li>
<li><strong>Edge cache HIT 時 LB 完全沒收到 request</strong> — 容量規劃時要把 cache hit ratio 算進 origin RPS、不是用使用者 RPS 直接 size LB</li>
</ul>
<p>詳見 <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> 的 origin protection 段。</p>
<h2 id="切流失敗的回退判讀">切流失敗的回退判讀</h2>
<p>切流失敗的回退判讀第一步是先分辨「平台問題」跟「流量生命週期問題」、再決定回退手法。平台問題用重啟服務恢復、流量生命週期問題用凍結切換並等待震盪收斂。回退手法錯位會把事故推進第二階段。</p>
<p>切流失敗的本質是 connection lifecycle 跟切換時序錯位、平台元件本身往往是健康的。對應 <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 管理錯位、重啟動作會放大震盪。以下基於通用工程知識展開回退節奏。</p>
<p>回退節奏有兩個時序階段、性質不同。</p>
<p><strong>第一階段：先讓震盪不擴大</strong>。發現切流失敗的第一動作是凍結 rollout（不再擴大切換範圍）跟恢復舊入口權重（把 LB 規則 / DNS 加權 / service mesh 流量切回舊版本主導）。新版本不立即關閉、保留作為對照證據。這個階段的目標是穩定當前狀態、為後續分析爭取時間、所有動作要在分鐘級內完成。</p>
<p><strong>第二階段：再讓系統可恢復</strong>。震盪不擴大後、進入「等待 + 修正」狀態。長連線跟 reconnect 風暴需要時間消化、盲目重啟新版本實例會把重連集中在新一輪實例上、造成 thundering herd。觀察連線數、reconnect rate、5xx 趨勢回到 baseline 是進入修正階段的訊號。修正動作聚焦於 drain window、idle timeout、health check、client retry 之間的節奏錯位、找出後修正、重新進入小範圍驗證。這個階段的時間尺度通常是小時級、不能用第一階段的緊急節奏對待。</p>
<p>兩階段時序不能合併。把第一階段（凍結 + 切回）跟第二階段（等待 + 修正）並列執行、會在連線尚未穩定時嘗試修正、造成第二次震盪。</p>
<p>回退時最常見的誤判是「LB 顯示新節點 healthy = 服務可服務」。LB 的健康判斷通常是定期 health check 通過，跟「該節點能承受重連潮」是不同問題。事故中要把這兩個訊號分開看：節點層健康（health check pass）、連線層健康（reconnect rate、長連線錯誤率、tail latency）。</p>
<h2 id="切流告警條件">切流告警條件</h2>
<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> 的「部署專屬告警條件」段：揭露切流期告警的三個核心訊號（批次內 5xx 突增、長連線重連率快速上升、rollback time 超過既定 RTO）。本段在 case 三條基礎上補第 4 條（per-version error rate 偏離）與操作建議。</p>
<p>切流期告警的核心責任是對應切流批次節奏、跟日常閾值分離。日常閾值在切流期會被切換本身的短暫波動觸發、變成 alert noise；切流期需要更嚴格的「批次內偏差」訊號。</p>
<p>可操作的切流期告警條件：</p>
<ul>
<li><strong>批次內 5xx 異常升高</strong>：當前批次相對於前一批的 5xx 升幅超過閾值、停止下一批。</li>
<li><strong>長連線重連率飆升</strong>：reconnect rate 超過 baseline N 倍、暗示 drain / timeout 錯位。</li>
<li><strong>回退時間超過 RTO</strong>：執行回退後恢復時間超過既定 RTO、升級為事故等級。</li>
<li><strong>per-version error rate 偏離</strong>：新舊版本 error rate 差距超過閾值、不收斂（屬本章補強、case 未明示）。</li>
</ul>
<p>這些告警的閾值要在 release plan 中先定義、進事故時直接套用、避免臨時拍定。把切流告警跟一般日常告警分流到不同 channel，避免事故團隊在切流期被日常 noise 淹沒。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>rollout 期間 5xx 上升且集中在舊版本</td>
          <td>drain 順序或窗口不足</td>
          <td>拉長 drain 時間、調整摘流順序</td>
      </tr>
      <tr>
          <td>readiness 通過但首批請求延遲高</td>
          <td>應用啟動完成與可服務條件未對齊</td>
          <td>細化 readiness 指標、補 startup gate</td>
      </tr>
      <tr>
          <td>reconnect storm 出現在切版後</td>
          <td>timeout 與連線生命週期不匹配</td>
          <td>調整 idle timeout、分批切流</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary</a> 比例低時正常，擴到高比例出現抖動</td>
          <td>LB 權重策略與服務容量曲線不一致</td>
          <td>降低增量批次、補容量保護</td>
      </tr>
      <tr>
          <td>多租戶場景下單租戶延遲飆升</td>
          <td>sticky/routing policy 造成熱點聚集</td>
          <td>分離租戶路由、加入負載重平衡</td>
      </tr>
      <tr>
          <td>回退後 reconnect 風暴持續</td>
          <td>重啟動作放大震盪、未先恢復穩定路徑</td>
          <td>凍結切換、等連線數穩定、再修錯位點</td>
      </tr>
  </tbody>
</table>
<p>「回退後 reconnect 風暴持續」是切流事故中最容易誤判的訊號。判讀順序：先看是否「凍結切換」已執行（rollout 是否真的停了）、再看「舊入口權重」是否回到主導比例（DNS / LB 規則是否切回）、最後看連線數曲線是否進入下降。三項都做完仍見風暴持續、才考慮新版本實例層級的問題（image / config / runtime 漂移）、而非反向重啟新版本。解凍切換的條件是「連線數曲線回到 baseline + reconnect rate 低於閾值連續 N 分鐘」、不是「等夠久了就解凍」的時間導向。</p>
<h2 id="常見誤區">常見誤區</h2>
<p>把 load balancer 當成「只做轉發」的元件，會忽略它在部署與事故中的決策角色。LB 設定定義了流量切換節奏、回退可行性與故障擴散速度。</p>
<p>Health check 跟 readiness 的混淆會在切換時暴露隱性風險。health contract 要反映服務真實 readiness — 含依賴連線池、必要 config、關鍵背景任務狀態 — 而非停在單一探針成功訊號。</p>
<p>把「LB 顯示節點 healthy」當作「服務可承受流量」的訊號，也是事故中的常見誤判。健康檢查通過跟承受重連潮是不同層級的訊號。</p>
<h2 id="案例回寫">案例回寫</h2>
<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> 回寫。先看事件中的摘流量順序、drain 視窗與連線重建節奏，再回到本章判讀 connection contract 與 drain contract 是否對齊。</p>
<p>這個案例主要支撐的是「連線生命週期與摘流量順序」判讀，不直接支撐 container build 可重現性；若根因在映像與 runtime 漂移，應回到 5.1。</p>
<p>當回退後錯誤率仍高或重連風暴延續，通常表示 timeout 與 sticky policy 仍在放大舊連線狀態。先重建連線生命週期時序，再把回退判斷同步到 <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>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>load balancer contract 是部署平台與操作控制面的匯流點。</p>
<ol>
<li>與 5.6 的交接：drain 的生命週期定義與 workload 分類回到 <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 如何組成平台生命週期合約。">Platform Lifecycle Contract</a>。</li>
<li>與 04 的交接：版本切換訊號與錯誤率證據進入 <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>與 06 的交接：canary 放行與回退條件進入 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a>。</li>
<li>與 07 的交接：入口治理與管理面保護進入 <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>與 08 的交接：切換與回退判斷記錄到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
<li>與 <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> 的交接：CDN 是 origin LB 的上游、edge miss 後流量進 origin LB、timeout / retry 設定要協調。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把 LB 合約放進整體部署流程，接著讀 <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> 與 <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/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。</p>
]]></content:encoded></item><item><title>5.4 service discovery</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/service-discovery/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/service-discovery/</guid><description>&lt;p>服務發現（&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;/p>
&lt;h2 id="dns-與-registry">DNS 與 registry&lt;/h2>
&lt;p>service discovery 常見兩種路徑：DNS 查詢與 service registry。DNS 提供簡化解析路徑，適合標準服務發現；registry 提供更細節的實例狀態與元資料，適合複雜路由與多租戶治理。&lt;/p>
&lt;p>選擇重點是變更頻率與一致性需求。實例變動頻繁或跨區路由複雜時，registry 能提供更細控制；穩定內網服務可優先 DNS 路徑降低操作成本。&lt;/p>
&lt;h3 id="dns-based-discovery-的運作與限制">DNS-based Discovery 的運作與限制&lt;/h3>
&lt;p>Kubernetes Service 的 ClusterIP 模式是最常見的 DNS-based discovery：kube-dns / CoreDNS 回覆一個虛擬 IP，kube-proxy 用 iptables / IPVS 做 L4 負載均衡到實際 pod IP。Headless Service（&lt;code>clusterIP: None&lt;/code>）則直接回傳所有 pod IP 的 A record，讓客戶端自行選擇目標。&lt;/p>
&lt;p>DNS-based discovery 的限制來自 DNS 本身的語意：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>TTL 與快取&lt;/strong>：DNS 回應帶 TTL，客戶端和中間 resolver 會快取。當 pod 被摘除但 DNS 快取尚未過期，客戶端仍會嘗試連到已不存在的 IP。Kubernetes CoreDNS 的 Service TTL 預設 30 秒，但客戶端語言 runtime 可能有自己的 DNS cache（JVM &lt;code>networkaddress.cache.ttl&lt;/code> 預設 30 秒、有些版本預設 -1 代表永不過期）。&lt;/li>
&lt;li>&lt;strong>無健康資訊&lt;/strong>：DNS A record 不帶健康狀態。回覆的 IP 可能對應已經 not-ready 但尚未被 endpoint controller 移除的 pod。這個時間窗口取決於 kubelet sync 頻率與 endpoint controller 的反應速度。&lt;/li>
&lt;li>&lt;strong>無權重 / 元資料&lt;/strong>：DNS 不原生支援流量權重、版本標記、區域偏好。需要這些能力時要靠 service mesh 或 client-side load balancing。&lt;/li>
&lt;/ol>
&lt;p>DNS 路徑的工程價值在於零侵入——任何能解析 DNS 的程式碼都自動取得 discovery 能力，不需要額外 SDK 或 sidecar。缺點是控制粒度只到 IP 層，無法表達更豐富的路由語意。&lt;/p>
&lt;h3 id="registry-based-discovery-的運作模式">Registry-based Discovery 的運作模式&lt;/h3>
&lt;p>Service registry（Consul、etcd、Eureka、Nacos）維護 key-value store，每個 service instance 主動註冊自己的地址、metadata 與健康狀態。Client 透過 registry API 或 local agent 取得可用 instance 清單。&lt;/p>
&lt;p>Registry 的工程價值在於提供 DNS 無法表達的元資料：instance 的版本、區域、權重、標籤都可以作為路由條件。代價是所有 service 都需要 registry 連線邏輯（SDK 或 sidecar），且 registry 本身成為基礎設施依賴——registry 不可用時，新 instance 無法註冊、現有 instance 無法被發現。&lt;/p>
&lt;p>Registry 跟 DNS 不互斥。常見做法是 registry 作為 source of truth，再用 DNS interface 對外提供查詢（Consul DNS Interface、CoreDNS 的 etcd plugin）。這讓簡單場景走 DNS、複雜路由走 registry API、兩者共用同一份 instance 清單。&lt;/p>
&lt;h3 id="選擇判讀框架">選擇判讀框架&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>需求&lt;/th>
 &lt;th>DNS-based&lt;/th>
 &lt;th>Registry-based&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>instance 變動頻率低、路由簡單&lt;/td>
 &lt;td>適合：低維護、零侵入&lt;/td>
 &lt;td>過度設計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要權重路由或版本切流&lt;/td>
 &lt;td>不適合：DNS 不帶權重&lt;/td>
 &lt;td>適合：metadata + 路由規則&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要跨叢集 / 跨區域 discovery&lt;/td>
 &lt;td>需要外部 DNS 配合（困難）&lt;/td>
 &lt;td>適合：registry federation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務用多語言實作&lt;/td>
 &lt;td>適合：任何語言都能解 DNS&lt;/td>
 &lt;td>需要每個語言的 SDK 或 sidecar&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要即時健康反映&lt;/td>
 &lt;td>受 TTL 限制、有延遲窗口&lt;/td>
 &lt;td>適合：health check 即時更新&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="endpoint-discovery">endpoint discovery&lt;/h2>
&lt;p>&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 的責任是維持可連線目標集合。這包含註冊、健康檢查、摘除、重建後回註冊。服務端 readiness 與 discovery 健康判斷要對齊，否則會出現不可服務實例仍被路由的情況。&lt;/p></description><content:encoded><![CDATA[<p>服務發現（<a href="/blog/backend/knowledge-cards/service-discovery/" data-link-title="Service Discovery" data-link-desc="說明服務實例如何被查找與路由">service discovery</a>）的核心責任是讓服務在變動環境中仍能找到正確目標實例。它處理的是定位與可用集合，不處理業務設定判斷；這個邊界清楚後，部署切換與故障回退才可預期。</p>
<h2 id="dns-與-registry">DNS 與 registry</h2>
<p>service discovery 常見兩種路徑：DNS 查詢與 service registry。DNS 提供簡化解析路徑，適合標準服務發現；registry 提供更細節的實例狀態與元資料，適合複雜路由與多租戶治理。</p>
<p>選擇重點是變更頻率與一致性需求。實例變動頻繁或跨區路由複雜時，registry 能提供更細控制；穩定內網服務可優先 DNS 路徑降低操作成本。</p>
<h3 id="dns-based-discovery-的運作與限制">DNS-based Discovery 的運作與限制</h3>
<p>Kubernetes Service 的 ClusterIP 模式是最常見的 DNS-based discovery：kube-dns / CoreDNS 回覆一個虛擬 IP，kube-proxy 用 iptables / IPVS 做 L4 負載均衡到實際 pod IP。Headless Service（<code>clusterIP: None</code>）則直接回傳所有 pod IP 的 A record，讓客戶端自行選擇目標。</p>
<p>DNS-based discovery 的限制來自 DNS 本身的語意：</p>
<ol>
<li><strong>TTL 與快取</strong>：DNS 回應帶 TTL，客戶端和中間 resolver 會快取。當 pod 被摘除但 DNS 快取尚未過期，客戶端仍會嘗試連到已不存在的 IP。Kubernetes CoreDNS 的 Service TTL 預設 30 秒，但客戶端語言 runtime 可能有自己的 DNS cache（JVM <code>networkaddress.cache.ttl</code> 預設 30 秒、有些版本預設 -1 代表永不過期）。</li>
<li><strong>無健康資訊</strong>：DNS A record 不帶健康狀態。回覆的 IP 可能對應已經 not-ready 但尚未被 endpoint controller 移除的 pod。這個時間窗口取決於 kubelet sync 頻率與 endpoint controller 的反應速度。</li>
<li><strong>無權重 / 元資料</strong>：DNS 不原生支援流量權重、版本標記、區域偏好。需要這些能力時要靠 service mesh 或 client-side load balancing。</li>
</ol>
<p>DNS 路徑的工程價值在於零侵入——任何能解析 DNS 的程式碼都自動取得 discovery 能力，不需要額外 SDK 或 sidecar。缺點是控制粒度只到 IP 層，無法表達更豐富的路由語意。</p>
<h3 id="registry-based-discovery-的運作模式">Registry-based Discovery 的運作模式</h3>
<p>Service registry（Consul、etcd、Eureka、Nacos）維護 key-value store，每個 service instance 主動註冊自己的地址、metadata 與健康狀態。Client 透過 registry API 或 local agent 取得可用 instance 清單。</p>
<p>Registry 的工程價值在於提供 DNS 無法表達的元資料：instance 的版本、區域、權重、標籤都可以作為路由條件。代價是所有 service 都需要 registry 連線邏輯（SDK 或 sidecar），且 registry 本身成為基礎設施依賴——registry 不可用時，新 instance 無法註冊、現有 instance 無法被發現。</p>
<p>Registry 跟 DNS 不互斥。常見做法是 registry 作為 source of truth，再用 DNS interface 對外提供查詢（Consul DNS Interface、CoreDNS 的 etcd plugin）。這讓簡單場景走 DNS、複雜路由走 registry API、兩者共用同一份 instance 清單。</p>
<h3 id="選擇判讀框架">選擇判讀框架</h3>
<table>
  <thead>
      <tr>
          <th>需求</th>
          <th>DNS-based</th>
          <th>Registry-based</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>instance 變動頻率低、路由簡單</td>
          <td>適合：低維護、零侵入</td>
          <td>過度設計</td>
      </tr>
      <tr>
          <td>需要權重路由或版本切流</td>
          <td>不適合：DNS 不帶權重</td>
          <td>適合：metadata + 路由規則</td>
      </tr>
      <tr>
          <td>需要跨叢集 / 跨區域 discovery</td>
          <td>需要外部 DNS 配合（困難）</td>
          <td>適合：registry federation</td>
      </tr>
      <tr>
          <td>服務用多語言實作</td>
          <td>適合：任何語言都能解 DNS</td>
          <td>需要每個語言的 SDK 或 sidecar</td>
      </tr>
      <tr>
          <td>需要即時健康反映</td>
          <td>受 TTL 限制、有延遲窗口</td>
          <td>適合：health check 即時更新</td>
      </tr>
  </tbody>
</table>
<h2 id="endpoint-discovery">endpoint discovery</h2>
<p><a href="/blog/backend/knowledge-cards/internal-endpoint/" data-link-title="Internal Endpoint" data-link-desc="說明服務內部通訊入口如何配合網路邊界與服務發現">Internal Endpoint</a> discovery 的責任是維持可連線目標集合。這包含註冊、健康檢查、摘除、重建後回註冊。服務端 readiness 與 discovery 健康判斷要對齊，否則會出現不可服務實例仍被路由的情況。</p>
<p>endpoint 變更需要可追溯訊號，讓事故期間能快速判讀是路由失真、註冊延遲，還是下游本身不可用。</p>
<h3 id="註冊時序與-readiness-對齊">註冊時序與 Readiness 對齊</h3>
<p>endpoint 的註冊時機是 discovery 穩定性的關鍵變數。註冊太早（服務尚未 ready 就被加入可用集合）會導致客戶端打到未就緒實例；註冊太晚（服務已 ready 但尚未被 discovery 看到）會導致容量不足。</p>
<p>Kubernetes 的做法是把 endpoint 跟 readinessProbe 綁定：readiness pass 才把 pod IP 加入 Endpoints 物件。這個設計讓 readiness 定義直接決定 discovery 行為。但 readiness probe 的判斷到 Endpoints 更新之間仍有延遲（endpoint controller sync 週期 + kube-proxy rules 更新），這個延遲窗口內的行為要理解：</p>
<ul>
<li>Pod 剛從 not-ready 變 ready：endpoint controller 需要同步周期把 pod IP 加入 Endpoints → kube-proxy 更新 iptables / IPVS → 流量才會到。期間該 pod 不接流量但已可服務。</li>
<li>Pod 從 ready 變 not-ready：同樣有延遲。期間客戶端仍可能打到已 not-ready 的 pod。drain 設計要覆蓋這段窗口。</li>
</ul>
<h3 id="摘除節奏與-drain-的配合">摘除節奏與 Drain 的配合</h3>
<p>endpoint 摘除不是瞬時的。從 pod 標記 not-ready 到所有 client 停止向它送流量，中間經過多個同步步驟。這段時間內，被摘除的 pod 仍會收到流量。</p>
<p>穩定做法是在 preStop hook 加入短暫等待（通常 5-15 秒），讓 endpoint 更新有時間傳播到所有 kube-proxy / envoy，然後再開始 graceful shutdown。這段 preStop 等待是 <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> 中 drain 總窗口（短 API 通常 5-30 秒）的 endpoint 傳播子區間，drain 總窗口還要覆蓋 preStop 之後的在途請求收斂時間。</p>
<h3 id="跨叢集-discovery-的挑戰">跨叢集 Discovery 的挑戰</h3>
<p>對應 <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 → EKS</a>：揭露「遷移難點通常在跨叢集服務依賴與流量切換、不在 Kubernetes API 本身」。跨叢集 discovery 是遷移期的核心難題——服務 A 在新叢集、服務 B 在舊叢集，A 要能找到 B。</p>
<p>跨叢集 discovery 的常見做法：</p>
<ol>
<li><strong>外部 DNS + 加權路由</strong>：兩個叢集的 service 都註冊到外部 DNS（Route 53、Cloud DNS），用權重控制流量比例。簡單但粒度粗，只能整體切、不能 per-service 切。</li>
<li><strong>Service mesh federation</strong>：Istio multi-cluster、Linkerd multi-cluster 把跨叢集 endpoint 統一管理。粒度細、可以 per-service 切流量，但引入 mesh 的複雜度。</li>
<li><strong>Application-level routing</strong>：應用自己管理多叢集 endpoint（通常透過 config 或 feature flag），切換時改 config。最靈活但最手動，適合遷移期的過渡方案。</li>
</ol>
<p>遷移期最危險的狀態是「服務切過去了、discovery 沒切過去」——新叢集的服務 A 仍透過舊 discovery 找舊叢集的 B，跨網路延遲從微秒級跳到毫秒級，或在網路分區時完全斷開。discovery 切換要跟服務切換同批規劃。</p>
<h2 id="failure-fallback">failure fallback</h2>
<p><a href="/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback</a> 在 discovery 層的責任是縮小定位失敗影響。常見策略包含本地快取最後可用集合、區域優先回退、受控重試與短暫降級。</p>
<p>fallback 設計要明確停止條件。長期依賴過期 endpoint 快取會造成隱性錯誤累積，事故期反而更難收斂。</p>
<h3 id="fallback-的三層防線">Fallback 的三層防線</h3>
<p>discovery 故障的 fallback 可分三層，每層有不同的代價與風險：</p>
<p><strong>第一層：本地 endpoint 快取</strong>。Client 維持最後一次成功查詢的 endpoint 清單。discovery 服務不可用時，繼續用快取 endpoint。風險是快取中的 endpoint 可能已經下線或不健康。有效期要設上限——超過 N 分鐘的快取視為不可信，進入第二層。</p>
<p><strong>第二層：區域降級</strong>。本區域的 endpoint 全部不可用時，降級到其他區域的 endpoint。代價是跨區延遲增加。風險是其他區域也可能因為同源故障而不可用。降級時要觀測跨區延遲是否在 SLO 內，超出則進第三層。</p>
<p><strong>第三層：服務降級</strong>。discovery 完全失效時，服務本身降級——返回快取回應、靜態頁面、或明確的錯誤訊息。這一層的設計責任落在應用的 <a href="/blog/backend/knowledge-cards/fallback/" data-link-title="Fallback" data-link-desc="說明主要路徑失敗時使用替代結果或替代流程的設計責任">fallback</a> 策略，discovery 只負責提供「目前無可用 endpoint」的訊號。</p>
<p>三層防線的共同原則是每一層都有明確的進入條件和退出條件。進入 fallback 不是終點——要持續嘗試恢復正常路徑，fallback 狀態持續時間要被觀測和告警。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務延遲上升且下游錯誤分布不均</td>
          <td>路由到不可用或高負載實例</td>
          <td>檢查註冊健康、刷新 endpoint 集合</td>
      </tr>
      <tr>
          <td>節點重啟後短時間大量 5xx</td>
          <td>註冊與 readiness 時序不對齊</td>
          <td>延後註冊時機、收斂就緒條件</td>
      </tr>
      <tr>
          <td>跨區呼叫比例異常升高</td>
          <td>區域內可用集合失真或容量不足</td>
          <td>檢查區域路由策略、恢復本地優先</td>
      </tr>
      <tr>
          <td>discovery 查詢成功但連線失敗率升高</td>
          <td>endpoint 新鮮度不足或 DNS 快取漂移</td>
          <td>縮短 TTL、加入主動刷新</td>
      </tr>
      <tr>
          <td>fallback 命中率長期偏高</td>
          <td>主路徑失效被掩蓋</td>
          <td>啟動故障調查、限制 fallback 存活時間</td>
      </tr>
      <tr>
          <td>擴容後新 pod 遲遲不接流量</td>
          <td>endpoint 註冊延遲或 kube-proxy 同步慢</td>
          <td>檢查 endpoint controller 延遲</td>
      </tr>
      <tr>
          <td>遷移期跨叢集延遲突增</td>
          <td>discovery 沒切過去、跨網路打舊叢集</td>
          <td>規劃 discovery 切換與服務切換同批</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>Service discovery 跟 DNS 設定的混淆，會讓註冊時序、健康判斷與摘除節奏的缺口在平時被忽略。這類缺口在平時不明顯，通常在切版、擴縮容或區域異常時集中爆發。</p>
<p>把 fallback 命中率視為穩定指標也容易誤判。fallback 長期偏高代表主路徑問題被遮蔽，應回頭檢查 endpoint 新鮮度與註冊健康，而不是只放寬重試。</p>
<p>把 DNS TTL 設成 0 試圖取得即時一致性，會大幅增加 DNS 查詢量。DNS 的設計前提是快取——TTL 0 在高流量服務下會讓 DNS server 成為瓶頸。穩定做法是設合理 TTL（5-30 秒）搭配 client-side 主動刷新。</p>
<p>把 JVM 的 DNS cache 當成 OS 的 DNS TTL——JVM <code>networkaddress.cache.ttl</code> 的預設值在不同版本不同（有些版本是 30 秒、有些是永不過期）。容器化部署時要顯式設定，避免 pod IP 變了但 JVM 還在打舊 IP。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>service discovery 專注「找到可用實例」。當問題進入設定分發、版本切換、策略開關，責任轉到 <a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout</a> 與部署策略章節。邊界分明能避免故障排查時把不同控制面混為一談。</p>
<p>discovery 跟 load balancing 的邊界：discovery 回答「有哪些 endpoint 可用」，load balancing 回答「在可用 endpoint 中選哪一個」。DNS round-robin 把兩者混在一起，registry-based 方案通常把兩者分開，讓 LB 策略（round-robin、least-connection、consistent hash）在 discovery 結果之上獨立運作。</p>
<h2 id="案例回寫">案例回寫</h2>
<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> 回寫。先看遷移期間實例註冊、摘除與 DNS/registry 同步節奏，再對照本章判讀 endpoint 新鮮度與 fallback 壽命是否合理。</p>
<p><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 → EKS</a> 從跨叢集角度支撐：揭露遷移期的 discovery 挑戰——「難點在跨叢集服務依賴與流量切換」。遷移期 discovery 要處理新舊叢集的 endpoint 共存、切換時序、回退路徑。</p>
<p>這些案例主要支撐「定位集合新鮮度」與「跨叢集 discovery 同步」判讀。不直接支撐 LB 連線 timeout 或 runtime 建置一致性；若問題在連線生命週期或映像漂移，應轉到 5.3 或 5.1。</p>
<p>遇到「查詢成功但連線失敗率高」時，應拆成註冊時序、TTL 與快取刷新三條線同步驗證，避免把定位問題誤判成下游異常，再把證據分流到 <a href="/blog/backend/08-incident-response/incident-intake-evidence-triage/" data-link-title="8.18 Incident Intake &amp; Evidence Triage" data-link-desc="把告警、客訴、支援回報與第三方狀態轉成同一個 intake / evidence 判讀流程">8.18 Incident Intake &amp; Evidence Triage</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 5.2 的交接：實例註冊與可用判定回到 <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 的交接：路由目標與流量合約回到 <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.6 的交接：endpoint 註冊時序與 readiness 的對齊回到 <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 如何組成平台生命週期合約。">Platform Lifecycle Contract</a>。</li>
<li>與 5.7 的交接：discovery 與 control plane boundary 的分責回到 <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 與管理面如何分責任與回退。">Traffic、Config 與 Control Plane Boundary</a>。</li>
<li>與 4.13 的交接：依賴拓樸與發現信號回到 <a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">Service Topology 與 Dependency Map</a>。</li>
<li>與 8.18 的交接：定位故障的證據分流回到 <a href="/blog/backend/08-incident-response/incident-intake-evidence-triage/" data-link-title="8.18 Incident Intake &amp; Evidence Triage" data-link-desc="把告警、客訴、支援回報與第三方狀態轉成同一個 intake / evidence 判讀流程">Incident Intake &amp; Evidence Triage</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把發現機制放進流量契約，接著讀 <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>。要看部署切換如何影響可用集合，接著讀 <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>。要看 discovery 在 control plane 邊界中的定位，接著讀 <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 Traffic、Config 與 Control Plane Boundary</a>。</p>
]]></content:encoded></item><item><title>5.5 平台與入口威脅建模（Threat Modeling）</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/attacker-view-platform-entry-risks/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/attacker-view-platform-entry-risks/</guid><description>&lt;p>平台與入口威脅建模的核心責任是把部署平台的弱點維持在可操作的概念層。本章的輸出是平台問題地圖、案例對照與交接條件，讓實作前決策可先對齊，避免進入 YAML / unit file / LB rule 前就已經漏掉攻擊面。&lt;/p>
&lt;h2 id="服務環節問題地圖">服務環節問題地圖&lt;/h2>
&lt;p>平台弱點盤點的第一層是把服務環節跟攻擊面對齊。同一個服務交付路徑上、入口、生命週期、設定、交付節奏各自有不同失分模式。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>環節&lt;/th>
 &lt;th>主要問題&lt;/th>
 &lt;th>注意事項&lt;/th>
 &lt;th>優先案例&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>入口暴露面&lt;/td>
 &lt;td>入口分級與實際可達範圍不一致&lt;/td>
 &lt;td>入口清單與責任鏈要先對齊&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>生命週期訊號&lt;/td>
 &lt;td>readiness、draining、shutdown 節奏不一致&lt;/td>
 &lt;td>平台合約要先定義再驗證&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/ivanti-2024-vpn-chain/" data-link-title="7.R7.3.2 Ivanti 2024：CVE-2023-46805/2024-21887 VPN 邊界漏洞鏈" data-link-desc="多漏洞串接下，邊界設備事件如何轉為持續控制風險">Ivanti 2024&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>設定與密鑰下發&lt;/td>
 &lt;td>設定漂移與權限擴張同時發生&lt;/td>
 &lt;td>高風險設定要進 release gate，並分離 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/management-plane/" data-link-title="Management Plane" data-link-desc="說明管理平面如何與業務流量平面分離，避免高權限入口擴散">management plane&lt;/a>&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/f5-bigip-cve-2023-46747-auth-bypass/" data-link-title="7.R7.3.19 F5 BIG-IP 2023：CVE-2023-46747 認證繞過" data-link-desc="BIG-IP 組態管理入口認證繞過如何放大邊界設備治理壓力">F5 BIG-IP 2023&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>交付切換節奏&lt;/td>
 &lt;td>回滾與切換條件不清晰&lt;/td>
 &lt;td>先定停損條件再定交付速度&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/" data-link-title="7.R7.2.10 TeamCity 2024：CVE-2024-27198/27199 入口鏈" data-link-desc="TeamCity 連續漏洞揭示 CI 平台入口繞過與路徑穿越的供應鏈風險">TeamCity 2024&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="入口暴露面">入口暴露面&lt;/h3>
&lt;p>入口暴露面的主要弱點判讀是「實際可達範圍是否超過設計意圖」。容器化、service mesh、ingress controller 升級、新增 LoadBalancer 都可能無意中把內部服務暴露到公網。入口清單跟責任鏈先對齊、能避免發版本就改變了攻擊面。升級流程跟回退窗口設計見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#%e5%b9%b3%e5%8f%b0%e5%85%83%e4%bb%b6%e5%8d%87%e7%b4%9a%e7%9a%84%e5%8f%af%e9%87%8d%e6%92%ad%e6%b5%81%e7%a8%8b" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 平台元件升級的可重播流程&lt;/a>。&lt;/p>
&lt;p>入口暴露面的盤點要區分三類入口，各自有不同的失分模式：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>設計意圖內的入口&lt;/strong>（Ingress / LoadBalancer Service / API Gateway）：這些入口有明確 owner、有 WAF / TLS 保護。弱點在於設定漂移——port 範圍擴大、路由規則放寬、wildcard host 引入。盤點方式是定期比對實際 Ingress 規則與設計意圖。&lt;/li>
&lt;li>&lt;strong>隱性入口&lt;/strong>（NodePort、hostNetwork pod、debug endpoint、metrics endpoint）：這些入口在設計時不被視為外部可達，但在特定網路拓樸下可能從外部存取。NodePort 預設 range 30000-32767 在某些雲端 security group 設定下可能對外開放。metrics endpoint（/metrics、/debug/pprof）常不帶認證、暴露服務內部狀態。&lt;/li>
&lt;li>&lt;strong>暫態入口&lt;/strong>（kubectl port-forward、臨時 LoadBalancer、tunnel 測試）：開發或除錯時臨時打開的入口，使用後忘記關閉。這類入口沒有 WAF、沒有 TLS、沒有 audit log，是攻擊面中最難盤點的部分。&lt;/li>
&lt;/ol>
&lt;p>Tunnel 形態的入口（cloudflared、Tailscale Funnel）有獨立的弱點盤點框架，見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/outbound-tunnel-entry/" data-link-title="5.10 Outbound Tunnel 入口與生命週期" data-link-desc="整理 cloudflared / Tailscale 等反向隧道的入口形態、生命週期合約與故障模式">5.10 Outbound Tunnel 入口&lt;/a> 的認證疊法段。&lt;/p>
&lt;h3 id="生命週期訊號">生命週期訊號&lt;/h3>
&lt;p>生命週期訊號的弱點聚焦於脆弱窗口期被利用：readiness 過早通過、shutdown 階段仍在處理 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight&lt;/a> request、drain 視窗內接收新請求，都會把短暫的脆弱窗口拉長。&lt;/p>
&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> 的生命週期狀態對齊：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>startup → readiness 窗口&lt;/strong>：服務正在初始化、依賴尚未驗證、安全中介軟體（WAF sidecar、auth proxy）可能還沒就緒。此時如果 readiness 過早通過讓流量進來，請求可能繞過安全層直接打到後端。&lt;/li>
&lt;li>&lt;strong>readiness → drain 窗口&lt;/strong>：正常服務狀態，弱點集中在 readiness 條件太鬆——只檢查 port 可達但 auth middleware 沒初始化。&lt;/li>
&lt;li>&lt;strong>drain → shutdown 窗口&lt;/strong>：服務正在收斂，此時安全元件（rate limiter、WAF）可能已停止更新規則但仍在處理請求。攻擊者若在 drain 窗口送入惡意請求，安全元件可能無法正常攔截。&lt;/li>
&lt;/ul>
&lt;h3 id="設定與密鑰下發">設定與密鑰下發&lt;/h3>
&lt;p>設定與密鑰下發是最容易被忽略的維度。Image 沒變但 config / secret 變了、權限因 RBAC 漂移擴張、feature flag 在 production 偷偷開啟未經 review 的新行為。這些變更不走 release gate 的話，攻擊者有大量低噪音入口可以利用。&lt;/p></description><content:encoded><![CDATA[<p>平台與入口威脅建模的核心責任是把部署平台的弱點維持在可操作的概念層。本章的輸出是平台問題地圖、案例對照與交接條件，讓實作前決策可先對齊，避免進入 YAML / unit file / LB rule 前就已經漏掉攻擊面。</p>
<h2 id="服務環節問題地圖">服務環節問題地圖</h2>
<p>平台弱點盤點的第一層是把服務環節跟攻擊面對齊。同一個服務交付路徑上、入口、生命週期、設定、交付節奏各自有不同失分模式。</p>
<table>
  <thead>
      <tr>
          <th>環節</th>
          <th>主要問題</th>
          <th>注意事項</th>
          <th>優先案例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>入口暴露面</td>
          <td>入口分級與實際可達範圍不一致</td>
          <td>入口清單與責任鏈要先對齊</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/moveit-2023-mass-exfiltration/" data-link-title="7.R7.3.1 MOVEit 2023：外網檔案服務批量外送" data-link-desc="MFT 對外入口在零時差事件中如何被批量利用">MOVEit 2023</a></td>
      </tr>
      <tr>
          <td>生命週期訊號</td>
          <td>readiness、draining、shutdown 節奏不一致</td>
          <td>平台合約要先定義再驗證</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/ivanti-2024-vpn-chain/" data-link-title="7.R7.3.2 Ivanti 2024：CVE-2023-46805/2024-21887 VPN 邊界漏洞鏈" data-link-desc="多漏洞串接下，邊界設備事件如何轉為持續控制風險">Ivanti 2024</a></td>
      </tr>
      <tr>
          <td>設定與密鑰下發</td>
          <td>設定漂移與權限擴張同時發生</td>
          <td>高風險設定要進 release gate，並分離 <a href="/blog/backend/knowledge-cards/management-plane/" data-link-title="Management Plane" data-link-desc="說明管理平面如何與業務流量平面分離，避免高權限入口擴散">management plane</a></td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/edge-exposure/f5-bigip-cve-2023-46747-auth-bypass/" data-link-title="7.R7.3.19 F5 BIG-IP 2023：CVE-2023-46747 認證繞過" data-link-desc="BIG-IP 組態管理入口認證繞過如何放大邊界設備治理壓力">F5 BIG-IP 2023</a></td>
      </tr>
      <tr>
          <td>交付切換節奏</td>
          <td>回滾與切換條件不清晰</td>
          <td>先定停損條件再定交付速度</td>
          <td><a href="/blog/backend/07-security-data-protection/red-team/cases/supply-chain/teamcity-2024-cve-27198-27199-auth-path-traversal/" data-link-title="7.R7.2.10 TeamCity 2024：CVE-2024-27198/27199 入口鏈" data-link-desc="TeamCity 連續漏洞揭示 CI 平台入口繞過與路徑穿越的供應鏈風險">TeamCity 2024</a></td>
      </tr>
  </tbody>
</table>
<h3 id="入口暴露面">入口暴露面</h3>
<p>入口暴露面的主要弱點判讀是「實際可達範圍是否超過設計意圖」。容器化、service mesh、ingress controller 升級、新增 LoadBalancer 都可能無意中把內部服務暴露到公網。入口清單跟責任鏈先對齊、能避免發版本就改變了攻擊面。升級流程跟回退窗口設計見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#%e5%b9%b3%e5%8f%b0%e5%85%83%e4%bb%b6%e5%8d%87%e7%b4%9a%e7%9a%84%e5%8f%af%e9%87%8d%e6%92%ad%e6%b5%81%e7%a8%8b" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 平台元件升級的可重播流程</a>。</p>
<p>入口暴露面的盤點要區分三類入口，各自有不同的失分模式：</p>
<ol>
<li><strong>設計意圖內的入口</strong>（Ingress / LoadBalancer Service / API Gateway）：這些入口有明確 owner、有 WAF / TLS 保護。弱點在於設定漂移——port 範圍擴大、路由規則放寬、wildcard host 引入。盤點方式是定期比對實際 Ingress 規則與設計意圖。</li>
<li><strong>隱性入口</strong>（NodePort、hostNetwork pod、debug endpoint、metrics endpoint）：這些入口在設計時不被視為外部可達，但在特定網路拓樸下可能從外部存取。NodePort 預設 range 30000-32767 在某些雲端 security group 設定下可能對外開放。metrics endpoint（/metrics、/debug/pprof）常不帶認證、暴露服務內部狀態。</li>
<li><strong>暫態入口</strong>（kubectl port-forward、臨時 LoadBalancer、tunnel 測試）：開發或除錯時臨時打開的入口，使用後忘記關閉。這類入口沒有 WAF、沒有 TLS、沒有 audit log，是攻擊面中最難盤點的部分。</li>
</ol>
<p>Tunnel 形態的入口（cloudflared、Tailscale Funnel）有獨立的弱點盤點框架，見 <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="生命週期訊號">生命週期訊號</h3>
<p>生命週期訊號的弱點聚焦於脆弱窗口期被利用：readiness 過早通過、shutdown 階段仍在處理 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a> request、drain 視窗內接收新請求，都會把短暫的脆弱窗口拉長。</p>
<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> 的生命週期狀態對齊：</p>
<ul>
<li><strong>startup → readiness 窗口</strong>：服務正在初始化、依賴尚未驗證、安全中介軟體（WAF sidecar、auth proxy）可能還沒就緒。此時如果 readiness 過早通過讓流量進來，請求可能繞過安全層直接打到後端。</li>
<li><strong>readiness → drain 窗口</strong>：正常服務狀態，弱點集中在 readiness 條件太鬆——只檢查 port 可達但 auth middleware 沒初始化。</li>
<li><strong>drain → shutdown 窗口</strong>：服務正在收斂，此時安全元件（rate limiter、WAF）可能已停止更新規則但仍在處理請求。攻擊者若在 drain 窗口送入惡意請求，安全元件可能無法正常攔截。</li>
</ul>
<h3 id="設定與密鑰下發">設定與密鑰下發</h3>
<p>設定與密鑰下發是最容易被忽略的維度。Image 沒變但 config / secret 變了、權限因 RBAC 漂移擴張、feature flag 在 production 偷偷開啟未經 review 的新行為。這些變更不走 release gate 的話，攻擊者有大量低噪音入口可以利用。</p>
<p>設定變更的弱點盤點要分兩個方向：</p>
<p><strong>顯式設定變更</strong>（ConfigMap、Secret、feature flag 更新）：變更本身是可追蹤的，弱點在於 review 機制是否涵蓋高風險設定。payment endpoint、auth provider URL、rate limit 閾值、CORS 允許來源——這些設定的變更影響跟程式碼變更等量，應走同等 review 流程。設定變更的 review 與 rollout 策略見 <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 Config Boundary</a>。</p>
<p><strong>隱式設定漂移</strong>（RBAC 逐步放寬、network policy 例外累積、service account 權限擴張）：這類變更是多次小修改累積的結果，單次變更看起來合理但累積後超出安全邊界。盤點方式是定期用 policy-as-code（OPA/Gatekeeper、Kyverno）掃描 cluster 內的 RBAC binding、network policy、pod security 設定，跟 baseline 比對偏移程度。</p>
<h3 id="交付切換節奏">交付切換節奏</h3>
<p>交付切換節奏的弱點判讀是「在不穩定窗口期、系統是否還有防禦能力」。Canary / rollout / rollback 期間 5xx 升高、connection 重建、auth 短暫失敗，會掩蓋同期間的攻擊訊號。沒有先定停損條件就推交付速度、是把切換期變成攻擊期的常見做法。</p>
<p>交付窗口期的防禦能力退化有兩個機制：</p>
<p><strong>訊號淹沒</strong>：rollout 本身產生的短暫錯誤（5xx spike、reconnect、auth retry）跟攻擊訊號長得一樣。事故團隊在切流期把所有異常歸因於部署變更，攻擊者剛好利用這個注意力盲區。對策是把切流期 alert 跟安全 alert 分流到不同 channel，安全訊號走獨立通道、由 security on-call 獨立判讀。</p>
<p><strong>防禦元件版本不一致</strong>：<a href="/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary</a> 期間新舊版本同時在線，WAF 規則、rate limit 設定、auth middleware 版本可能不同。攻擊者可以針對舊版本的已知弱點送流量，利用 canary 期間的路由特性讓流量到達舊版本。對策是把安全元件的更新跟應用版本解耦——WAF 規則、rate limit 是平台層設定，應在所有版本一致生效。</p>
<h2 id="案例對照表情境--判讀--注意事項--路由章節">案例對照表（情境 → 判讀 → 注意事項 → 路由章節）</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>判讀</th>
          <th>注意事項</th>
          <th>路由章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>外網可達入口在發版後增加</td>
          <td>入口分級與交付節奏存在脫鉤</td>
          <td>入口盤點要成為交付前條件</td>
          <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 Load Balancer Contract</a></td>
      </tr>
      <tr>
          <td>readiness 通過但實際流量錯誤率上升</td>
          <td>生命週期合約與流量模型不一致</td>
          <td>探針、draining、shutdown 要同批驗證</td>
          <td><a href="/blog/backend/06-reliability/failure-mode-pre-mortem/" data-link-title="6.5 失敗模式預判（Pre-mortem 與 FMEA）" data-link-desc="用 pre-mortem 反向推導失敗路徑、用 FMEA 分類軸評估驗證缺口，把可靠性盲區變成可排序的改善輸入">6.5 失敗模式預判</a></td>
      </tr>
      <tr>
          <td>設定異動與異常事件同時出現</td>
          <td>設定漂移可能已跨越安全邊界</td>
          <td>設定審查與責任追蹤要同步維護</td>
          <td><a href="/blog/backend/08-incident-response/post-incident-review/" data-link-title="8.5 復盤與改進追蹤" data-link-desc="把 RCA 與 action items 轉成可驗證閉環">8.5 復盤與改進追蹤</a></td>
      </tr>
      <tr>
          <td>切流期間入侵告警被淹沒</td>
          <td>rollout 噪音掩蓋攻擊訊號</td>
          <td>切流期 alert 分流、攻擊訊號獨立通道</td>
          <td><a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 訊號治理閉環</a></td>
      </tr>
  </tbody>
</table>
<p>「外網可達入口在發版後增加」是平台變更弱點盤點的頭號議題。Ingress class 換、Service type 改、LB 規則重組都可能讓原本內部服務獲得外部 IP。把入口盤點放進 release pre-check、能讓這類變更在合併前被擋下。</p>
<p>「readiness 通過但實際流量錯誤率上升」揭露 readiness probe 設計失誤的弱點面向。Probe 只回 200 OK 不代表服務可承受真實流量、攻擊者剛好可以在這個窗口送高頻 request 看是否壓垮服務。Readiness 反映依賴就緒條件而非單一探針成功、能縮短這個窗口。</p>
<p>「設定異動與異常事件同時出現」是 config rollout 的弱點風險。Config 變更後出現異常事件、可能是設定本身的問題、也可能是攻擊者剛好利用了設定窗口。Config 審查跟責任追蹤同步維護、能讓事後復盤分辨兩者。</p>
<p>「切流期間入侵告警被淹沒」是新加入的議題。切流產生大量短暫 5xx、reconnect、auth retry、可能淹沒真正的攻擊訊號。把切流期 alert 跟一般 alert 分流、攻擊訊號走獨立通道、能避免攻擊在切流窗口下被忽略。</p>
<h2 id="平台遷移期的攻擊面變動">平台遷移期的攻擊面變動</h2>
<p>對應 5.C1 / 5.C4 / 5.C5 揭露的遷移分段切換流程、本段從弱點盤點角度補充其攻擊面變動風險（case 庫未直接揭露此角度、屬通用工程經驗）。遷移期的職責邊界重訂見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/#managed-%e5%b9%b3%e5%8f%b0%e8%b7%9f%e5%9c%98%e9%9a%8a%e8%81%b7%e8%b2%ac%e9%82%8a%e7%95%8c" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Managed 平台跟團隊職責邊界</a>、弱點盤點跟治理視角合用才完整。</p>
<p>平台遷移（self-managed → managed、單 cluster → 多 cluster、舊版本 → 新版本）會短期擴大攻擊面、然後逐步收斂。遷移期顯式管理攻擊面變化、避免雙軌期變成攻擊面雙倍期。</p>
<p>可重複套用的弱點判讀：</p>
<ol>
<li><strong>盤點雙軌期入口</strong>：舊平台跟新平台的入口清單分別列出、確認新平台不繼承舊平台已知漏洞、舊平台的廢棄入口確實關閉。</li>
<li><strong>identity / credential 重新對位</strong>：service account、API token、TLS cert 在新平台是否走新的 rotation flow、舊平台的 credential 是否在切換完成後撤除。</li>
<li><strong>observability 對應更新</strong>：新平台的 audit log、access log、security event 是否進入同一個 SIEM / 告警通道、避免遷移期內攻擊訊號掉到觀測缺口。</li>
<li><strong>回退路徑的攻擊面評估</strong>：回退到舊平台時、舊平台是否仍處於最新 patch 狀態、回退本身會不會把已修補的漏洞重新引入。</li>
</ol>
<p>遷移計畫要把資安 review 列為 gate 之一、讓遷移期攻擊面變動進入可見治理流程。沒有這道 gate、遷移期容易被當成純技術項目處理、漏掉攻擊面的隱性擴大。</p>
<h2 id="到實作前的最後一層">到實作前的最後一層</h2>
<p>弱點盤點在概念層回答的是平台風險判讀與交接節奏。當討論進入 Kubernetes 欄位、LB 規則、系統服務參數或腳本配置時，就代表已進入實作層。</p>
<p>實作層的防護驗證跟概念層分工：實作層看具體 YAML / config / rule 是否符合 hardening baseline、概念層看交付路徑跟責任鏈是否完整。兩者都做才能讓平台變更的攻擊面在 release 前可見。</p>
<p>進實作層後接 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資料保護模組</a> 的具體 hardening 章節、跟 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a> 對齊入口分級。</p>
]]></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>5.7 Traffic、Config 與 Control Plane Boundary</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/</guid><description>&lt;p>Traffic、config 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane&lt;/a> boundary 的核心責任是把平台切換中的資料面與控制面分開。進入 Kubernetes、ELB、Envoy、Consul 或 Terraform 前，讀者需要先知道流量、設定、secret、service discovery 與管理面各自有不同風險與回退方式。&lt;/p>
&lt;h2 id="traffic-boundary">Traffic Boundary&lt;/h2>
&lt;p>Traffic boundary 的責任是決定 request 如何進入服務、如何分流、如何回退。它包含 load balancer、routing rule、health check、sticky session、timeout 與 drain。&lt;/p>
&lt;p>流量切換要能回答三個問題：哪一批 request 會到新版本、失敗時如何停止擴批、舊版本是否仍能承接回退流量。這三個答案明確後，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary&lt;/a> 才能從比例設定變成可回退策略。&lt;/p>
&lt;p>Traffic boundary 的判讀重點是 customer impact 如何被分批限制。小比例 canary、區域切流、tenant 切流與 route rule 都是不同切換單位；切換單位越清楚，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window&lt;/a> 越容易被驗證。&lt;/p>
&lt;h3 id="切換單位的選擇">切換單位的選擇&lt;/h3>
&lt;p>切換單位決定故障的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a> 與回退的精準度。常見切換單位各有不同操作特性：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>切換單位&lt;/th>
 &lt;th>blast radius&lt;/th>
 &lt;th>回退精準度&lt;/th>
 &lt;th>操作複雜度&lt;/th>
 &lt;th>適用場景&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>比例（%）&lt;/td>
 &lt;td>按流量比例&lt;/td>
 &lt;td>粗（全域）&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>通用 canary&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>區域 / AZ&lt;/td>
 &lt;td>限定地理範圍&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>跨區部署的服務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>租戶 / 組織&lt;/td>
 &lt;td>限定特定客戶&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>多租戶 SaaS&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>路由規則&lt;/td>
 &lt;td>限定特定路徑&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>API 版本切換、功能漸進上線&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>比例切換最簡單但 blast radius 不可控——5% 的流量中可能包含大客戶的關鍵路徑。租戶切換精準度最高但操作複雜度也最高——需要在 routing 層維護租戶到版本的映射。穩定做法是從比例切換開始，遇到需要精準控制 impact 時再升級到租戶或路由規則切換。&lt;/p>
&lt;h2 id="config-boundary">Config Boundary&lt;/h2>
&lt;p>設定如何下發、如何生效、如何回退——Config boundary 回答這三個問題。&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;/p>
&lt;p>高風險設定包含 payment provider endpoint、feature flag、rate limit、routing rule、timeout 與 fallback policy。這些設定變更可能不需要新 image，卻能改變 production 行為，因此要進 release gate。&lt;/p>
&lt;h3 id="config-變更的風險分級">Config 變更的風險分級&lt;/h3>
&lt;p>設定變更的風險不一致——有些設定改了只影響 log level，有些設定改了直接影響付款路徑。分級後才能對不同風險的設定套用對應的 review 與 rollout 強度。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>風險等級&lt;/th>
 &lt;th>設定類型&lt;/th>
 &lt;th>review 與 rollout 要求&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>高&lt;/td>
 &lt;td>payment endpoint、auth provider URL、encryption key&lt;/td>
 &lt;td>等同 code review + staged rollout + rollback 驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>中&lt;/td>
 &lt;td>rate limit、timeout、feature flag、CORS 設定&lt;/td>
 &lt;td>變更 review + 觀測窗口&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>低&lt;/td>
 &lt;td>log level、debug flag、非關鍵 UI 文案&lt;/td>
 &lt;td>變更紀錄即可&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>風險分級的判讀依據是「這個設定改錯時、使用者會看到什麼」。改錯 payment endpoint 會讓付款打到錯誤目標；改錯 rate limit 可能讓合法流量被擋；改錯 log level 最多是 log 太吵或太安靜。設定的注入方式與版本追蹤見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 配置注入方式與取捨&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Traffic、config 與 <a href="/blog/backend/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane</a> boundary 的核心責任是把平台切換中的資料面與控制面分開。進入 Kubernetes、ELB、Envoy、Consul 或 Terraform 前，讀者需要先知道流量、設定、secret、service discovery 與管理面各自有不同風險與回退方式。</p>
<h2 id="traffic-boundary">Traffic Boundary</h2>
<p>Traffic boundary 的責任是決定 request 如何進入服務、如何分流、如何回退。它包含 load balancer、routing rule、health check、sticky session、timeout 與 drain。</p>
<p>流量切換要能回答三個問題：哪一批 request 會到新版本、失敗時如何停止擴批、舊版本是否仍能承接回退流量。這三個答案明確後，<a href="/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary</a> 才能從比例設定變成可回退策略。</p>
<p>Traffic boundary 的判讀重點是 customer impact 如何被分批限制。小比例 canary、區域切流、tenant 切流與 route rule 都是不同切換單位；切換單位越清楚，<a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a> 越容易被驗證。</p>
<h3 id="切換單位的選擇">切換單位的選擇</h3>
<p>切換單位決定故障的 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 與回退的精準度。常見切換單位各有不同操作特性：</p>
<table>
  <thead>
      <tr>
          <th>切換單位</th>
          <th>blast radius</th>
          <th>回退精準度</th>
          <th>操作複雜度</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>比例（%）</td>
          <td>按流量比例</td>
          <td>粗（全域）</td>
          <td>低</td>
          <td>通用 canary</td>
      </tr>
      <tr>
          <td>區域 / AZ</td>
          <td>限定地理範圍</td>
          <td>中</td>
          <td>中</td>
          <td>跨區部署的服務</td>
      </tr>
      <tr>
          <td>租戶 / 組織</td>
          <td>限定特定客戶</td>
          <td>高</td>
          <td>高</td>
          <td>多租戶 SaaS</td>
      </tr>
      <tr>
          <td>路由規則</td>
          <td>限定特定路徑</td>
          <td>高</td>
          <td>高</td>
          <td>API 版本切換、功能漸進上線</td>
      </tr>
  </tbody>
</table>
<p>比例切換最簡單但 blast radius 不可控——5% 的流量中可能包含大客戶的關鍵路徑。租戶切換精準度最高但操作複雜度也最高——需要在 routing 層維護租戶到版本的映射。穩定做法是從比例切換開始，遇到需要精準控制 impact 時再升級到租戶或路由規則切換。</p>
<h2 id="config-boundary">Config Boundary</h2>
<p>設定如何下發、如何生效、如何回退——Config boundary 回答這三個問題。<a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">config rollout</a> 和應用版本不一定同步，因此要保留相容窗口。</p>
<p>高風險設定包含 payment provider endpoint、feature flag、rate limit、routing rule、timeout 與 fallback policy。這些設定變更可能不需要新 image，卻能改變 production 行為，因此要進 release gate。</p>
<h3 id="config-變更的風險分級">Config 變更的風險分級</h3>
<p>設定變更的風險不一致——有些設定改了只影響 log level，有些設定改了直接影響付款路徑。分級後才能對不同風險的設定套用對應的 review 與 rollout 強度。</p>
<table>
  <thead>
      <tr>
          <th>風險等級</th>
          <th>設定類型</th>
          <th>review 與 rollout 要求</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>高</td>
          <td>payment endpoint、auth provider URL、encryption key</td>
          <td>等同 code review + staged rollout + rollback 驗證</td>
      </tr>
      <tr>
          <td>中</td>
          <td>rate limit、timeout、feature flag、CORS 設定</td>
          <td>變更 review + 觀測窗口</td>
      </tr>
      <tr>
          <td>低</td>
          <td>log level、debug flag、非關鍵 UI 文案</td>
          <td>變更紀錄即可</td>
      </tr>
  </tbody>
</table>
<p>風險分級的判讀依據是「這個設定改錯時、使用者會看到什麼」。改錯 payment endpoint 會讓付款打到錯誤目標；改錯 rate limit 可能讓合法流量被擋；改錯 log level 最多是 log 太吵或太安靜。設定的注入方式與版本追蹤見 <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>。</p>
<h2 id="secret-boundary">Secret Boundary</h2>
<p>Credential、token、certificate 與 machine identity 需要可輪替、可稽核、可回退——Secret boundary 管理這組生命週期。Secret 變更同時影響平台、應用與外部依賴，應使用比普通 config 更嚴格的 evidence 與 rollback window。</p>
<p>Secret rollout 要回答版本相容、雙軌驗證、舊 secret 撤除時間與失敗回退。這裡要接到 <a href="/blog/backend/07-security-data-protection/credential-rotation-scoped-evidence/" data-link-title="7.27 Credential Rotation with Scoped Evidence 實作示範" data-link-desc="以 webhook/API credential 輪替示範 scope map、證據欄位與回退窗口如何一起設計。">7.27 Credential Rotation with Scoped Evidence</a>。</p>
<h3 id="secret-rollout-的雙軌驗證">Secret Rollout 的雙軌驗證</h3>
<p>Secret 輪替跟應用版本部署有本質差異：rollback secret 不是「換回舊版本」那麼單純——舊 secret 可能已經被撤銷、過期、或在外部系統中標記為失效。Secret rollout 的安全做法是雙軌驗證：</p>
<ol>
<li><strong>新 secret 先加入、舊 secret 暫不移除</strong>：應用先驗證能用新 secret 正常運作。</li>
<li><strong>觀測窗口確認新 secret 穩定</strong>：auth 成功率、API 呼叫成功率、certificate handshake 成功率都在 baseline 內。</li>
<li><strong>確認後移除舊 secret</strong>：舊 secret 的撤除要有明確時間點，而且要在撤除前確認沒有服務還在用舊 secret。</li>
</ol>
<p>這個流程的風險點是第 3 步：撤除舊 secret 後發現某個遺漏的服務或 job 還在用、導致該服務認證失敗。盤點覆蓋率的做法是在觀測窗口內搜尋 audit log，確認所有 secret 使用都已切到新版本。</p>
<h2 id="service-discovery-boundary">Service Discovery Boundary</h2>
<p>Service discovery 的責任是維持可用 endpoint 集合。它回答服務應該連到哪些實例；業務設定與版本正確性則分別交給 config boundary 與 rollout gate。Discovery 的 DNS / registry 運作模式與註冊時序見 <a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4 Service Discovery</a>。</p>
<p>Discovery 失準常見於 rollout、擴縮容與區域故障。判讀時要拆成註冊時序、健康判斷、DNS/registry 新鮮度與 fallback 存活時間。</p>
<h2 id="control-plane-boundary">Control Plane Boundary</h2>
<p>設定、策略、部署與路由規則的管理落在 <a href="/blog/backend/knowledge-cards/management-plane/" data-link-title="Management Plane" data-link-desc="說明管理平面如何與業務流量平面分離，避免高權限入口擴散">management plane</a>。Control plane 變更會影響大量服務，因此需要更嚴格的 evidence、gate 與 decision log。</p>
<p>Control plane 事故常見於規則推送、routing 誤配、secret 下發失敗與 registry 異常。這類事故要先保留 decision timeline，避免事後只看到資料面錯誤率。</p>
<h3 id="control-plane-變更的-blast-radius-控制">Control Plane 變更的 Blast Radius 控制</h3>
<p>Control plane 變更的 blast radius 跟 data plane 變更不同——一條 routing rule 推送錯誤可能同時影響所有服務的流量。控制 blast radius 的做法：</p>
<ol>
<li><strong>分批推送</strong>：規則變更先推到 staging / canary namespace、驗證後再推到 production。推送結果的觀測應包含受影響服務的 error rate 與 latency。</li>
<li><strong>approval gate</strong>：高影響變更（network policy、admission webhook、RBAC binding）需要多人 review。變更的 blast radius 估算（影響多少 namespace / service）應在 review 時可見。</li>
<li><strong>decision log</strong>：所有 control plane 變更記入 <a href="/blog/backend/08-incident-response/control-plane-decision-log-write-back/" data-link-title="8.23 Control Plane Decision Log and Write-back 實作示範" data-link-desc="以 rule/config rollout 事故示範 decision log 與 write-back 如何形成可回放閉環。">8.23 Control Plane Decision Log</a>，包含時間、操作者、受影響範圍、預期效果與回退條件。事故時對照 decision log 跟 data plane 症狀的時間序列，可以快速判斷因果。</li>
</ol>
<h2 id="平台元件升級的可重播流程">平台元件升級的可重播流程</h2>
<p>平台基礎元件升級是 control plane 風險最高的場景。Service mesh、ingress controller、CNI、API server 這類元件影響面廣、單次升級可能形成全域風險放大器。</p>
<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>：揭露 1 個判讀（基礎平台元件升級缺乏分批治理會形成全域風險放大器）+ 3 條策略（分批升級 + 回退窗口、升級驗證標準固定化、升級事件接入 incident command 節奏）。以下基於通用工程知識展開、「升級事件進 timeline」是從 case「接入 incident command」策略進一步推到具體操作。</p>
<p>可重複套用的升級流程：</p>
<ol>
<li><strong>分批升級單位</strong>：先在開發 / staging 叢集驗證、再選低流量 production 叢集 / namespace 作為先導、之後分批擴大。分批單位可以是叢集、namespace、region、tenant，依風險面選擇。</li>
<li><strong>回退窗口跟驗證標準同時設</strong>：每批升級前定義「驗證通過」的具體訊號（SLI 維持、特定 metric 不偏移、無新告警），跟「回退窗口」（多久內可以回退）。沒有驗證標準的分批等於連續高風險動作。</li>
<li><strong>升級流程紀錄到 incident-style 文件</strong>：升級期間的決策、觀察、停止點都用 incident decision log 格式紀錄。下次升級可重播、不依賴執行者個人經驗。</li>
<li><strong>升級事件進 timeline</strong>：升級本身產生的短暫錯誤、reconnect、配置同步延遲，要在事故 timeline 上可見、避免被誤判成事故。</li>
</ol>
<p>平台元件升級的核心治理價值是把「一次性高風險作業」變成「可重複的低風險作業」。第一次升級用流程，第二次升級用同樣流程，第三次升級流程已經穩定到可以委派、不再需要資深工程師親自執行。</p>
<h2 id="managed-平台跟團隊職責邊界">Managed 平台跟團隊職責邊界</h2>
<p>平台託管化（self-managed → managed）改變維運責任跟團隊精力的分配。本段聚焦團隊職責邊界；流量跟依賴的分段切換流程見 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/#%e5%88%86%e9%9a%8e%e6%ae%b5%e5%b9%b3%e5%8f%b0%e9%81%b7%e7%a7%bb" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 分階段平台遷移</a>、紅隊視角的攻擊面變動見 <a href="/blog/backend/05-deployment-platform/attacker-view-platform-entry-risks/#%e5%b9%b3%e5%8f%b0%e9%81%b7%e7%a7%bb%e6%9c%9f%e7%9a%84%e6%94%bb%e6%93%8a%e9%9d%a2%e8%ae%8a%e5%8b%95" data-link-title="5.5 平台與入口威脅建模（Threat Modeling）" data-link-desc="以概念層判讀部署平台弱點，聚焦入口、生命週期、設定與交付節奏">5.5 平台遷移期的攻擊面變動</a>、三者組合才完整。</p>
<p>Platform team 從「維持 Kubernetes 跑起來」轉向「定義 release flow、observability convention、cost governance」。managed 平台採用後第一個治理動作是顯式重新定義職責邊界、讓 platform team 從 cluster ops 轉到 release flow / observability convention / cost governance。重新定義缺位、組織轉型紅利容易被誤判為純技術升級。</p>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5 Miro Managed EKS 遷移</a>：揭露 1 個判讀（平台託管化的價值在讓團隊把心力從底層維護轉到交付效率與可靠性策略）+ 3 條策略（先定義遷移後的平台責任邊界、自動化流程取代手動平台操作、incident 跟 release policy 接回平台治理）。對應 <a href="/blog/backend/09-performance-capacity/cases/maersk-bosch-azure-aks/" data-link-title="9.C33 Maersk &#43; Bosch：傳統產業在 Azure AKS 上的微服務治理" data-link-desc="全球海運 Maersk 跟 Bosch 智慧建築把 AKS 當微服務治理基礎、釋放工程資源做業務功能">9.C33 Maersk + Bosch Azure AKS</a>：揭露 Maersk 工程訴求引語「focus on things that makes the most business impact」、傳統產業 K8s 動機是治理一致性 + 釋放工程資源到業務功能（後者屬作者判讀）。以下基於通用工程知識展開。</p>
<p>managed 平台採用後的職責邊界重訂可以分四層：</p>
<ol>
<li><strong>Cluster 層</strong>：control plane 上游接管（API server、etcd、scheduler、controller-manager）、platform team 從 cluster ops 退到 cluster policy。CIS benchmark、network policy、admission controller 配置仍是 platform 責任。</li>
<li><strong>Cluster-internal 層</strong>：CNI、ingress controller、service mesh、cluster DNS、storage CSI 通常仍由 platform team own。這層是 managed 服務沒覆蓋的 grey zone、需要明確 ownership。</li>
<li><strong>Application 層</strong>：deployment、service、HPA、PDB 由 service team own、platform 提供 convention 跟 review process。</li>
<li><strong>跨層議題</strong>：cost governance、observability convention、release flow、incident response 是 platform / service / SRE / finance 跨層協作、需要 operating model 明確化。</li>
</ol>
<p>managed 採用後 day-1 治理項目有兩件事：明確界定 grey zone ownership（避免「以為 managed 服務什麼都管了」的心智模型）、把 platform team 心力從 cluster ops 轉到組織轉型紅利（release flow、observability convention、cost governance）。把重新定義職責當 day-2 議題、會錯失組織轉型紅利。</p>
<h2 id="選型前判準">選型前判準</h2>
<p>平台選型前要先回答：</p>
<ol>
<li>哪些變更屬於 traffic，哪些屬於 config，哪些屬於 secret。</li>
<li>每種變更是否能分批、暫停與回退。</li>
<li>Discovery 失準時是否有可控 fallback。</li>
<li>Control plane 變更是否有 audit、owner 與 <a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 限制。</li>
<li>基礎元件升級是否有可重播流程跟回退窗口。</li>
<li>Managed 平台採用後團隊職責邊界是否重新定義。</li>
</ol>
<p>這些答案決定後續要比較 load balancer、service mesh、secret manager、service registry 或 deployment controller 的能力。</p>
<h2 id="實體服務討論承接點">實體服務討論承接點</h2>
<p>實體平台文章要承接本篇的 traffic、config 與 control plane boundary。ELB、nginx、Envoy、service mesh、Consul、Kubernetes controller、secret manager 或 Terraform 的比較，要先分清它們是在資料面接流量、在控制面改規則，還是在設定面下發狀態。</p>
<p>若主問題是流量切換，後續文章要比較 routing rule、weight、health check、drain 與 rollback。若主問題是設定與 secret，後續文章要比較 rollout、audit、rotation 與相容窗口。若主問題是 control plane 風險，後續文章要比較 blast radius、approval、observability 與 incident decision log。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>要把流量邊界接到實際 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>。要把 control plane 決策寫入事故流程，接著讀 <a href="/blog/backend/08-incident-response/control-plane-decision-log-write-back/" data-link-title="8.23 Control Plane Decision Log and Write-back 實作示範" data-link-desc="以 rule/config rollout 事故示範 decision log 與 write-back 如何形成可回放閉環。">8.23 Control Plane Decision Log and Write-back</a>。</p>
]]></content:encoded></item><item><title>5.8 Deployment Rollout with Drain and Rollback（實作示範）</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/</guid><description>&lt;p>Deployment rollout with drain and rollback 的核心責任是把版本、流量、連線、設定與回退條件拆成可驗證批次。這篇以 checkout service 為例，示範平台切換如何從 preflight、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary&lt;/a>、drain 到事故回退都保留一致證據。&lt;/p>
&lt;p>本篇以 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes 部署策略&lt;/a> 與 &lt;a href="https://tarrragon.github.io/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 合約&lt;/a> 為前置知識——rollout 批次、probe 對齊、drain contract 等概念在該兩篇定義，本篇直接操作化。lifecycle 狀態的完整定義見 &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>。&lt;/p>
&lt;h2 id="服務路徑與切換責任">服務路徑與切換責任&lt;/h2>
&lt;p>這條路徑是 &lt;code>client -&amp;gt; load balancer -&amp;gt; checkout-api -&amp;gt; payment provider/order db/order event&lt;/code>。部署期間新舊版本會同時承接流量，核心風險在流量生命週期是否可收斂，image 替換本身反而是最可預測的部分。&lt;/p>
&lt;p>切換責任分三層：&lt;/p>
&lt;ol>
&lt;li>版本可啟動：container/runtime/config 可用。&lt;/li>
&lt;li>版本可接流量：readiness 與依賴狀態對齊。&lt;/li>
&lt;li>版本可退場：drain 與在途請求可收束。&lt;/li>
&lt;/ol>
&lt;h2 id="preflight先驗證可服務基線">Preflight：先驗證可服務基線&lt;/h2>
&lt;p>Preflight 的責任是把「可啟動」與「可服務」拆開驗證。最小檢查包含：&lt;/p>
&lt;ol>
&lt;li>image 與 runtime config 版本對齊。&lt;/li>
&lt;li>secret 已注入且權限正確。&lt;/li>
&lt;li>startup/readiness probe 能反映真實依賴狀態。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-balancer-contract/" data-link-title="Load Balancer Contract" data-link-desc="說明服務與負載平衡器之間的流量與健康檢查約定">load balancer contract&lt;/a> 參數與服務期望一致。&lt;/li>
&lt;li>service discovery 註冊與摘除路徑可用。&lt;/li>
&lt;/ol>
&lt;p>Preflight 失敗時不進 canary。先把失敗收斂在控制面，避免切流後才發現版本不可服務。&lt;/p>
&lt;h3 id="preflight-自動化">Preflight 自動化&lt;/h3>
&lt;p>手動 preflight 在低頻部署時可行，部署頻率上升後會成為瓶頸或被跳過。穩定做法是把 preflight 檢查嵌入 CI/CD pipeline 的 pre-deploy stage：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>image 與 config 版本對齊檢查&lt;/strong>：pipeline 比對即將部署的 image tag 與 ConfigMap / Secret 版本是否在相容矩陣內。版本矩陣可維護在 git（如 &lt;code>deploy/compat-matrix.yaml&lt;/code>），CI 自動比對。&lt;/li>
&lt;li>&lt;strong>infra drift detection&lt;/strong>：部署前用 IaC 工具（Terraform plan、Crossplane drift check）掃描目標環境的實際狀態是否跟宣告狀態一致。drift 存在時暫停部署——在已漂移的環境上部署新版本，會把漂移與版本變更的影響混在一起，事故時無法分辨根因。&lt;/li>
&lt;li>&lt;strong>probe 語意驗證&lt;/strong>：在 staging 環境對新版本觸發 startup → readiness → liveness 全流程，確認 probe 回應與依賴就緒條件吻合。這步抓的是 probe 設定退化（如 readiness endpoint 被改成永遠回 200）。&lt;/li>
&lt;li>&lt;strong>rollback 可行性驗證&lt;/strong>：確認舊版本 image 仍在 registry 且可拉取、舊版本 config 仍相容。rollback 能力在 preflight 階段驗證，比事故時才發現「舊版拉不到」代價低得多。&lt;/li>
&lt;/ol>
&lt;p>Preflight 自動化的產出是一份 go/no-go 報告，進入 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate&lt;/a> 作為放行依據。pipeline 中的 preflight stage 失敗應阻擋部署而非產生警告——可忽略的 preflight 等於沒有 preflight。&lt;/p>
&lt;h2 id="canary-batch-與-stop-condition">Canary Batch 與 Stop Condition&lt;/h2>
&lt;p>小流量先驗證新版本行為，再決定是否擴批——Canary 回答的是「這個版本值不值得擴大」。&lt;/p></description><content:encoded><![CDATA[<p>Deployment rollout with drain and rollback 的核心責任是把版本、流量、連線、設定與回退條件拆成可驗證批次。這篇以 checkout service 為例，示範平台切換如何從 preflight、<a href="/blog/backend/knowledge-cards/canary-release/" data-link-title="Canary Release" data-link-desc="分批把流量導向新版本、用 stop condition 控制 blast radius 的部署策略">canary</a>、drain 到事故回退都保留一致證據。</p>
<p>本篇以 <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> 與 <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> 為前置知識——rollout 批次、probe 對齊、drain contract 等概念在該兩篇定義，本篇直接操作化。lifecycle 狀態的完整定義見 <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>。</p>
<h2 id="服務路徑與切換責任">服務路徑與切換責任</h2>
<p>這條路徑是 <code>client -&gt; load balancer -&gt; checkout-api -&gt; payment provider/order db/order event</code>。部署期間新舊版本會同時承接流量，核心風險在流量生命週期是否可收斂，image 替換本身反而是最可預測的部分。</p>
<p>切換責任分三層：</p>
<ol>
<li>版本可啟動：container/runtime/config 可用。</li>
<li>版本可接流量：readiness 與依賴狀態對齊。</li>
<li>版本可退場：drain 與在途請求可收束。</li>
</ol>
<h2 id="preflight先驗證可服務基線">Preflight：先驗證可服務基線</h2>
<p>Preflight 的責任是把「可啟動」與「可服務」拆開驗證。最小檢查包含：</p>
<ol>
<li>image 與 runtime config 版本對齊。</li>
<li>secret 已注入且權限正確。</li>
<li>startup/readiness probe 能反映真實依賴狀態。</li>
<li><a href="/blog/backend/knowledge-cards/load-balancer-contract/" data-link-title="Load Balancer Contract" data-link-desc="說明服務與負載平衡器之間的流量與健康檢查約定">load balancer contract</a> 參數與服務期望一致。</li>
<li>service discovery 註冊與摘除路徑可用。</li>
</ol>
<p>Preflight 失敗時不進 canary。先把失敗收斂在控制面，避免切流後才發現版本不可服務。</p>
<h3 id="preflight-自動化">Preflight 自動化</h3>
<p>手動 preflight 在低頻部署時可行，部署頻率上升後會成為瓶頸或被跳過。穩定做法是把 preflight 檢查嵌入 CI/CD pipeline 的 pre-deploy stage：</p>
<ol>
<li><strong>image 與 config 版本對齊檢查</strong>：pipeline 比對即將部署的 image tag 與 ConfigMap / Secret 版本是否在相容矩陣內。版本矩陣可維護在 git（如 <code>deploy/compat-matrix.yaml</code>），CI 自動比對。</li>
<li><strong>infra drift detection</strong>：部署前用 IaC 工具（Terraform plan、Crossplane drift check）掃描目標環境的實際狀態是否跟宣告狀態一致。drift 存在時暫停部署——在已漂移的環境上部署新版本，會把漂移與版本變更的影響混在一起，事故時無法分辨根因。</li>
<li><strong>probe 語意驗證</strong>：在 staging 環境對新版本觸發 startup → readiness → liveness 全流程，確認 probe 回應與依賴就緒條件吻合。這步抓的是 probe 設定退化（如 readiness endpoint 被改成永遠回 200）。</li>
<li><strong>rollback 可行性驗證</strong>：確認舊版本 image 仍在 registry 且可拉取、舊版本 config 仍相容。rollback 能力在 preflight 階段驗證，比事故時才發現「舊版拉不到」代價低得多。</li>
</ol>
<p>Preflight 自動化的產出是一份 go/no-go 報告，進入 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a> 作為放行依據。pipeline 中的 preflight stage 失敗應阻擋部署而非產生警告——可忽略的 preflight 等於沒有 preflight。</p>
<h2 id="canary-batch-與-stop-condition">Canary Batch 與 Stop Condition</h2>
<p>小流量先驗證新版本行為，再決定是否擴批——Canary 回答的是「這個版本值不值得擴大」。</p>
<table>
  <thead>
      <tr>
          <th>批次階段</th>
          <th>判讀重點</th>
          <th>停損條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1-5%</td>
          <td>per-version error rate、p95/p99 latency</td>
          <td>錯誤率高於基線、延遲持續惡化</td>
      </tr>
      <tr>
          <td>10-25%</td>
          <td>payment dependency timeout、fallback 比例</td>
          <td>依賴 timeout 連續超門檻</td>
      </tr>
      <tr>
          <td>50%</td>
          <td>drain 成功率、reconnect 波形、下游事件完整性</td>
          <td>drain 未完成或 reconnect storm</td>
      </tr>
      <tr>
          <td>100% 前</td>
          <td>新舊版本差異是否收斂、rollback 可行性</td>
          <td>仍需依賴舊版本特殊路徑</td>
      </tr>
  </tbody>
</table>
<p>canary 判讀要維持 per-version 視角。只看整體服務平均值會掩蓋新版本局部退化。</p>
<h2 id="traffic--drain把退場變成可驗證流程">Traffic / Drain：把退場變成可驗證流程</h2>
<p>Drain 的責任是讓舊版本在下線前完成在途請求，不讓 rollout 把短暫切換放大成用戶錯誤。</p>
<p>退場順序：</p>
<ol>
<li>舊實例 readiness 先轉 <code>not-ready</code> 停接新流量。</li>
<li>保留 drain 窗口完成 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a> request。</li>
<li>確認連線數下降到門檻後再終止進程。</li>
<li>驗證無異常 reconnect 尖峰再進下一批。</li>
</ol>
<p>Drain 條件的完整 workload 分類回到 <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>，本段以 checkout service 為例：短 API 的 <a href="/blog/backend/knowledge-cards/draining/" data-link-title="Draining" data-link-desc="說明服務如何先停止接收新流量，再讓既有工作完成">draining</a> 窗口可短，長輪詢與 webhook callback 要更保守。</p>
<h2 id="rollback-compatibility">Rollback Compatibility</h2>
<p>舊版本回來時仍可運作，是 rollback 能成立的前提——回退如果變成第二次故障，就失去了回退的工程價值。</p>
<p>要先驗證四個相容面：</p>
<ol>
<li>config 相容：新設定不會讓舊版啟動失敗。</li>
<li>schema 相容：資料結構仍可被舊版讀取。</li>
<li>cache key 相容：舊版可讀新快取或有 fallback。</li>
<li>event schema 相容：舊版 consumer 不會因新事件欄位崩潰。</li>
</ol>
<p>若這四項未完成，所謂 rollback 只會停在「版本回切」，無法恢復服務正確性。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>每一批切換要可被判讀、可被追責、可被回放——部署 evidence 支撐這三個條件。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>deployment logs、LB metrics、service metrics、dependency logs</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">Time range</a></td>
          <td>每批 rollout/drain 觀察窗口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">Query link</a></td>
          <td>per-version error、latency、5xx、timeout、drain completion</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>platform owner、checkout owner、SRE on-call</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">Data quality</a></td>
          <td>指標延遲、分區覆蓋、log 掉點</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">Confidence</a></td>
          <td>confirmed / suspected / needs follow-up</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">Known gap</a></td>
          <td>尚未覆蓋長連線場景、低流量區域樣本不足</td>
      </tr>
  </tbody>
</table>
<p>這份 evidence 要對齊 <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 Observability Evidence Package</a>。</p>
<h2 id="release-gate">Release Gate</h2>
<p>Release gate 的責任是決定下一批切換與是否凍結 rollout，不是報告「目前看起來正常」。</p>
<table>
  <thead>
      <tr>
          <th>Gate 欄位</th>
          <th>最小內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/gate-decision/" data-link-title="Gate Decision" data-link-desc="說明 release gate 如何把證據轉成放行、暫停、回退或補證據的決策">Gate decision</a></td>
          <td>放行下一批、維持 canary、freeze rollout、rollback version</td>
      </tr>
      <tr>
          <td>Checks</td>
          <td>per-version SLI、dependency timeout、drain completion</td>
      </tr>
      <tr>
          <td>Stop condition</td>
          <td>error <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a>、reconnect storm、drain 逾時</td>
      </tr>
      <tr>
          <td>Rollback window</td>
          <td>可回切時間、舊版可服務窗口、config 回退窗口</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>release owner、platform on-call</td>
      </tr>
  </tbody>
</table>
<p>這組欄位要對齊 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a>。</p>
<h2 id="incident-decision-log">Incident Decision Log</h2>
<p>freeze rollout、rollback version、隔離 region、延長 drain 都屬事故決策，需寫入 <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/knowledge-cards/control-plane/" data-link-title="Control Plane" data-link-desc="負責下發策略、配置與路由決策的控制層">control plane</a> 設定推送的決策、見 <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> 跟 <a href="/blog/backend/08-incident-response/control-plane-decision-log-write-back/" data-link-title="8.23 Control Plane Decision Log and Write-back 實作示範" data-link-desc="以 rule/config rollout 事故示範 decision log 與 write-back 如何形成可回放閉環。">8.23 Control Plane Decision Log</a>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">incident_decision</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">timestamp</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-05-11T15:06:00Z</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">decision</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;freeze rollout at 25% and rollback one region&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">context</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;new version timeout to payment provider increased in ap-northeast&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">evidence</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span>- <span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l">checkout_error_rate_by_version_region</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span>- <span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l">payment_timeout_ratio_by_region</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">owner</span><span class="p">:</span><span class="w"> </span><span class="l">release-incident-commander</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">expected_effect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;contain customer impact and restore baseline success rate&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">rollback_condition</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;timeout ratio does not recover after rollback batch completes&#34;</span></span></span></code></pre></div><h2 id="case-write-back-與邊界">Case Write-back 與邊界</h2>
<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/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift</a> 與 <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</a>：前者看切換失序，後兩者看遷移路徑與回退策略。preflight / canary / drain 各階段的生命週期定義回到 <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>。</p>
<p>這篇不處理 schema migration 本身、cache stampede 或 queue replay。若核心風險在資料正式狀態、快取回源或事件恢復，路由到 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a>、<a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9 Cache Migration 與 Stampede Rollback</a> 或 <a href="/blog/backend/03-message-queue/queue-consumer-retry-replay-handoff/" data-link-title="3.8 Queue Consumer Retry 與 Replay Handoff（實作示範）" data-link-desc="以 order_created consumer 示範 queue 路徑如何交付 idempotency evidence、DLQ handling、replay runbook 與 incident decision log。">3.8 Queue Consumer Retry 與 Replay Handoff</a>。</p>
]]></content:encoded></item><item><title>5.9 邊緣分發與靜態資源（CDN / Origin Protection）</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/edge-cdn-static-distribution/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/edge-cdn-static-distribution/</guid><description>&lt;p>邊緣分發的核心責任是把靜態與半靜態內容放到離使用者最近的網路節點，讓 origin 不必為每一筆讀取請求承擔流量與延遲。CDN 屬於部署平台的網路入口層，跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 模組的應用層快取&lt;/a> 是不同責任：CDN 解決「請求是否需要進到應用程式」，應用層快取解決「應用程式如何降低資料層讀寫成本」。這個邊界清楚後，origin 保護策略與快取一致性設計才能各自展開。&lt;/p>
&lt;h2 id="三層快取的責任分工">三層快取的責任分工&lt;/h2>
&lt;p>CDN、應用層快取與資料層快取串成一條快取分層。每一層各有自己的 freshness 模型、失效路徑與失敗代價，需要各自設計策略。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層級&lt;/th>
 &lt;th>主要載體&lt;/th>
 &lt;th>主要責任&lt;/th>
 &lt;th>失效成本&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>邊緣層&lt;/td>
 &lt;td>CDN edge node、browser cache&lt;/td>
 &lt;td>降低跨網延遲、保護 origin 流量&lt;/td>
 &lt;td>全球節點 purge&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>應用層&lt;/td>
 &lt;td>Redis、in-memory cache、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-aside/" data-link-title="Cache Aside" data-link-desc="說明 application 如何在讀取時自行管理快取與正式資料來源">cache aside&lt;/a>&lt;/td>
 &lt;td>降低資料層查詢成本&lt;/td>
 &lt;td>區域 cluster purge&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料層快取&lt;/td>
 &lt;td>DB buffer pool、query cache&lt;/td>
 &lt;td>降低硬碟 I/O&lt;/td>
 &lt;td>內部自動管理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>讀者實作時要先判斷需求屬於哪一層。把使用者頭像、商品圖片、活動 banner 放邊緣層；把熱門商品價格、會員等級放應用層；DB 自身的 buffer pool 留給資料庫引擎管理。混用會造成失效路徑互相覆蓋，事故時難以判斷快取漂移來自哪一層。&lt;/p>
&lt;h2 id="origin-protection-的設計責任">Origin Protection 的設計責任&lt;/h2>
&lt;p>CDN 在規模成長路徑上承擔 origin protection。當 KOL 引流或熱門活動同秒帶入大量請求時，沒有邊緣層遮蔽，origin 的應用伺服器、API gateway 與資料庫會被同步擊穿。邊緣層的責任是讓 origin 流量曲線跟使用者請求曲線解耦。&lt;/p>
&lt;p>origin protection 的核心策略包含三個方向：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>cache hit ratio 優化&lt;/strong>：把高頻、可共用的內容做成可快取資源（含正確的 cache-control header、ETag 跟 vary 設計）。命中率每提升 10 個百分點，origin 流量幾乎等比例下降。&lt;/li>
&lt;li>&lt;strong>回源行為控制&lt;/strong>：edge 沒命中時用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">Cache Stampede&lt;/a> 保護機制（origin shield 是 CDN 內部多一層中央節點集中回源、coalescing / request collapsing 把同時打進來的 N 個請求合併成一次 origin 呼叫）、避免擊穿。&lt;/li>
&lt;li>&lt;strong>failure fallback&lt;/strong>：origin 不健康時、edge 可以回傳舊版本（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stale-while-revalidate/" data-link-title="Stale-While-Revalidate" data-link-desc="HTTP cache-control directive，cache 過期後仍立即回舊版、背景發出 origin request 拉取新版本更新快取">stale-while-revalidate&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stale-if-error/" data-link-title="Stale-If-Error" data-link-desc="HTTP cache-control directive、origin 出錯時用舊版頂著、確保使用者拿到有效回應">stale-if-error&lt;/a>）、避免使用者直接看到 5xx。代價是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stale-data/" data-link-title="Stale Data" data-link-desc="說明過期資料在快取、replica 與衍生資料中的產品影響">Stale Data&lt;/a> 風險暫時提高、需要在 freshness budget 內。&lt;/li>
&lt;/ol>
&lt;p>Origin shield 跟 request coalescing 常被混為一談，兩者解決的問題不同。Origin shield 在 CDN 內部插入一層中央節點——全球 edge POP 的 cache miss 先集中到 shield 節點，shield 再向 origin 回源；它解決的是「N 個 edge POP 同時 miss 變成 N 次 origin 請求」的扇出放大。Request coalescing（也叫 request collapsing）在單一節點內把同時到達的多個相同請求合併成一次 origin 呼叫；它解決的是「同一個 edge POP 在同一毫秒收到 1000 個相同請求」的並發放大。兩者是不同層級的保護——shield 跨節點收斂、coalescing 單節點收斂——可以同時啟用形成兩層防線。&lt;/p>
&lt;p>這三項決定了「能不能撐住高峰」。三項做齊才能形成保護網；缺項時邊緣層僅能發揮降低延遲的效果。&lt;/p>
&lt;h2 id="cacheable-vs-non-cacheable-的判讀">Cacheable vs Non-Cacheable 的判讀&lt;/h2>
&lt;p>CDN 適合承接的資源有明確判讀條件：對所有使用者一致、且可容忍短暫舊版。符合這兩個條件的資源放邊緣層收益最高，不符合的留在應用層或 origin 處理。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>資源類型&lt;/th>
 &lt;th>適合放 CDN？&lt;/th>
 &lt;th>判讀理由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>靜態 asset（JS/CSS）&lt;/td>
 &lt;td>適合&lt;/td>
 &lt;td>內容與使用者無關，hash 命名後可長期快取&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>圖片、影片&lt;/td>
 &lt;td>適合&lt;/td>
 &lt;td>公開資源，跨使用者共用，命中率高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>商品頁、活動頁&lt;/td>
 &lt;td>條件適合&lt;/td>
 &lt;td>對未登入者一致；對登入者需要分版本或退到應用層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>訂單頁、會員中心&lt;/td>
 &lt;td>不適合&lt;/td>
 &lt;td>跟特定使用者綁定，邊緣層無法共用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>個人化推薦&lt;/td>
 &lt;td>不適合&lt;/td>
 &lt;td>每個請求結果不同，命中率近於零&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫入 API&lt;/td>
 &lt;td>不適合&lt;/td>
 &lt;td>邊緣層不該攔截狀態改變&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表覆蓋傳統靜態 / 動態二分情境。邊緣層演化出來的中間態超出表格範圍 — 包含 API responses with short TTL（GET、idempotent）、SSR / SSG 混合頁、signed URL / per-user 私有 asset（CloudFront / Cloudflare 可帶簽章對特定 user 快取）、i18n / 地理變體用 Vary header 處理跨 locale 共用、以及 edge personalization / edge compute（Cloudflare Workers、Lambda@Edge、Akamai EdgeWorkers）。進入這層要評估 edge compute 成本與 cache key 設計複雜度、不是簡單套表決定。&lt;/p></description><content:encoded><![CDATA[<p>邊緣分發的核心責任是把靜態與半靜態內容放到離使用者最近的網路節點，讓 origin 不必為每一筆讀取請求承擔流量與延遲。CDN 屬於部署平台的網路入口層，跟 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 模組的應用層快取</a> 是不同責任：CDN 解決「請求是否需要進到應用程式」，應用層快取解決「應用程式如何降低資料層讀寫成本」。這個邊界清楚後，origin 保護策略與快取一致性設計才能各自展開。</p>
<h2 id="三層快取的責任分工">三層快取的責任分工</h2>
<p>CDN、應用層快取與資料層快取串成一條快取分層。每一層各有自己的 freshness 模型、失效路徑與失敗代價，需要各自設計策略。</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>主要載體</th>
          <th>主要責任</th>
          <th>失效成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>邊緣層</td>
          <td>CDN edge node、browser cache</td>
          <td>降低跨網延遲、保護 origin 流量</td>
          <td>全球節點 purge</td>
      </tr>
      <tr>
          <td>應用層</td>
          <td>Redis、in-memory cache、<a href="/blog/backend/knowledge-cards/cache-aside/" data-link-title="Cache Aside" data-link-desc="說明 application 如何在讀取時自行管理快取與正式資料來源">cache aside</a></td>
          <td>降低資料層查詢成本</td>
          <td>區域 cluster purge</td>
      </tr>
      <tr>
          <td>資料層快取</td>
          <td>DB buffer pool、query cache</td>
          <td>降低硬碟 I/O</td>
          <td>內部自動管理</td>
      </tr>
  </tbody>
</table>
<p>讀者實作時要先判斷需求屬於哪一層。把使用者頭像、商品圖片、活動 banner 放邊緣層；把熱門商品價格、會員等級放應用層；DB 自身的 buffer pool 留給資料庫引擎管理。混用會造成失效路徑互相覆蓋，事故時難以判斷快取漂移來自哪一層。</p>
<h2 id="origin-protection-的設計責任">Origin Protection 的設計責任</h2>
<p>CDN 在規模成長路徑上承擔 origin protection。當 KOL 引流或熱門活動同秒帶入大量請求時，沒有邊緣層遮蔽，origin 的應用伺服器、API gateway 與資料庫會被同步擊穿。邊緣層的責任是讓 origin 流量曲線跟使用者請求曲線解耦。</p>
<p>origin protection 的核心策略包含三個方向：</p>
<ol>
<li><strong>cache hit ratio 優化</strong>：把高頻、可共用的內容做成可快取資源（含正確的 cache-control header、ETag 跟 vary 設計）。命中率每提升 10 個百分點，origin 流量幾乎等比例下降。</li>
<li><strong>回源行為控制</strong>：edge 沒命中時用 <a href="/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">Cache Stampede</a> 保護機制（origin shield 是 CDN 內部多一層中央節點集中回源、coalescing / request collapsing 把同時打進來的 N 個請求合併成一次 origin 呼叫）、避免擊穿。</li>
<li><strong>failure fallback</strong>：origin 不健康時、edge 可以回傳舊版本（<a href="/blog/backend/knowledge-cards/stale-while-revalidate/" data-link-title="Stale-While-Revalidate" data-link-desc="HTTP cache-control directive，cache 過期後仍立即回舊版、背景發出 origin request 拉取新版本更新快取">stale-while-revalidate</a> / <a href="/blog/backend/knowledge-cards/stale-if-error/" data-link-title="Stale-If-Error" data-link-desc="HTTP cache-control directive、origin 出錯時用舊版頂著、確保使用者拿到有效回應">stale-if-error</a>）、避免使用者直接看到 5xx。代價是 <a href="/blog/backend/knowledge-cards/stale-data/" data-link-title="Stale Data" data-link-desc="說明過期資料在快取、replica 與衍生資料中的產品影響">Stale Data</a> 風險暫時提高、需要在 freshness budget 內。</li>
</ol>
<p>Origin shield 跟 request coalescing 常被混為一談，兩者解決的問題不同。Origin shield 在 CDN 內部插入一層中央節點——全球 edge POP 的 cache miss 先集中到 shield 節點，shield 再向 origin 回源；它解決的是「N 個 edge POP 同時 miss 變成 N 次 origin 請求」的扇出放大。Request coalescing（也叫 request collapsing）在單一節點內把同時到達的多個相同請求合併成一次 origin 呼叫；它解決的是「同一個 edge POP 在同一毫秒收到 1000 個相同請求」的並發放大。兩者是不同層級的保護——shield 跨節點收斂、coalescing 單節點收斂——可以同時啟用形成兩層防線。</p>
<p>這三項決定了「能不能撐住高峰」。三項做齊才能形成保護網；缺項時邊緣層僅能發揮降低延遲的效果。</p>
<h2 id="cacheable-vs-non-cacheable-的判讀">Cacheable vs Non-Cacheable 的判讀</h2>
<p>CDN 適合承接的資源有明確判讀條件：對所有使用者一致、且可容忍短暫舊版。符合這兩個條件的資源放邊緣層收益最高，不符合的留在應用層或 origin 處理。</p>
<table>
  <thead>
      <tr>
          <th>資源類型</th>
          <th>適合放 CDN？</th>
          <th>判讀理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>靜態 asset（JS/CSS）</td>
          <td>適合</td>
          <td>內容與使用者無關，hash 命名後可長期快取</td>
      </tr>
      <tr>
          <td>圖片、影片</td>
          <td>適合</td>
          <td>公開資源，跨使用者共用，命中率高</td>
      </tr>
      <tr>
          <td>商品頁、活動頁</td>
          <td>條件適合</td>
          <td>對未登入者一致；對登入者需要分版本或退到應用層</td>
      </tr>
      <tr>
          <td>訂單頁、會員中心</td>
          <td>不適合</td>
          <td>跟特定使用者綁定，邊緣層無法共用</td>
      </tr>
      <tr>
          <td>個人化推薦</td>
          <td>不適合</td>
          <td>每個請求結果不同，命中率近於零</td>
      </tr>
      <tr>
          <td>寫入 API</td>
          <td>不適合</td>
          <td>邊緣層不該攔截狀態改變</td>
      </tr>
  </tbody>
</table>
<p>這張表覆蓋傳統靜態 / 動態二分情境。邊緣層演化出來的中間態超出表格範圍 — 包含 API responses with short TTL（GET、idempotent）、SSR / SSG 混合頁、signed URL / per-user 私有 asset（CloudFront / Cloudflare 可帶簽章對特定 user 快取）、i18n / 地理變體用 Vary header 處理跨 locale 共用、以及 edge personalization / edge compute（Cloudflare Workers、Lambda@Edge、Akamai EdgeWorkers）。進入這層要評估 edge compute 成本與 cache key 設計複雜度、不是簡單套表決定。</p>
<p>判讀後仍要再對齊 freshness：商品價格在限時活動期間每 5 分鐘改一次，10 分鐘 TTL 就會出現超賣或顯示差價。這類情境要把價格放應用層快取、頁面結構放 CDN，整頁邊緣化會超出 freshness budget。</p>
<h2 id="purge-與-invalidation-的操作模型">Purge 與 Invalidation 的操作模型</h2>
<p>CDN 的 <a href="/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">Cache Invalidation</a> 跟應用層的失效路徑不一樣：應用層 purge 在自家 cluster 內可控，CDN purge 要等全球節點同步。傳統 origin-pull CDN 的全球 purge 需要數秒到數十秒；現代 push-based CDN（Cloudflare、Fastly 等）的 instant purge 在 150ms 級別、語意接近同步、但這條能力依 vendor 而異、要事前驗證。</p>
<p>操作上的三種策略各有適用場景：</p>
<ul>
<li><strong>TTL 自然過期</strong>：適合內容變動慢、不需要立即生效的資源。優點是不依賴 purge API，缺點是無法應對緊急下架。搭配 stale-while-revalidate 後可以兼顧低 origin 壓力與最終新鮮度、是現代 default 而非「弱版本」。</li>
<li><strong>顯式 purge</strong>：適合內容變動時要立刻生效的場景（價格更新、文章下架、合規移除）。要把 purge 列入發布流程，事故期能在分鐘內收回錯誤內容。</li>
<li><strong>版本化路徑</strong>：適合 JS/CSS 等可永久快取的資源。檔名含 hash（<code>app.a3f1b2.js</code>），新版本上線時直接換路徑、舊版本自然失效。這是命中率最高的策略，因為可以設定 <code>max-age=31536000, immutable</code>。</li>
</ul>
<p>這三種策略以 origin pull 模型為主、是基底但不窮盡。現代 CDN 還有兩種重要策略需要展開。</p>
<h3 id="tag-based-purge-的操作模型">Tag-based Purge 的操作模型</h3>
<p><a href="/blog/backend/knowledge-cards/cache-tag-purge/" data-link-title="Cache Tag Purge" data-link-desc="CDN / cache 用 tag / surrogate key 批量失效多個關聯資源">Tag-based / surrogate-key purge</a>（Fastly surrogate key、Cloudflare cache tag、Akamai cache tag）是大型內容系統的事實標準。它解決的核心問題是「一個業務事件需要同時失效多個 URL」——商品下架要同時 purge 商品頁、商品圖、搜尋結果頁中含該商品的快取。</p>
<p>操作流程分三步：</p>
<ol>
<li><strong>打 tag</strong>：origin 在 response header 中標記 tag（如 <code>Surrogate-Key: product-123 category-electronics</code>）。CDN 存快取時同時建立 tag → URL 的反向索引。</li>
<li><strong>按 tag purge</strong>：業務系統發出 <code>PURGE tag=product-123</code> API 呼叫，CDN 用反向索引找出所有帶這個 tag 的快取項目並失效。一次 API 呼叫可能失效數百個 URL。</li>
<li><strong>回源補快取</strong>：被 purge 的 URL 下一次被請求時回源、重新快取。搭配 stale-while-revalidate 可以讓第一個回源請求不阻塞使用者。</li>
</ol>
<p>Tag-based purge 跟顯式 purge（按 URL purge）的本質差異在於「失效單位是業務實體、不是 URL」。按 URL purge 要在業務端維護「一個商品對應哪些 URL」的映射，tag purge 把這個映射交給 CDN 的反向索引。代價是 tag 設計要跟業務模型對齊——tag 太粗（一個 tag 覆蓋太多資源）會過度 purge，tag 太細會退化成按 URL purge。</p>
<p><strong>Push-based instant purge</strong>（Cloudflare、Fastly 規格 &lt;150ms 全球同步）讓全球 purge 從「分鐘級」變成「準同步」。選擇策略時要按 vendor 能力跟資源更新模式組合。</p>
<p>選錯策略的代價會在事故時放大。把限時優惠的價格用「TTL 自然過期」策略佈在 CDN、活動結束後仍有客人看到舊價格繼續下單、客服與退款成本會壓回業務端。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>origin 流量隨使用者線性成長</td>
          <td>cache hit ratio 偏低，邊緣層沒發揮 origin protection</td>
          <td>檢查 cache-control header、命中率分布、coalescing 設定</td>
      </tr>
      <tr>
          <td>edge 命中率忽然下降</td>
          <td>purge 設定誤觸全網、或 cache key 設計過細</td>
          <td>檢查近期 purge 操作、vary 與 query string 設計</td>
      </tr>
      <tr>
          <td>purge 後仍看到舊內容</td>
          <td>全球節點同步延遲、或 CDN 與應用層快取沒對齊</td>
          <td>確認 CDN purge 完成訊號、再追應用層快取狀態</td>
      </tr>
      <tr>
          <td>高峰時 origin 出現 5xx 尖峰</td>
          <td>edge 沒做 stale-if-error，origin 過載直接打回使用者</td>
          <td>啟用 stale-while-revalidate、檢查 origin shield 設定</td>
      </tr>
      <tr>
          <td>部分區域延遲偏高</td>
          <td>區域節點覆蓋不足、或回源走錯區域</td>
          <td>檢查路由策略、加開 edge POP、考慮多 CDN 策略</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>CDN 跟「加速工具」的混淆，會讓 origin protection 跟一致性責任被忽略。多數團隊上線後第一次撞牆，是 KOL 引流或活動高峰把 origin 直接打掛，事後才發現 CDN 只覆蓋了靜態 asset、HTML 與 API 都直接打回 origin。</p>
<p>把 purge 當成同步操作也容易出事。緊急下架觸發 purge 後立刻通知公關「已下線」，但全球節點還沒收斂，仍有區域看到原內容。這類風險要把「purge 已完成」當成可觀測訊號處理，不是 API 回 200 就視為完成。</p>
<p>把 CDN 當成應用層快取替代品則是另一個極端。商品價格、會員等級這類「跟使用者狀態相關」的資料放邊緣層，會在用戶切帳號、優惠變更時暴露其他人的資料或舊狀態，是 <a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">Stale Read</a> 的擴大版。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>CDN 專注「靜態與半靜態內容的網路層分發」。當問題進入動態 API 的延遲、跨服務一致性、寫入路徑保護，責任分別交給 <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>、<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">02 cache aside</a> 與 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 message queue</a> 模組。</p>
<p>跟 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">07 入口治理</a> 的交接：CDN 同時是公網入口，需要承接 WAF、bot mitigation、TLS termination 等資安責任。邊緣層的安全設定不可遺漏，否則 origin 被繞過直接攻擊。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>邊緣分發策略可用以下案例回寫：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13 Hotstar：1800 萬同時觀眾的 IPL 直播</a> — 極端峰值靠多 CDN + origin shield 把 origin 流量壓在容量範圍內。Hotstar 的具體做法是把 hot content（live stream segment）跟 warm content（VOD）分配到不同 CDN provider、利用「edge cache miss 時不是同時打 origin」這條 cache stampede 防禦機制讓 origin 流量曲線跟使用者請求曲線解耦。對照本章「origin protection」段三大策略落地。</li>
<li><a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom：COVID 30 倍突發</a> — 30 倍突發中，登入頁、會議連結頁這類靜態資源由邊緣層吸收絕大部分讀取流量，API 叢集只面對真實的會議建立 / 結束請求。對照本章「Cacheable vs Non-Cacheable 判讀」段：登入頁屬未登入者一致、適合邊緣化；會議內互動屬寫入 API、保持在 origin。</li>
<li><a href="/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/" data-link-title="2.C7 Cloudflare：Cache Reserve 分層儲存快取" data-link-desc="邊緣快取延伸到持久層以降低回源壓力的案例。">2.C7 Cloudflare Cache Reserve 與 Tiered Storage</a> — Cloudflare 在 CDN 內部再分一層 Cache Reserve（持久層）、把 warm 內容從 origin 卸下、避免 edge LRU 淘汰後又回到 origin。對照本章「三層快取」段：邊緣層內部本身也能有 hot / warm 分層、是同一概念的遞迴應用。</li>
</ul>
<p>三個案例依規模從外向內展開：Hotstar 是極端峰值下 origin protection 防禦的天花板測試、Zoom 是把非交易流量（登入 / 連結頁）分流降低 API 叢集壓力的標準應用、Cloudflare Cache Reserve 則展示 CDN vendor 自身把 hot / warm 內容再分層的內部架構。讀者可串著讀理解規模光譜、也可以挑一條深入。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">02 cache aside</a> 的交接：應用層快取與邊緣層的失效路徑要對齊，避免兩層 stale 同時發生。</li>
<li>與 <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> 的交接：edge miss 後流量進到 origin LB，超時與重試設定要協調。</li>
<li>與 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理</a> 的交接：CDN 是公網入口，WAF、TLS 與 bot mitigation 在邊緣層落地。</li>
<li>與 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃</a> 的交接：cache hit ratio 是 origin 容量規劃的核心輸入，命中率假設失準會直接撞牆。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p><strong>規模成長路線下一站 → <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 模組訊息佇列</a></strong>：邊緣層擋住讀流量後、寫流量與事務鏈的下一塊是非同步化。</p>
<p>其他延伸方向：</p>
<ul>
<li>邊緣失效跟應用層失效串成 invalidation pipeline → <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside 與失效策略</a></li>
<li>高峰活動把 CDN 跟排隊機制組合成保護網 → <a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11 高峰事件準備</a></li>
<li>Origin 端的入口流量合約 → <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></li>
</ul>
]]></content:encoded></item><item><title>5.10 Outbound Tunnel 入口與生命週期</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/outbound-tunnel-entry/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/outbound-tunnel-entry/</guid><description>&lt;p>家用主機沒有固定 IP、路由器不想開 port，但手機要能連進來操作 — outbound tunnel 用反向連線解這個入口問題。它跟 load balancer 入口是兩種不同的入口形態：LB 假設 instance 有對外可達位址、流量從外網路由進來;tunnel 由本機進程主動外連到邊緣、把流量沿反向隧道帶回來、路由器零開 port、對公網零入站面。家用服務、個人自架工具、無固定 IP 的環境常用這種入口。&lt;/p>
&lt;h2 id="適用判斷">適用判斷&lt;/h2>
&lt;p>選 outbound tunnel 的前提是「要被外部觸及、但不想暴露公網入口」。典型場景：手機遠端操作自有主機、家庭網路內的服務對外、開發環境臨時對外驗證。服務本身值不值得自建、見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型&lt;/a> 的個人自架工具段;這裡只處理「入口形態選了 tunnel 之後」的部署合約。&lt;/p>
&lt;p>cloudflared（綁 Cloudflare 邊緣與網域）、Tailscale（綁私有網路 / Funnel 對外）、Boundary 各有定位差異，但入口生命週期的判讀框架相同。&lt;/p>
&lt;h2 id="tunnel-contract-組成">tunnel contract 組成&lt;/h2>
&lt;p>tunnel 入口合約跟 &lt;a href="https://tarrragon.github.io/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 contract&lt;/a> 對照、差異集中在連線方向與就緒語意：&lt;/p>
&lt;ol>
&lt;li>connection contract：本機進程主動對邊緣建立並維持反向隧道、無入站 port;隧道斷線的重連策略決定外部可達性的恢復速度。&lt;/li>
&lt;li>readiness contract：對外可達 = 隧道已建立 &lt;strong>且&lt;/strong> 後端服務已可服務。兩個條件任一不成立、外部請求就拿到 502 / 連線中斷。&lt;/li>
&lt;li>ordering contract：啟動順序是後端服務先就緒、tunnel 再宣告 ready;關閉順序相反、tunnel 先收斂停止帶入新流量、後端再退出。&lt;/li>
&lt;li>auth contract：tunnel 只負責把流量帶回來、本身不是認證。隧道網址是位址、不是密碼 — 任何拿到網址的人都可達後端、所以認證必須疊在 tunnel 之後（見下）。&lt;/li>
&lt;/ol>
&lt;h2 id="生命週期與-readiness-對齊">生命週期與 readiness 對齊&lt;/h2>
&lt;p>tunnel 入口的就緒判讀比 LB 多一層。LB 的 health check 打後端 instance、通過代表可接流量;tunnel 場景下、「後端 health check 通過」不等於「外部可達」 — 還要隧道本身連上邊緣。readiness 要同時涵蓋兩者、否則會出現「服務自己覺得健康、外面卻連不進來」的盲區。&lt;/p>
&lt;p>啟動順序錯位的後果具體：tunnel 比後端早 ready、邊緣開始導流量進來、後端還沒起、外部看到一批 502。所以 startup 階段 tunnel 的 ready 訊號要 gate 在後端 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a> 之後。關閉時序則相反、先讓 tunnel 停止帶入新連線、給在途請求收斂窗口、後端再 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown&lt;/a>;這層責任跟 &lt;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 / drain 一致、只是 drain 的對象從 LB 摘流量換成 tunnel 收斂。&lt;/p>
&lt;h2 id="穩態維持與重連策略">穩態維持與重連策略&lt;/h2>
&lt;p>隧道建立後進入穩態：tunnel 進程與邊緣之間維持長連線，邊緣用心跳（keepalive）偵測連線是否存活。心跳間隔與超時由供應商決定（cloudflared 預設每 5 秒心跳、連續失敗觸發重連；Tailscale 由 WireGuard 層的 persistent keepalive 維持 NAT 映射）。穩態下不需要額外操作，但要理解一個語意：邊緣側判定「連線已斷」到本機進程偵測到斷線之間有延遲，這段時間外部請求會 timeout 而非立即拿到錯誤。&lt;/p>
&lt;p>連線中斷後 tunnel 進程自動重連，重連策略的關鍵是 backoff：首次斷線立即重試、連續失敗拉長間隔、避免在邊緣側故障時打滿重連請求。重連成功後 readiness 要重新驗證——隧道恢復不等於後端仍然健康，特別是斷線期間後端可能已經被別的事件影響。&lt;/p>
&lt;h3 id="隧道多連線與冗餘">隧道多連線與冗餘&lt;/h3>
&lt;p>cloudflared 預設對每個 tunnel 建立 4 條連線到不同邊緣節點（Cloudflare 在不同 data center 的 edge server）。單條連線斷線時，流量自動切到其餘連線，外部使用者感受不到中斷。4 條連線全部斷開才會觸發完全不可達。&lt;/p>
&lt;p>Tailscale 的冗餘模型不同：WireGuard tunnel 是點對點連線，沒有多邊緣節點分散。Tailscale 的高可用靠 DERP relay server 做中繼——直連失敗時退到 relay，延遲增加但可達性維持。&lt;/p></description><content:encoded><![CDATA[<p>家用主機沒有固定 IP、路由器不想開 port，但手機要能連進來操作 — outbound tunnel 用反向連線解這個入口問題。它跟 load balancer 入口是兩種不同的入口形態：LB 假設 instance 有對外可達位址、流量從外網路由進來;tunnel 由本機進程主動外連到邊緣、把流量沿反向隧道帶回來、路由器零開 port、對公網零入站面。家用服務、個人自架工具、無固定 IP 的環境常用這種入口。</p>
<h2 id="適用判斷">適用判斷</h2>
<p>選 outbound tunnel 的前提是「要被外部觸及、但不想暴露公網入口」。典型場景：手機遠端操作自有主機、家庭網路內的服務對外、開發環境臨時對外驗證。服務本身值不值得自建、見 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a> 的個人自架工具段;這裡只處理「入口形態選了 tunnel 之後」的部署合約。</p>
<p>cloudflared（綁 Cloudflare 邊緣與網域）、Tailscale（綁私有網路 / Funnel 對外）、Boundary 各有定位差異，但入口生命週期的判讀框架相同。</p>
<h2 id="tunnel-contract-組成">tunnel contract 組成</h2>
<p>tunnel 入口合約跟 <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 contract</a> 對照、差異集中在連線方向與就緒語意：</p>
<ol>
<li>connection contract：本機進程主動對邊緣建立並維持反向隧道、無入站 port;隧道斷線的重連策略決定外部可達性的恢復速度。</li>
<li>readiness contract：對外可達 = 隧道已建立 <strong>且</strong> 後端服務已可服務。兩個條件任一不成立、外部請求就拿到 502 / 連線中斷。</li>
<li>ordering contract：啟動順序是後端服務先就緒、tunnel 再宣告 ready;關閉順序相反、tunnel 先收斂停止帶入新流量、後端再退出。</li>
<li>auth contract：tunnel 只負責把流量帶回來、本身不是認證。隧道網址是位址、不是密碼 — 任何拿到網址的人都可達後端、所以認證必須疊在 tunnel 之後（見下）。</li>
</ol>
<h2 id="生命週期與-readiness-對齊">生命週期與 readiness 對齊</h2>
<p>tunnel 入口的就緒判讀比 LB 多一層。LB 的 health check 打後端 instance、通過代表可接流量;tunnel 場景下、「後端 health check 通過」不等於「外部可達」 — 還要隧道本身連上邊緣。readiness 要同時涵蓋兩者、否則會出現「服務自己覺得健康、外面卻連不進來」的盲區。</p>
<p>啟動順序錯位的後果具體：tunnel 比後端早 ready、邊緣開始導流量進來、後端還沒起、外部看到一批 502。所以 startup 階段 tunnel 的 ready 訊號要 gate 在後端 <a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a> 之後。關閉時序則相反、先讓 tunnel 停止帶入新連線、給在途請求收斂窗口、後端再 <a href="/blog/backend/knowledge-cards/graceful-shutdown/" data-link-title="Graceful Shutdown" data-link-desc="說明服務停止前如何排空流量、完成工作與保存狀態">graceful shutdown</a>;這層責任跟 <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 / drain 一致、只是 drain 的對象從 LB 摘流量換成 tunnel 收斂。</p>
<h2 id="穩態維持與重連策略">穩態維持與重連策略</h2>
<p>隧道建立後進入穩態：tunnel 進程與邊緣之間維持長連線，邊緣用心跳（keepalive）偵測連線是否存活。心跳間隔與超時由供應商決定（cloudflared 預設每 5 秒心跳、連續失敗觸發重連；Tailscale 由 WireGuard 層的 persistent keepalive 維持 NAT 映射）。穩態下不需要額外操作，但要理解一個語意：邊緣側判定「連線已斷」到本機進程偵測到斷線之間有延遲，這段時間外部請求會 timeout 而非立即拿到錯誤。</p>
<p>連線中斷後 tunnel 進程自動重連，重連策略的關鍵是 backoff：首次斷線立即重試、連續失敗拉長間隔、避免在邊緣側故障時打滿重連請求。重連成功後 readiness 要重新驗證——隧道恢復不等於後端仍然健康，特別是斷線期間後端可能已經被別的事件影響。</p>
<h3 id="隧道多連線與冗餘">隧道多連線與冗餘</h3>
<p>cloudflared 預設對每個 tunnel 建立 4 條連線到不同邊緣節點（Cloudflare 在不同 data center 的 edge server）。單條連線斷線時，流量自動切到其餘連線，外部使用者感受不到中斷。4 條連線全部斷開才會觸發完全不可達。</p>
<p>Tailscale 的冗餘模型不同：WireGuard tunnel 是點對點連線，沒有多邊緣節點分散。Tailscale 的高可用靠 DERP relay server 做中繼——直連失敗時退到 relay，延遲增加但可達性維持。</p>
<p>這個差異在穩定性預期上很重要：cloudflared 的可達性依賴 Cloudflare 邊緣網路的多點冗餘，Tailscale 的可達性依賴直連品質與 DERP 中繼。選擇時要問「我的網路環境是否穩定到不需要多連線冗餘」。</p>
<h2 id="故障模式network-層與-application-層的分離">故障模式：network 層與 application 層的分離</h2>
<p>tunnel 斷線跟 LB health check 失敗是不同層的故障。LB health check 失敗多半是 application 層（後端掛了、依賴不通）；tunnel 斷線常是 network 層（邊緣連線中斷、本機外連受阻、供應商側問題）、而後端服務本身完全健康。事故判讀要先分清這兩層：後端 log 一切正常、但外部全部連不進來、第一個要看的是 tunnel 進程的連線狀態、不是後端。</p>
<p>這也改變監控訊號的設計。LB 場景看後端 5xx 與 latency 就能覆蓋多數入口問題；tunnel 場景要額外監控隧道本身的連線狀態與重連次數——隧道靜默斷掉時、後端指標一片祥和、唯一的訊號在 tunnel 進程那邊。</p>
<h3 id="故障分類與判讀順序">故障分類與判讀順序</h3>
<p>tunnel 環境下的故障可按層級分類，判讀順序從外到內：</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>症狀</th>
          <th>判讀第一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>供應商邊緣</td>
          <td>所有 tunnel 用戶同時受影響</td>
          <td>查供應商 status page</td>
      </tr>
      <tr>
          <td>本機外連</td>
          <td>單一 tunnel 斷線、其他外連也有問題</td>
          <td>查本機網路、NAT、防火牆</td>
      </tr>
      <tr>
          <td>tunnel 進程</td>
          <td>tunnel 進程 crash 或 hang</td>
          <td>查 tunnel 進程 log 與 restart 狀態</td>
      </tr>
      <tr>
          <td>後端服務</td>
          <td>tunnel 正常但外部拿到 502</td>
          <td>查後端服務 readiness</td>
      </tr>
      <tr>
          <td>認證閘道</td>
          <td>tunnel + 後端正常但外部拿到 403</td>
          <td>查認證設定（token / ACL 過期）</td>
      </tr>
  </tbody>
</table>
<p>判讀順序的重點是「先確認 tunnel 層是否正常、再往內看」。如果跳過 tunnel 層直接排查後端，會在後端 log 一切正常的情況下浪費時間。</p>
<h2 id="認證必須疊在-tunnel-之後">認證必須疊在 tunnel 之後</h2>
<p>tunnel 把後端的可達性開到了外部、但它不認證。隧道網址可能從瀏覽器紀錄、分享連結、Referer 外洩、不該被當成安全機制。所以 tunnel 之後必須疊認證閘道、且預設拒絕 — 未通過認證的流量不該觸及後端。</p>
<p>常見的疊法是邊緣與本機各一層：邊緣層（cloudflared 配 Cloudflare Access service token、Tailscale 配 ACL）讓未授權流量在邊緣就被擋、根本到不了本機;本機層（反向代理驗共享密鑰 / basic auth）作為邊緣萬一失效的縱深。入口威脅建模見 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a>;單人自用工具的裝置綁定認證見 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/#%e5%96%ae%e4%ba%ba%e8%a3%9d%e7%bd%ae%e8%aa%8d%e8%ad%89%e6%a8%a1%e5%9e%8b" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 單人裝置認證模型</a>。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>外部全部連不進來、後端 log 正常</td>
          <td>故障在 network 層、隧道斷線</td>
          <td>先查 tunnel 進程連線狀態、不是後端</td>
      </tr>
      <tr>
          <td>啟動後短時間外部拿到一批 502</td>
          <td>tunnel 比後端早 ready、導流量進空服務</td>
          <td>把 tunnel ready gate 在後端 readiness 後</td>
      </tr>
      <tr>
          <td>隧道頻繁重連、外部間歇中斷</td>
          <td>本機外連不穩或邊緣側抖動</td>
          <td>查 cloudflared / tailscaled 的重連 log、確認 backoff 間隔是否正常拉長</td>
      </tr>
      <tr>
          <td>拿到網址的人直接連到後端</td>
          <td>認證沒疊在 tunnel 之後、網址被當密碼</td>
          <td>補邊緣 / 本機認證閘道、預設拒絕</td>
      </tr>
      <tr>
          <td>部署切換隧道時對外中斷拉長</td>
          <td>關閉順序錯位、tunnel 未先收斂</td>
          <td>先停 tunnel 帶入新連線、再退後端</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 tunnel 網址當密碼、是最常見也最危險的誤判。網址不好猜不代表是祕密、它會從各種地方外洩、認證要靠 tunnel 之後的閘道、不是靠網址難猜。</p>
<p>把「後端健康」當成「外部可達」、忽略隧道本身是獨立的失效點。tunnel 場景的可達性是後端健康與隧道連線的交集、監控要覆蓋兩者。</p>
<p>把 tunnel 當「永久掛著」的常駐入口、放大暴露窗。自用場景常更適合用時起、用完關 — 暴露窗壓到最小;要常駐時、認證閘道與監控的投資等級要隨之上調。</p>
<p>把 tunnel 供應商視為零停機、不設本機降級預案。tunnel 依賴外部供應商的邊緣網路與協調伺服器，供應商事故期間本機服務完全健康但外部無法觸及。有降級需求的場景要準備替代入口路徑（如臨時開 port + 反向代理），或接受供應商 SLA 決定自身可用性。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <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> 的交接：tunnel 的 startup / readiness / drain 對齊生命週期合約、只是 drain 對象換成隧道收斂。</li>
<li>與 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a> 的交接：tunnel 作為對外入口的威脅建模與認證疊法。</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> 的交接：tunnel 憑證與認證閘道密鑰的保管與輪替。</li>
<li>與 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">4 觀測</a> 的交接：隧道連線狀態與重連次數要進監控、否則 network 層故障無訊號。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把 tunnel 入口放進整體生命週期、接著讀 <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>。要把 tunnel 之後的認證做紮實、接著讀 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a> 與 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">7.2 身分與授權邊界</a>。判斷服務是否屬於個人自架工具形態、回 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a>。</p>
]]></content:encoded></item></channel></rss>