<?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>Microservice on Tarragon</title><link>https://tarrragon.github.io/blog/tags/microservice/</link><description>Recent content in Microservice on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 27 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/microservice/index.xml" rel="self" type="application/rss+xml"/><item><title>10.1 服務拆分與邊界判讀</title><link>https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-boundaries/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-boundaries/</guid><description>&lt;p>Monolith 與 microservice 是兩種耦合策略、各自承擔代價：monolith 用單一程式碼庫換低協作成本、microservice 用獨立邊界換團隊與部署彈性。本章處理「演進速度跟組織能力對齊」這個決策邊界 — 起點是辨識當下壓力來源、再選擇拆分軸、流行度與堅持習慣都是次要訊號。&lt;/p>
&lt;h2 id="monolith-與-microservice-的責任差異">Monolith 與 Microservice 的責任差異&lt;/h2>
&lt;p>Monolith 用「同一個程式碼庫、同一個部署單位、同一個資料庫」換取低協作成本與簡單事務語意。Microservice 用「獨立程式碼庫、獨立部署、獨立資料邊界」換取團隊獨立性、技術選型彈性與局部故障隔離。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Monolith&lt;/th>
 &lt;th>Microservice&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>變更速度&lt;/td>
 &lt;td>單庫改完直接上線&lt;/td>
 &lt;td>跨服務協調，需要契約對齊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事務一致性&lt;/td>
 &lt;td>本地 transaction 就解決&lt;/td>
 &lt;td>跨服務需要 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/saga/" data-link-title="Saga" data-link-desc="處理跨服務分散事務的補償型 transaction 序列、用最終一致換 ACID atomic">saga&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox&lt;/a> 或最終一致性&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>N 組基礎設施 + 服務間通訊監控&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Debug 路徑&lt;/td>
 &lt;td>同一個 stack trace 看到底&lt;/td>
 &lt;td>跨服務 trace context、log 聚合不可省&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>適合規模&lt;/td>
 &lt;td>早期、單一團隊、業務尚未分化&lt;/td>
 &lt;td>多團隊、業務已分化、可獨立演進&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>讀者要從這張表反推自己的真實壓力來源。如果痛點是「部署互相卡住、發布頻率被別人拖慢」，拆分能解決；如果痛點是「程式碼太亂、新人看不懂」，拆服務只會把亂的範圍擴大成跨服務契約混亂。&lt;/p>
&lt;p>這張表是兩端對比、實際系統常落在中間。常見折衷形態：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/modular-monolith/" data-link-title="Modular Monolith" data-link-desc="單一部署單位 &amp;#43; 模組化內部邊界的架構、是 monolith 跟 microservice 之間的折衷形態">Modular monolith&lt;/a>&lt;/strong>（單一部署 + 模組化邊界）：保留 monolith 的部署簡單、用模組邊界防止程式碼互相穿透。Shopify、Basecamp、Stack Overflow 是大規模長期維持的代表 — monolith 不是進化中段、是 valid endgame。&lt;/li>
&lt;li>&lt;strong>Macro-services&lt;/strong>（少量大服務、5-15 個）：避免 microservice 的極端碎片化、保留拆分帶來的部署獨立性。是多數中型團隊的實際終點、不是過渡形態。&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cell-based-architecture/" data-link-title="Cell-Based Architecture" data-link-desc="把系統拆成多個 isolated cell、控制 blast radius、跨 cell 共用標準介面">Cell-based architecture&lt;/a>&lt;/strong>（多 cell 各自獨立、跨 cell 共用標準介面）：AWS、Slack、DoorDash 用來控制 blast radius — 把整個系統複製成多個 isolated cell、每個 cell 內可以是 monolith 或 microservice。&lt;/li>
&lt;/ul>
&lt;p>拆分不是進化方向、是壓力應對工具。維持 monolith 在某些情境（極小團隊、PMF 前期、無 DevOps 能力）是更負責任的選擇。&lt;/p>
&lt;h2 id="拆分軸的判讀">拆分軸的判讀&lt;/h2>
&lt;p>服務邊界不只一條軸。常見的四條軸對應不同的壓力來源，正確的拆法是「壓力在哪裡、就沿那條軸拆」，不是同時動四條軸。&lt;/p>
&lt;h3 id="資料邊界">資料邊界&lt;/h3>
&lt;p>當兩塊業務的資料&lt;strong>生命週期不同、一致性需求不同、查詢模式不同&lt;/strong>時，資料邊界已經形成。例如訂單資料需要強一致性與長期保留，瀏覽紀錄可以最終一致性、定期清理。把這兩類資料放同一個 schema 會讓 backup、migration、index 策略互相干擾。&lt;/p>
&lt;p>判讀訊號：同一張表上不同欄位的 read/write QPS 差三個數量級、同一個 transaction 同時寫入多種業務概念、schema migration 一動就要鎖住整個業務的寫入。&lt;/p>
&lt;h3 id="團隊邊界">團隊邊界&lt;/h3>
&lt;p>當兩塊業務由不同團隊維護、發布節奏不同、技術棧偏好不同時，團隊邊界已經形成。Conway&amp;rsquo;s Law 反過來操作：用服務邊界保護團隊邊界，避免一隊改動觸發另一隊重 review。&lt;/p>
&lt;p>判讀訊號：PR review 跨團隊比例過半、發版需要協調多個團隊、技術升級（語言版本、framework 升級）因為其他團隊未準備好而被擋住。&lt;/p>
&lt;h3 id="部署邊界">部署邊界&lt;/h3>
&lt;p>當部分功能需要&lt;strong>獨立的部署節奏、獨立的擴展策略、獨立的可用性等級&lt;/strong>時，部署邊界已經形成。背景批次工作要按小時排程、API 服務要 7×24 線上、報表服務只在工作日運行，三者放同一個部署單位會讓最嚴格的可用性要求拖累其他。&lt;/p>
&lt;p>判讀訊號：高峰時某個功能擴展速度跟不上、低峰時某個功能浪費資源、單一發版策略覆蓋不了所有功能的風險等級。&lt;/p>
&lt;h3 id="流量邊界">流量邊界&lt;/h3>
&lt;p>當不同功能的&lt;strong>流量形狀、失敗代價、SLO 等級不同&lt;/strong>時，流量邊界已經形成。付款 API 一秒 100 個請求、商品搜尋一秒 10000 個請求、後台報表一天 100 個請求，三者放同一個服務會讓彼此爭資源，付款被搜尋擠掉是業務災難。&lt;/p>
&lt;p>判讀訊號：高頻 endpoint 壓爆低頻 endpoint 共用的連線池、不同 endpoint 的 latency 分布同時惡化、無法針對核心交易設定獨立的 SLO 跟 alert。&lt;/p>
&lt;h3 id="其他常見拆分軸">其他常見拆分軸&lt;/h3>
&lt;p>上面四條是技術驅動的主要拆分軸。實務上還有其他軸常成為真實驅動力、要一併納入判讀：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>失敗代價 / blast radius 軸&lt;/strong>：核心交易（掛了會有業務災難）跟邊緣推薦（掛了沒人在意）的可用性等級差距大、適合拆開降低 blast radius。跟 SLO 軸高相關但不同 — 重點在「失敗時誰受影響」的範圍隔離。&lt;/li>
&lt;li>&lt;strong>變更頻率 / 風險軸&lt;/strong>：high-velocity 實驗功能跟 stable 核心應拆開、降低實驗對核心穩定性的牽連。跟團隊軸高相關但獨立 — 同一團隊也可能維持兩種變更頻率的程式碼。&lt;/li>
&lt;li>&lt;strong>資料敏感度 / 合規邊界&lt;/strong>：PCI / PII / 醫療資料的隔離常是合規硬要求（GDPR data residency 強制資料拆境），不是技術選擇。這類軸跟資料邊界相關但服從不同壓力。&lt;/li>
&lt;li>&lt;strong>組織非技術約束&lt;/strong>：併購整合、外部合規節奏、團隊 reorg、預算切分都會強制拆分 — 比 metric 訊號更早觸發、技術上不一定最佳但無法繞過。&lt;/li>
&lt;/ul>
&lt;p>這些軸跟前四條可以同時生效、也可能彼此衝突（合規逼資料拆境、但流量軸建議聚合）。處理衝突時優先順序通常是「合規 &amp;gt; 失敗代價 &amp;gt; 部署 / 流量 &amp;gt; 團隊 &amp;gt; 資料 &amp;gt; 變更頻率」、但每個組織會有自己的權重。&lt;/p></description><content:encoded><![CDATA[<p>Monolith 與 microservice 是兩種耦合策略、各自承擔代價：monolith 用單一程式碼庫換低協作成本、microservice 用獨立邊界換團隊與部署彈性。本章處理「演進速度跟組織能力對齊」這個決策邊界 — 起點是辨識當下壓力來源、再選擇拆分軸、流行度與堅持習慣都是次要訊號。</p>
<h2 id="monolith-與-microservice-的責任差異">Monolith 與 Microservice 的責任差異</h2>
<p>Monolith 用「同一個程式碼庫、同一個部署單位、同一個資料庫」換取低協作成本與簡單事務語意。Microservice 用「獨立程式碼庫、獨立部署、獨立資料邊界」換取團隊獨立性、技術選型彈性與局部故障隔離。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Monolith</th>
          <th>Microservice</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>變更速度</td>
          <td>單庫改完直接上線</td>
          <td>跨服務協調，需要契約對齊</td>
      </tr>
      <tr>
          <td>事務一致性</td>
          <td>本地 transaction 就解決</td>
          <td>跨服務需要 <a href="/blog/backend/knowledge-cards/saga/" data-link-title="Saga" data-link-desc="處理跨服務分散事務的補償型 transaction 序列、用最終一致換 ACID atomic">saga</a>、<a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox</a> 或最終一致性</td>
      </tr>
      <tr>
          <td>故障隔離</td>
          <td>單點失敗會整個服務掛掉</td>
          <td>一個服務掛了，其他可能還能服務</td>
      </tr>
      <tr>
          <td>部署單位</td>
          <td>整個應用一次部署</td>
          <td>各服務獨立部署，發布節奏不互相阻擋</td>
      </tr>
      <tr>
          <td>運維複雜度</td>
          <td>一組基礎設施</td>
          <td>N 組基礎設施 + 服務間通訊監控</td>
      </tr>
      <tr>
          <td>Debug 路徑</td>
          <td>同一個 stack trace 看到底</td>
          <td>跨服務 trace context、log 聚合不可省</td>
      </tr>
      <tr>
          <td>適合規模</td>
          <td>早期、單一團隊、業務尚未分化</td>
          <td>多團隊、業務已分化、可獨立演進</td>
      </tr>
  </tbody>
</table>
<p>讀者要從這張表反推自己的真實壓力來源。如果痛點是「部署互相卡住、發布頻率被別人拖慢」，拆分能解決；如果痛點是「程式碼太亂、新人看不懂」，拆服務只會把亂的範圍擴大成跨服務契約混亂。</p>
<p>這張表是兩端對比、實際系統常落在中間。常見折衷形態：</p>
<ul>
<li><strong><a href="/blog/backend/knowledge-cards/modular-monolith/" data-link-title="Modular Monolith" data-link-desc="單一部署單位 &#43; 模組化內部邊界的架構、是 monolith 跟 microservice 之間的折衷形態">Modular monolith</a></strong>（單一部署 + 模組化邊界）：保留 monolith 的部署簡單、用模組邊界防止程式碼互相穿透。Shopify、Basecamp、Stack Overflow 是大規模長期維持的代表 — monolith 不是進化中段、是 valid endgame。</li>
<li><strong>Macro-services</strong>（少量大服務、5-15 個）：避免 microservice 的極端碎片化、保留拆分帶來的部署獨立性。是多數中型團隊的實際終點、不是過渡形態。</li>
<li><strong><a href="/blog/backend/knowledge-cards/cell-based-architecture/" data-link-title="Cell-Based Architecture" data-link-desc="把系統拆成多個 isolated cell、控制 blast radius、跨 cell 共用標準介面">Cell-based architecture</a></strong>（多 cell 各自獨立、跨 cell 共用標準介面）：AWS、Slack、DoorDash 用來控制 blast radius — 把整個系統複製成多個 isolated cell、每個 cell 內可以是 monolith 或 microservice。</li>
</ul>
<p>拆分不是進化方向、是壓力應對工具。維持 monolith 在某些情境（極小團隊、PMF 前期、無 DevOps 能力）是更負責任的選擇。</p>
<h2 id="拆分軸的判讀">拆分軸的判讀</h2>
<p>服務邊界不只一條軸。常見的四條軸對應不同的壓力來源，正確的拆法是「壓力在哪裡、就沿那條軸拆」，不是同時動四條軸。</p>
<h3 id="資料邊界">資料邊界</h3>
<p>當兩塊業務的資料<strong>生命週期不同、一致性需求不同、查詢模式不同</strong>時，資料邊界已經形成。例如訂單資料需要強一致性與長期保留，瀏覽紀錄可以最終一致性、定期清理。把這兩類資料放同一個 schema 會讓 backup、migration、index 策略互相干擾。</p>
<p>判讀訊號：同一張表上不同欄位的 read/write QPS 差三個數量級、同一個 transaction 同時寫入多種業務概念、schema migration 一動就要鎖住整個業務的寫入。</p>
<h3 id="團隊邊界">團隊邊界</h3>
<p>當兩塊業務由不同團隊維護、發布節奏不同、技術棧偏好不同時，團隊邊界已經形成。Conway&rsquo;s Law 反過來操作：用服務邊界保護團隊邊界，避免一隊改動觸發另一隊重 review。</p>
<p>判讀訊號：PR review 跨團隊比例過半、發版需要協調多個團隊、技術升級（語言版本、framework 升級）因為其他團隊未準備好而被擋住。</p>
<h3 id="部署邊界">部署邊界</h3>
<p>當部分功能需要<strong>獨立的部署節奏、獨立的擴展策略、獨立的可用性等級</strong>時，部署邊界已經形成。背景批次工作要按小時排程、API 服務要 7×24 線上、報表服務只在工作日運行，三者放同一個部署單位會讓最嚴格的可用性要求拖累其他。</p>
<p>判讀訊號：高峰時某個功能擴展速度跟不上、低峰時某個功能浪費資源、單一發版策略覆蓋不了所有功能的風險等級。</p>
<h3 id="流量邊界">流量邊界</h3>
<p>當不同功能的<strong>流量形狀、失敗代價、SLO 等級不同</strong>時，流量邊界已經形成。付款 API 一秒 100 個請求、商品搜尋一秒 10000 個請求、後台報表一天 100 個請求，三者放同一個服務會讓彼此爭資源，付款被搜尋擠掉是業務災難。</p>
<p>判讀訊號：高頻 endpoint 壓爆低頻 endpoint 共用的連線池、不同 endpoint 的 latency 分布同時惡化、無法針對核心交易設定獨立的 SLO 跟 alert。</p>
<h3 id="其他常見拆分軸">其他常見拆分軸</h3>
<p>上面四條是技術驅動的主要拆分軸。實務上還有其他軸常成為真實驅動力、要一併納入判讀：</p>
<ul>
<li><strong>失敗代價 / blast radius 軸</strong>：核心交易（掛了會有業務災難）跟邊緣推薦（掛了沒人在意）的可用性等級差距大、適合拆開降低 blast radius。跟 SLO 軸高相關但不同 — 重點在「失敗時誰受影響」的範圍隔離。</li>
<li><strong>變更頻率 / 風險軸</strong>：high-velocity 實驗功能跟 stable 核心應拆開、降低實驗對核心穩定性的牽連。跟團隊軸高相關但獨立 — 同一團隊也可能維持兩種變更頻率的程式碼。</li>
<li><strong>資料敏感度 / 合規邊界</strong>：PCI / PII / 醫療資料的隔離常是合規硬要求（GDPR data residency 強制資料拆境），不是技術選擇。這類軸跟資料邊界相關但服從不同壓力。</li>
<li><strong>組織非技術約束</strong>：併購整合、外部合規節奏、團隊 reorg、預算切分都會強制拆分 — 比 metric 訊號更早觸發、技術上不一定最佳但無法繞過。</li>
</ul>
<p>這些軸跟前四條可以同時生效、也可能彼此衝突（合規逼資料拆境、但流量軸建議聚合）。處理衝突時優先順序通常是「合規 &gt; 失敗代價 &gt; 部署 / 流量 &gt; 團隊 &gt; 資料 &gt; 變更頻率」、但每個組織會有自己的權重。</p>
<h2 id="拆分時機的判讀">拆分時機的判讀</h2>
<p>拆分時機不能等到「已經痛到動不了」才開始，那時候拆分要付的代價最高。也不能在「還沒長出邊界」時提早拆，那會用 microservice 的協調成本懲罰一個還沒到規模的系統。</p>
<p>提早訊號（可以開始準備但不一定立刻動手）：</p>
<ul>
<li>程式碼裡同一份邏輯被三個 PR 同時修改、merge conflict 增加</li>
<li>同一個 service 的不同功能開始有不同的擴展需求</li>
<li>不同團隊對同一個發版視窗的需求開始衝突</li>
</ul>
<p>該動手訊號（再拖就要付高昂代價）：</p>
<ul>
<li>任何一個功能改動需要 freeze 整個服務發版</li>
<li>局部高峰擴展時整個服務一起擴展，成本翻倍</li>
<li>一個團隊的事故會直接影響另一個團隊的營運指標</li>
<li>跨團隊 deadlock：A 等 B 改完才能上、B 等 A 改完才能上</li>
</ul>
<p>過晚訊號（拆分要付遷移代價）：</p>
<ul>
<li>已經出現過跨團隊事故、且復盤結論是「無法分離責任」</li>
<li>DB 連線池在多個業務間爭搶、無法用 connection pool 隔離解決</li>
<li>部署平台跑不動：CI 太慢、build 太大、本地開發無法啟動完整環境</li>
</ul>
<h2 id="拆分代價與回退路徑">拆分代價與回退路徑</h2>
<p>拆分不是免費操作。每多一個服務，就多一份運維成本、跨服務 trace 成本、契約治理成本。讀者要在拆分前認知這些代價，而不是事後才發現。</p>
<table>
  <thead>
      <tr>
          <th>代價類型</th>
          <th>具體表現</th>
          <th>緩解方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>分散事務</td>
          <td>一筆業務動作跨多個服務、需要 saga 或最終一致性</td>
          <td><a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 message queue</a> 的 outbox、<a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a></td>
      </tr>
      <tr>
          <td>運維複雜度</td>
          <td>N 個服務 × M 個環境 × K 個版本，組合爆炸</td>
          <td>收斂部署平台、用 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 K8s 部署策略</a> 統一管理</td>
      </tr>
      <tr>
          <td>跨服務 debug</td>
          <td>一個請求跨多個服務、不知道在哪一段失敗</td>
          <td><a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">04 trace context</a>、結構化 log 聚合</td>
      </tr>
      <tr>
          <td>契約治理</td>
          <td>服務 A 的 API 改動會影響服務 B、C、D</td>
          <td><a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract test</a>、版本化 API</td>
      </tr>
      <tr>
          <td>資料一致性</td>
          <td>各服務 DB 獨立，跨服務查詢需要 join 或 read model</td>
          <td>CQRS、event-driven projection、reconciliation</td>
      </tr>
  </tbody>
</table>
<p>拆分失敗的回退路徑要在拆分前設計好。常見回退策略：保留原 monolith 程式碼一段時間（雙寫期），新服務出問題可以切回；先拆<strong>讀路徑</strong>驗證流量，再拆寫路徑；用 feature flag 控制是否走新服務。沒有回退路徑的拆分一旦撞牆，會比不拆更難收拾。</p>
<h3 id="拆分後的通訊優先級事件--同步-rpc">拆分後的通訊優先級：事件 &gt; 同步 RPC</h3>
<p>拆完後跨服務通訊有兩條路：同步 RPC（gRPC、REST）跟異步事件（<a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">message queue</a>、event bus）。預設應該選事件、保留 RPC 給「真的需要同步回應的查詢」。</p>
<p>理由：</p>
<ul>
<li><strong>失敗代價隔離</strong>：服務 A 發事件給 B、B 掛了不影響 A — 事件留在 queue 等。同步 RPC 下、B 掛了 A 也跟著掛</li>
<li><strong>流量解耦</strong>：事件本身就是 buffer、能吸收 burst。同步 RPC 是 throughput 的硬上限、A 的尖峰 = B 的尖峰</li>
<li><strong>可重放</strong>：事件可以重放（replay）做資料修補、debug、新服務 backfill。同步 RPC 過了就過了</li>
<li><strong>服務獨立演進</strong>：事件 schema 可以加欄位向下相容、consumer 慢慢 adapt。RPC interface 改動是 breaking change</li>
</ul>
<p>該用同步 RPC 的少數場景：使用者請求路徑需要立即回應（「使用者按下查詢、顯示結果」）、且兩個服務都在同一個 latency budget 內。其他都優先事件。</p>
<p>詳見 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 模組訊息佇列</a> 跟 <a href="/blog/backend/00-service-selection/async-delivery-selection/" data-link-title="0.3 非同步與事件傳遞選型" data-link-desc="區分背景工作、durable queue、stream、pub/sub 與 outbox 的選型邊界">0.3 非同步與事件傳遞選型</a>。</p>
<h2 id="反例拆分過度的收回">反例：拆分過度的收回</h2>
<p>服務拆分的反向動作是合併。當拆分後發現「服務間呼叫太頻繁、近乎同步、跨服務事務太多」時，代表這條邊界拆錯了。處理方式是把這兩個服務合回去，繼續增加跨服務工具只會堆疊複雜度。</p>
<p>判讀「該合併」的訊號：服務 A 與 B 之間每秒幾百次同步呼叫且失敗會連鎖、A 改動必定觸發 B 改動且兩者由同一團隊維護、跨服務事務佔總業務動作比例過高、跨服務 latency 是 SLO 主要消耗者。</p>
<p>合併不是失敗。它代表團隊已經理解這條邊界不該存在，及時收回比硬撐更負責任。Modular monolith（單一部署、模組化邊界）是常見的折衷形態：保留模組邊界、避免分散事務代價、未來壓力出現時再分拆。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多團隊發版互相阻擋</td>
          <td>部署邊界已形成、但服務仍綁在一起</td>
          <td>從 CI/部署單位開始拆，先讓發布獨立</td>
      </tr>
      <tr>
          <td>同一服務不同功能擴展需求差距大</td>
          <td>流量邊界已形成</td>
          <td>沿流量軸拆，高頻 endpoint 獨立服務 + 獨立 auto scaling</td>
      </tr>
      <tr>
          <td>DB 寫入鎖跨業務互相影響</td>
          <td>資料邊界已形成</td>
          <td>沿資料軸拆，獨立 schema 與獨立 DB instance</td>
      </tr>
      <tr>
          <td>拆分後跨服務同步呼叫激增</td>
          <td>邊界拆錯、實際耦合並未被服務界線解開</td>
          <td>評估合併、或改用事件驅動把同步呼叫變成非同步交接</td>
      </tr>
      <tr>
          <td>拆分後事故 MTTR 拉長</td>
          <td>跨服務觀測能力跟不上</td>
          <td>補 <a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">04 trace context</a> 與 service topology</td>
      </tr>
      <tr>
          <td>拆分後 dev velocity 反而下降</td>
          <td>契約治理跟跨服務協作成本超過拆分收益</td>
          <td>評估合併或建立 shared kernel</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把「technical debt」當成拆分理由。Monolith 程式碼髒亂的解法是重構，不是拆服務。拆服務只是把髒亂從單庫變成跨服務契約混亂，問題並沒有消失。</p>
<p>把「跟風 microservice」當成決策。沒有業務壓力、團隊規模不到位、運維能力不夠的情況下拆服務，新的協作成本會壓垮整個團隊，這比 monolith 的痛苦更大。</p>
<p>把拆分當成單向操作。沒有設計回退路徑、沒有保留合併選項，拆錯了就只能硬撐。成熟的服務演進策略要把「拆」跟「合」當成雙向可逆操作。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>本章專注「該不該拆、沿哪條軸拆、拆完怎麼收尾」。當問題進入具體拆分後的部署、流量、觀測責任，分別交給以下模組：</p>
<ul>
<li>服務獨立部署 → <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 deployment platform</a></li>
<li>跨服務交接與事件 → <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 message queue</a></li>
<li>跨服務觀測與 trace → <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a></li>
<li>跨服務一致性與冪等性 → <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 idempotency-replay</a> + outbox pattern</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<p>服務拆分判讀可用以下案例回寫：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 +75%、成本 -28%</a> — 反例方向：原本各 microservice 各自 DB 造成運維碎片化、最後做 consolidation；對照本章「拆分過度的收回」段。</li>
<li><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> — Condé Nast 把多 brand 各自的 K8s cluster 整併到統一 EKS 控制面、降低跨團隊運維分歧。對照本章「拆分代價 / 運維複雜度」段：拆出去快、合回來慢、設計時就要評估這種非對稱性。</li>
<li><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> — Riot 的拆分軸是「遊戲 × 地區 × 環境」三維交集、246 個 cluster 是這三軸的笛卡兒積取一個 subset。對照本章「拆分軸 / 部署邊界」段：實務上的拆分常常是多軸交集、不是單軸推進。</li>
</ul>
<p>Netflix Aurora consolidation 是反例最有教學價值的一筆 — 它證明「拆 microservice 各自 DB → consolidation 回 Aurora」是 valid endgame、拆服務不是單向操作。Condé Nast 跟 Riot Games 補充另兩條維度：碎片化的運維代價、多軸交集的設計複雜度。把這三筆放回「拆分時機判讀」框架的不同節點上、能看出拆分決策的本質是「沿哪幾條軸 + 接受哪些代價」的組合。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/00-service-selection/service-capability-map/" data-link-title="0.1 後端服務能力地圖" data-link-desc="用需求類型判斷應先評估資料庫、快取、訊息佇列、觀測平台或部署平台">0.1 後端服務能力地圖</a> 的交接：拆分前要先理解每塊責任屬於哪種能力分類，避免拆出語意混亂的服務。</li>
<li>與 <a href="/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5 流量與資料量評估</a> 的交接：流量軸拆分要先有流量基線。</li>
<li>與 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 message queue</a> 的交接：拆分後跨服務通訊優先用事件、不是同步 RPC。</li>
<li>與 <a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸</a> 的交接：拆分常常是水平擴展的前提（無狀態服務拆分後才能獨立水平擴展）。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p><strong>規模成長路線下一站 → <a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸與 Stateless 前提</a></strong>：拆分後接著要為每個服務選擇擴展軸。</p>
<p>其他延伸方向：</p>
<ul>
<li>實作層：服務如何獨立部署 → <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></li>
<li>事件層：拆分後跨服務通訊設計 → <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 模組訊息佇列</a></li>
</ul>
]]></content:encoded></item><item><title>10.2 服務拆分執行 Runbook（Strangler Fig / 雙寫期 / 切流 / 回退）</title><link>https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-execution-runbook/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-execution-runbook/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分與邊界判讀&lt;/a> 處理「該不該拆」、本章處理「決定拆之後實際怎麼動手」。拆服務是漸進演進的過程、一次性大爆炸（big bang）的成功率極低。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/strangler-fig/" data-link-title="Strangler Fig Pattern" data-link-desc="服務拆分 / 系統替換的漸進演進模式、用『新舊共存 &amp;#43; 逐步遷移 &amp;#43; 最終下架』取代 big bang 重寫">Strangler Fig pattern&lt;/a> 是這層的工程基底 — 用「新功能在新服務、舊功能慢慢搬」的方式、把整個 monolith 包圍、逐步替換。&lt;/p>
&lt;h2 id="strangler-fig-pattern-的工程含義">Strangler Fig Pattern 的工程含義&lt;/h2>
&lt;p>Strangler Fig（絞殺榕）是 Martin Fowler 對漸進拆分的命名比喻：榕樹依附在宿主樹上、慢慢長大、最終取代宿主。應用到服務拆分：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>舊系統繼續運作&lt;/strong>：拆分過程中、monolith 仍是 source of truth、新服務從旁長出&lt;/li>
&lt;li>&lt;strong>流量逐步遷移&lt;/strong>：用 routing layer（API gateway、proxy、feature flag）控制哪些 request 走新服務、哪些走舊&lt;/li>
&lt;li>&lt;strong>驗證 → 擴大&lt;/strong>：每個遷移的功能先小流量驗證、確認新舊一致後再加流量比例&lt;/li>
&lt;li>&lt;strong>舊系統最終下架&lt;/strong>：當所有功能都遷出後、monolith 才被退役&lt;/li>
&lt;/ul>
&lt;p>Strangler Fig 跟 big bang 拆分的本質差異是「失敗代價可控」— 大爆炸拆分失敗就整個服務掛、Strangler 拆分失敗只影響該功能、且可即時切回 monolith。&lt;/p>
&lt;h2 id="拆分執行階段">拆分執行階段&lt;/h2>
&lt;p>把 Strangler 細化成可操作的四階段：&lt;/p>
&lt;h3 id="階段-1邊界冷凍--adapter-抽出">階段 1：邊界冷凍 + Adapter 抽出&lt;/h3>
&lt;p>動手拆之前、先在 monolith 內部把「將要拆出去」的功能用 adapter / interface 封起來。所有外部呼叫該功能都走 adapter、不直接呼叫實作。&lt;/p>
&lt;p>這層動作的責任：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>強制 dependency 清楚&lt;/strong>：哪些功能依賴它、哪些功能被它依賴、必須變成顯式 interface 而非分散在 codebase&lt;/li>
&lt;li>&lt;strong>資料邊界明示&lt;/strong>：該功能用到哪些 table / column、用 repository / DAO 封裝、不讓其他功能直接 access&lt;/li>
&lt;li>&lt;strong>變更頻率冷凍&lt;/strong>：拆分期間原則上不接受該功能的新需求、避免「拆到一半新需求又進來」&lt;/li>
&lt;/ul>
&lt;p>階段 1 在 monolith 內完成、不動部署、不動資料。完成後、拆分的「邊界」已經在 codebase 顯現、是 prerequisite。&lt;/p>
&lt;h3 id="階段-2新服務--雙寫期">階段 2：新服務 + 雙寫期&lt;/h3>
&lt;p>新服務 spin up、實作 adapter 同樣的介面。寫入路徑進入「雙寫期」：所有寫入同時寫 monolith 跟新服務、讀取仍從 monolith 取。&lt;/p>
&lt;p>雙寫期的設計關鍵：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>寫入順序&lt;/strong>：先寫 monolith 還是先寫新服務？通常先寫 monolith（保持 source of truth 一致性）、新服務寫失敗時記 error 但不影響業務&lt;/li>
&lt;li>&lt;strong>跨服務一致性&lt;/strong>：兩邊寫入用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox pattern&lt;/a> 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/saga/" data-link-title="Saga" data-link-desc="處理跨服務分散事務的補償型 transaction 序列、用最終一致換 ACID atomic">saga&lt;/a> 保證最終一致、不能容忍長期不一致&lt;/li>
&lt;li>&lt;strong>資料對賬機制&lt;/strong>：每天 / 每小時跑對賬 job、找出兩邊不一致的 row、修正 + 統計差異率&lt;/li>
&lt;li>&lt;strong>雙寫期長度&lt;/strong>：通常 1-4 週、視差異率收斂速度決定。差異率穩定在 0.01% 以下、可進階段 3&lt;/li>
&lt;/ul>
&lt;p>雙寫期的失敗訊號：差異率持續高於 1%、代表資料模型對應有 gap、不該進切流階段。&lt;/p>
&lt;h3 id="階段-3切流讀路徑遷移">階段 3：切流（讀路徑遷移）&lt;/h3>
&lt;p>雙寫期穩定後、讀路徑開始從 monolith 切到新服務。切流策略選擇：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>按 user / tenant ID hash 分流&lt;/strong>：取 user_id mod 100、x% 走新服務、其餘走 monolith。漸進 ramp up（1% → 5% → 25% → 100%）&lt;/li>
&lt;li>&lt;strong>按 endpoint 分流&lt;/strong>：read endpoint A 全切、endpoint B 跟 C 還在 monolith。適合「不同 endpoint 風險不同」的場景&lt;/li>
&lt;li>&lt;strong>Dark launch&lt;/strong>：每個 request 同時打兩邊、用 monolith 結果回應、log 兩邊差異。是 shadow read、不是真實切流、但能在切流前找出 edge case&lt;/li>
&lt;/ul>
&lt;p>切流期間的觀測重點：&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分與邊界判讀</a> 處理「該不該拆」、本章處理「決定拆之後實際怎麼動手」。拆服務是漸進演進的過程、一次性大爆炸（big bang）的成功率極低。<a href="/blog/backend/knowledge-cards/strangler-fig/" data-link-title="Strangler Fig Pattern" data-link-desc="服務拆分 / 系統替換的漸進演進模式、用『新舊共存 &#43; 逐步遷移 &#43; 最終下架』取代 big bang 重寫">Strangler Fig pattern</a> 是這層的工程基底 — 用「新功能在新服務、舊功能慢慢搬」的方式、把整個 monolith 包圍、逐步替換。</p>
<h2 id="strangler-fig-pattern-的工程含義">Strangler Fig Pattern 的工程含義</h2>
<p>Strangler Fig（絞殺榕）是 Martin Fowler 對漸進拆分的命名比喻：榕樹依附在宿主樹上、慢慢長大、最終取代宿主。應用到服務拆分：</p>
<ul>
<li><strong>舊系統繼續運作</strong>：拆分過程中、monolith 仍是 source of truth、新服務從旁長出</li>
<li><strong>流量逐步遷移</strong>：用 routing layer（API gateway、proxy、feature flag）控制哪些 request 走新服務、哪些走舊</li>
<li><strong>驗證 → 擴大</strong>：每個遷移的功能先小流量驗證、確認新舊一致後再加流量比例</li>
<li><strong>舊系統最終下架</strong>：當所有功能都遷出後、monolith 才被退役</li>
</ul>
<p>Strangler Fig 跟 big bang 拆分的本質差異是「失敗代價可控」— 大爆炸拆分失敗就整個服務掛、Strangler 拆分失敗只影響該功能、且可即時切回 monolith。</p>
<h2 id="拆分執行階段">拆分執行階段</h2>
<p>把 Strangler 細化成可操作的四階段：</p>
<h3 id="階段-1邊界冷凍--adapter-抽出">階段 1：邊界冷凍 + Adapter 抽出</h3>
<p>動手拆之前、先在 monolith 內部把「將要拆出去」的功能用 adapter / interface 封起來。所有外部呼叫該功能都走 adapter、不直接呼叫實作。</p>
<p>這層動作的責任：</p>
<ul>
<li><strong>強制 dependency 清楚</strong>：哪些功能依賴它、哪些功能被它依賴、必須變成顯式 interface 而非分散在 codebase</li>
<li><strong>資料邊界明示</strong>：該功能用到哪些 table / column、用 repository / DAO 封裝、不讓其他功能直接 access</li>
<li><strong>變更頻率冷凍</strong>：拆分期間原則上不接受該功能的新需求、避免「拆到一半新需求又進來」</li>
</ul>
<p>階段 1 在 monolith 內完成、不動部署、不動資料。完成後、拆分的「邊界」已經在 codebase 顯現、是 prerequisite。</p>
<h3 id="階段-2新服務--雙寫期">階段 2：新服務 + 雙寫期</h3>
<p>新服務 spin up、實作 adapter 同樣的介面。寫入路徑進入「雙寫期」：所有寫入同時寫 monolith 跟新服務、讀取仍從 monolith 取。</p>
<p>雙寫期的設計關鍵：</p>
<ul>
<li><strong>寫入順序</strong>：先寫 monolith 還是先寫新服務？通常先寫 monolith（保持 source of truth 一致性）、新服務寫失敗時記 error 但不影響業務</li>
<li><strong>跨服務一致性</strong>：兩邊寫入用 <a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox pattern</a> 或 <a href="/blog/backend/knowledge-cards/saga/" data-link-title="Saga" data-link-desc="處理跨服務分散事務的補償型 transaction 序列、用最終一致換 ACID atomic">saga</a> 保證最終一致、不能容忍長期不一致</li>
<li><strong>資料對賬機制</strong>：每天 / 每小時跑對賬 job、找出兩邊不一致的 row、修正 + 統計差異率</li>
<li><strong>雙寫期長度</strong>：通常 1-4 週、視差異率收斂速度決定。差異率穩定在 0.01% 以下、可進階段 3</li>
</ul>
<p>雙寫期的失敗訊號：差異率持續高於 1%、代表資料模型對應有 gap、不該進切流階段。</p>
<h3 id="階段-3切流讀路徑遷移">階段 3：切流（讀路徑遷移）</h3>
<p>雙寫期穩定後、讀路徑開始從 monolith 切到新服務。切流策略選擇：</p>
<ul>
<li><strong>按 user / tenant ID hash 分流</strong>：取 user_id mod 100、x% 走新服務、其餘走 monolith。漸進 ramp up（1% → 5% → 25% → 100%）</li>
<li><strong>按 endpoint 分流</strong>：read endpoint A 全切、endpoint B 跟 C 還在 monolith。適合「不同 endpoint 風險不同」的場景</li>
<li><strong>Dark launch</strong>：每個 request 同時打兩邊、用 monolith 結果回應、log 兩邊差異。是 shadow read、不是真實切流、但能在切流前找出 edge case</li>
</ul>
<p>切流期間的觀測重點：</p>
<ul>
<li><strong>錯誤率對比</strong>：新服務 vs monolith 同 endpoint 的 5xx / 4xx 比例</li>
<li><strong>延遲分布對比</strong>：P50 / P95 / P99 latency</li>
<li><strong>業務指標對比</strong>：轉換率、跳出率、訂單成功率 — 確認沒有「技術指標看起來正常、業務指標掉」的隱形 regression</li>
</ul>
<p>任一指標惡化、切回 monolith、不繼續推進。</p>
<h3 id="階段-4寫路徑遷移--monolith-退役">階段 4：寫路徑遷移 + Monolith 退役</h3>
<p>讀路徑 100% 切完、且穩定觀察一段時間後（建議至少 2 週）、寫路徑才從「雙寫」變成「只寫新服務」。</p>
<p>寫路徑切換的步驟：</p>
<ol>
<li><strong>雙寫變成「新服務 + 異步 backfill 到 monolith」</strong>：以新服務為主、monolith 變成 standby</li>
<li><strong>觀察期 1-2 週</strong>：確認新服務寫入路徑穩定、無資料遺失或不一致</li>
<li><strong>停止 backfill</strong>：monolith 不再被寫入、變成 read-only</li>
<li><strong>Monolith 該功能下架</strong>：等確認所有 dependency 都已遷移後（通常還要再 1-4 週觀察）、刪掉 monolith 對應 code 跟 table</li>
</ol>
<p>階段 4 是 point of no return — 過了寫路徑切換、回 monolith 的成本變得很高（要把新服務累積的寫入 backfill 回去）。這個 checkpoint 必須有明確的 go/no-go 決策、不是「順勢推進」。</p>
<h2 id="回退路徑設計">回退路徑設計</h2>
<p>回退條件必須在拆分啟動前就定義、不是事故時臨時決策。常見回退路徑：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>失敗訊號</th>
          <th>回退動作</th>
          <th>成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>Adapter 抽出後 monolith 變慢 / 出錯</td>
          <td>revert PR、重新規劃 adapter 邊界</td>
          <td>低</td>
      </tr>
      <tr>
          <td>2</td>
          <td>雙寫期差異率 &gt; 1% 持續</td>
          <td>停雙寫、回 monolith 單寫、修資料模型對應</td>
          <td>中</td>
      </tr>
      <tr>
          <td>3</td>
          <td>切流期間錯誤率 / 延遲 / 業務指標惡化</td>
          <td>切流比例調回 0%、回 monolith 單讀、雙寫繼續</td>
          <td>中</td>
      </tr>
      <tr>
          <td>4</td>
          <td>寫路徑切換後 1 週內出資料遺失</td>
          <td>觸發 backfill from 新服務 → monolith、切回雙寫期</td>
          <td>高</td>
      </tr>
      <tr>
          <td>4+</td>
          <td>Monolith 已下架、新服務出事</td>
          <td>災難級別、需要從備份重建 + 大規模事件公告</td>
          <td>極高</td>
      </tr>
  </tbody>
</table>
<p>階段 4 之後的回退代價是指數成長的。設計時要把 monolith 下架時點延後到「確信不需要回退」、寧可多保留 monolith 1-2 個月。</p>
<h2 id="拆分執行的判讀訊號">拆分執行的判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Adapter 抽出時發現難以封裝（dependency 散落各處）</td>
          <td>邊界其實沒形成、拆分判斷錯了</td>
          <td>回 10.1 重新評估、考慮先重構 monolith 再拆</td>
      </tr>
      <tr>
          <td>雙寫期差異率不收斂</td>
          <td>資料模型對應有 gap、或業務邏輯有 monolith 隱式依賴</td>
          <td>暫停拆分、做 data audit、找出隱式依賴點</td>
      </tr>
      <tr>
          <td>切流比例增加後業務指標掉</td>
          <td>技術等價但業務行為不等價（例如 latency 微升影響轉換）</td>
          <td>切回 monolith、檢查 latency / 業務指標關聯</td>
      </tr>
      <tr>
          <td>階段 4 出現「monolith 還有人在用」</td>
          <td>dependency 沒清乾淨、有隱藏的呼叫者</td>
          <td>延後 monolith 下架、用 access log audit 找出殘留呼叫者</td>
      </tr>
      <tr>
          <td>拆分過程中 dev velocity 大幅下降</td>
          <td>拆分成本超過短期收益、可能拆錯時機</td>
          <td>評估暫停拆分、回到 modular monolith</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把拆分當成「直接把功能搬出去」、跳過階段 1 adapter 抽出。沒有 adapter 抽出、新服務跟 monolith 的 dependency 邊界不清楚、雙寫期會出現難以排查的隱式依賴問題。</p>
<p>把雙寫期當成「過渡而已、隨便寫」。雙寫期是拆分的 source of truth verification 階段、差異率沒收斂前不能進切流。隨便寫的結果是切流後出資料一致性事故。</p>
<p>把「monolith 下架」當成拆分成功訊號。Monolith 下架太早是常見事故來源 — 即使流量 100% 切完、可能仍有 batch job / report / 內部 tool 在用 monolith。下架前先用 access log audit 確認真實流量為 0。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>本章專注「Strangler Fig 漸進拆分的執行流程」。當問題進入「該不該拆」的判讀、回 <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分與邊界判讀</a>；進入跨服務通訊設計（同步 vs 異步、event-driven）、進 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 message queue</a>；進入部署層的切流機制（feature flag、canary、blue/green）、進 <a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.8 deployment rollout</a>；進入資料庫遷移層的具體技術（dual write、shadow read、cutover），進 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作</a>。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>09 / 05 案例庫中、Strangler 拆分案例不算多（多數案例是已拆完的狀態描述、而非拆分過程紀錄）。可用以下案例反向追問：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix Aurora consolidation</a> — Netflix 的故事是「拆完合回去」、隱含 strangler 反向。對照本章可問：合併過程是否也走了類似四階段、只是方向相反（雙寫期把多 DB 合到 Aurora、再切讀路徑、最後下架原 DB）？</li>
<li><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> — 平台層整併。本章在「服務層」、整併在「平台層」、邏輯類似但 surface 不同。</li>
</ul>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分判讀</a> 的交接：10.1 給「該拆」的判讀、本章給「怎麼拆」的執行。</li>
<li>與 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 message queue + outbox</a> 的交接：雙寫期跟拆分後跨服務通訊都依賴 outbox / saga 保證一致性。</li>
<li>與 <a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.8 deployment rollout</a> 的交接：階段 3 切流的技術機制（feature flag、canary）跟部署層的 rollout 同源。</li>
<li>與 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作</a> 的交接：階段 2 雙寫期跟資料庫遷移的雙寫期是同一套機制、只是 surface 不同。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看拆分判讀（該不該拆）、回 <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分與邊界判讀</a>。要看拆分後跨服務通訊設計、進 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 模組訊息佇列</a>。要看部署層的切流技術細節、進 <a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">5.8 Deployment Rollout</a>。</p>
]]></content:encoded></item></channel></rss>