<?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>Mongodb on Tarragon</title><link>https://tarrragon.github.io/blog/tags/mongodb/</link><description>Recent content in Mongodb on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 02 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/mongodb/index.xml" rel="self" type="application/rss+xml"/><item><title>DB3 Vendor Selection：document / KV / multi-model 三方選型 + workload shape 前置判讀</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/db3-vendor-selection/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/db3-vendor-selection/</guid><description>&lt;p>DB3 vendor selection 的核心責任是把讀者從「我該選 MongoDB / DynamoDB / Cosmos DB 哪一家」這個問題、推到「我的 workload 是 document / KV / multi-model 哪一類」這個更前置的問題。三家文件都標榜 scalable schema-less、但實際取捨在 &lt;em>資料形狀、access pattern 穩定度、consistency 可接受度&lt;/em> 三軸決定 — 不識別 workload shape 直接比 vendor 是源頭錯誤。本文是 DB3 reader 進來的第一站：先做 workload shape 三軸前置判讀、再過 migration path 三型 + federated DB 視角、最後落到三 vendor 對比 10 軸。&lt;/p>
&lt;p>本文 &lt;em>不&lt;/em> 展開 vendor 機制細節（partition key 設計 / consistency level / RU sizing / connection management 等）— 那些屬 per-vendor deep article 的責任、本文在每個軸後 cross-link 過去。本文也 &lt;em>不&lt;/em> 比較三家「誰比較強」— 三 vendor 在 workload-by-workload 適配光譜上各有位置、寫成優劣比較會誤導讀者把選型壓成單軸。&lt;/p>
&lt;h2 id="問題情境讀者進來時的真實壓力">問題情境：讀者進來時的真實壓力&lt;/h2>
&lt;p>典型啟動壓力分兩類：&lt;/p>
&lt;p>第一類、團隊評估 document / KV / multi-model NoSQL 三家、文件都說「scalable schema-less」、看不出實際取捨。讀者徵兆是「我的資料是 document-shaped 還是 KV-shaped？」「partition key 該怎麼選？」「Atlas 跟 Cosmos DB MongoDB API 不一樣的點在哪？」「Cosmos DB multi-model 是真用得到還是行銷話術？」「on-demand vs provisioned 怎麼選？」&lt;/p>
&lt;p>第二類、既有 PostgreSQL / MySQL workload 撞 connection limit（surge 下 1K-5K pool 是隱性天花板、F1.7）、想換 KV 但不知道是否適合。讀者徵兆是「我已經有 Memcached、還要再加 MongoDB cache 層嗎？」「DynamoDB 適合當 OLTP 嗎？」「換 NoSQL 是不是解 connection 問題的銀彈？」&lt;/p>
&lt;p>這兩類讀者進來時的 &lt;em>真實問題&lt;/em> 不在 vendor 之間、在 &lt;em>workload 自己屬哪一型&lt;/em>。Case anchor 覆蓋六個 unique 角度：&lt;/p>
&lt;ul>
&lt;li>多型 document workload — &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected&lt;/a>（車載 sensor schema 隨車型演進、20 個 Atlas DB blast radius 切分）&lt;/li>
&lt;li>Document 跨雲 hedging — &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes&lt;/a>（自管 → Atlas、6 個月遷移、跨雲彈性）&lt;/li>
&lt;li>同 model 換 vendor 的 dogfood signal — &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365&lt;/a>（MongoDB → Cosmos DB MongoDB API、保留 driver、wire compat 限制）&lt;/li>
&lt;li>KV-as-buffer 正向用例 — &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &amp;#43; 傳統伺服器當慢速消費者、承受 100K&amp;#43; 同時選位 &amp;#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft&lt;/a>（DynamoDB 寫入緩衝、6750x 彈性、後端慢消費）&lt;/li>
&lt;li>PK 天然均勻典範 — &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &amp;#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads&lt;/a>（90M reads/sec 年度峰值、KV pattern 純粹）&lt;/li>
&lt;li>Federated DB 真實系統 — &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &amp;#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase&lt;/a>（MongoDB + DynamoDB + Memcached + mongobetween + freshness token）&lt;/li>
&lt;/ul>
&lt;h2 id="workload-shape--access-pattern--consistency-三軸前置判讀">Workload shape × access pattern × consistency 三軸前置判讀&lt;/h2>
&lt;p>進三家 vendor 對比前先回答：你的 workload 屬哪一型？三軸的組合決定 vendor 候選清單、軸不識別清楚直接比 vendor 是把選型壓成「品牌偏好」、不是工程決策。&lt;/p></description><content:encoded><![CDATA[<p>DB3 vendor selection 的核心責任是把讀者從「我該選 MongoDB / DynamoDB / Cosmos DB 哪一家」這個問題、推到「我的 workload 是 document / KV / multi-model 哪一類」這個更前置的問題。三家文件都標榜 scalable schema-less、但實際取捨在 <em>資料形狀、access pattern 穩定度、consistency 可接受度</em> 三軸決定 — 不識別 workload shape 直接比 vendor 是源頭錯誤。本文是 DB3 reader 進來的第一站：先做 workload shape 三軸前置判讀、再過 migration path 三型 + federated DB 視角、最後落到三 vendor 對比 10 軸。</p>
<p>本文 <em>不</em> 展開 vendor 機制細節（partition key 設計 / consistency level / RU sizing / connection management 等）— 那些屬 per-vendor deep article 的責任、本文在每個軸後 cross-link 過去。本文也 <em>不</em> 比較三家「誰比較強」— 三 vendor 在 workload-by-workload 適配光譜上各有位置、寫成優劣比較會誤導讀者把選型壓成單軸。</p>
<h2 id="問題情境讀者進來時的真實壓力">問題情境：讀者進來時的真實壓力</h2>
<p>典型啟動壓力分兩類：</p>
<p>第一類、團隊評估 document / KV / multi-model NoSQL 三家、文件都說「scalable schema-less」、看不出實際取捨。讀者徵兆是「我的資料是 document-shaped 還是 KV-shaped？」「partition key 該怎麼選？」「Atlas 跟 Cosmos DB MongoDB API 不一樣的點在哪？」「Cosmos DB multi-model 是真用得到還是行銷話術？」「on-demand vs provisioned 怎麼選？」</p>
<p>第二類、既有 PostgreSQL / MySQL workload 撞 connection limit（surge 下 1K-5K pool 是隱性天花板、F1.7）、想換 KV 但不知道是否適合。讀者徵兆是「我已經有 Memcached、還要再加 MongoDB cache 層嗎？」「DynamoDB 適合當 OLTP 嗎？」「換 NoSQL 是不是解 connection 問題的銀彈？」</p>
<p>這兩類讀者進來時的 <em>真實問題</em> 不在 vendor 之間、在 <em>workload 自己屬哪一型</em>。Case anchor 覆蓋六個 unique 角度：</p>
<ul>
<li>多型 document workload — <a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a>（車載 sensor schema 隨車型演進、20 個 Atlas DB blast radius 切分）</li>
<li>Document 跨雲 hedging — <a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a>（自管 → Atlas、6 個月遷移、跨雲彈性）</li>
<li>同 model 換 vendor 的 dogfood signal — <a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a>（MongoDB → Cosmos DB MongoDB API、保留 driver、wire compat 限制）</li>
<li>KV-as-buffer 正向用例 — <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a>（DynamoDB 寫入緩衝、6750x 彈性、後端慢消費）</li>
<li>PK 天然均勻典範 — <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a>（90M reads/sec 年度峰值、KV pattern 純粹）</li>
<li>Federated DB 真實系統 — <a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a>（MongoDB + DynamoDB + Memcached + mongobetween + freshness token）</li>
</ul>
<h2 id="workload-shape--access-pattern--consistency-三軸前置判讀">Workload shape × access pattern × consistency 三軸前置判讀</h2>
<p>進三家 vendor 對比前先回答：你的 workload 屬哪一型？三軸的組合決定 vendor 候選清單、軸不識別清楚直接比 vendor 是把選型壓成「品牌偏好」、不是工程決策。</p>
<h3 id="軸-1--資料形狀document--kv--不清楚">軸 1 — 資料形狀：document / KV / 不清楚</h3>
<p>資料形狀的核心判讀是 <em>aggregate root 邊界是否明確</em> 跟 <em>schema 是否會隨產品演進新增欄位</em>。document 適合的場景是資料天然多型、單筆記錄欄位差異大、應用層用 aggregate root 模式存取；KV 適合的場景是資料形狀固定、access pattern 數量少（&lt; 5 種）、固定 lookup by key。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>適配資料模型</th>
          <th>對應 case</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>資料天然多型（不同記錄欄位不同）、隨產品演進 schema 增刪欄位、aggregate root 邊界明確</td>
          <td>Document（MongoDB / Cosmos DB SQL API / MongoDB API）</td>
          <td>Toyota sensor schema 隨車型演進、Forbes CMS article 欄位多型</td>
      </tr>
      <tr>
          <td>資料形狀固定、access pattern &lt; 5 種、固定 lookup by key（meeting_id / message_id / user_id）</td>
          <td>KV（DynamoDB / Cosmos DB Table API / Redis 持久化變體）</td>
          <td>Amazon Ads 用 ad_id 查、Disney+ 用 user_id 查 watchlist、PayPay 用 message_id 查通知</td>
      </tr>
      <tr>
          <td>資料形狀還在探索、access pattern 變動頻繁、未來 6 個月會加 5+ 種新 query</td>
          <td>暫緩 NoSQL 選型、用 PostgreSQL + JSONB 過渡</td>
          <td>屬讀者誤判常見模式、case 沒揭露但 F1.3 / F1.6 推論：NoSQL 假設 access pattern 穩定、未穩定就上 NoSQL 會撞 single-table 設計天花板</td>
      </tr>
  </tbody>
</table>
<p>第三列的「暫緩 NoSQL」是反指標。NoSQL（特別是 DynamoDB single-table design）的核心假設是「access pattern 在設計時已知、後續變動有限」。資料模型還在探索、access pattern 半年內會大幅增減的場景、PostgreSQL + JSONB 給的彈性遠高於 NoSQL — JSONB 欄位可以演進、ad-hoc query 可以用 SQL 跑、未來釐清穩定 access pattern 後再選 NoSQL 不遲。</p>
<h3 id="軸-2--access-pattern-穩定度kv-適用度前置判讀">軸 2 — Access pattern 穩定度（KV 適用度前置判讀）</h3>
<p>KV 適用度的核心判讀是 <em>partition key 天然均勻度</em>。partition key 不均勻會讓 vendor 廣告的「scale infinitely」變成「scale 到 hot partition 為止」、單一 logical key 流量超過該 partition 上限就 throttle 或 latency spike（F1.1）。</p>
<ul>
<li><strong>天然均勻 PK + 穩定 access pattern</strong>（meeting_id / player_id / message_id / user_id）→ DynamoDB / Cosmos DB Table API 適用、PK 不需 composite key 修補。Amazon Ads 用 ad_id 撐 90M reads/sec、Zoom 用 meeting_id、Capcom 用 player_id、PayPay 用 message_id、Disney+ 用 user_id — 五個 case 都揭露同一 frame：<em>業務天然存在均勻 key 時 KV 是最自然的選擇</em>。</li>
<li><strong>天然不均勻 PK</strong>（event_id 一場演唱會集中 / date 時間序集中）→ 需 composite key 或 write sharding 修補。Tixcraft（9.C15）用 <code>event_id + user_id_hash</code> composite key 把單一熱門演唱會的 6750x spike 攤平到 partition 上 — 不是 DynamoDB 自身彈性、是 partition key 均勻分散的結果（F1.2）。</li>
<li><strong>Access pattern 變動頻繁</strong>（探索期、&lt; 5 種 query 還會增加）→ 不適合 DynamoDB single-table design、回 RDB。Single-table 把 access pattern 編進 PK / SK 結構、增加新 query 等於改 schema、改 schema 等於重新 load 資料、成本不對。</li>
</ul>
<p>KV 適用度判讀的延伸細節（hot partition 反模式 / composite key 設計 / adaptive capacity）見 <a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">DynamoDB partition key antipatterns</a>。</p>
<h3 id="軸-3--consistency-需求是否可接受-eventual">軸 3 — Consistency 需求是否可接受 eventual</h3>
<p>Consistency 需求的核心判讀是 <em>跨 partition / 跨 region transaction 是否為產品契約</em>。三家 vendor 都支援單 partition / 單 region 強一致、但 cross-partition / cross-region transaction 的機制跟限制差異大。</p>
<ul>
<li><strong>可接受 eventual / session consistency</strong>：DynamoDB（default eventually consistent reads、可選 strong）、Cosmos DB（5 個 consistency level、default session）、MongoDB（read concern 多級）— 三家都可以、選擇看其他軸。多數 KV / document workload 屬此類（social timeline、watchlist、message queue、analytics aggregation）。</li>
<li><strong>需要強一致 cross-partition transaction</strong>：DynamoDB 跨 partition transaction 限制（單一 transaction 最多 100 個 action、跨 region 不支援）、MongoDB 4.0+ 支援 multi-document transaction 但 sharded cluster 仍有 limitation、Cosmos DB 跨 logical partition transaction 受限 — 都不如 SQL／distributed SQL 自然、應回 DB4 entry point 評估 <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">Aurora DSQL / Spanner / CockroachDB</a>。</li>
<li><strong>跨 region active-active write</strong>：三家機制完全不同 — Cosmos DB multi-region write 跟 Strong consistency 是 <em>互斥</em> 設定（CAP 取捨硬約束、見 <a href="/blog/backend/01-database/vendors/cosmosdb/multi-region-write-conflict/" data-link-title="Cosmos DB Multi-Region Write：active-active、LWW、custom merge、Strong &#43; multi-region 互斥的 AP 取捨" data-link-desc="Multi-region active-active write 的 conflict resolution（LWW / custom merge / conflict feed）、Strong 跟 multi-region write 為什麼互斥、廣告 SLA vs 實測可用性鏈路拆解 — 從 Minecraft Earth &#43; Toyota Connected 切入">Cosmos DB multi-region write conflict</a> SSoT 主寫位置）；DynamoDB Global Tables 走 LWW（last-writer-wins）conflict resolution；MongoDB Atlas 跨 region 需手動 conflict 處理。三家不在同一光譜、選擇前必看各 vendor outline 的機制段。</li>
</ul>
<h2 id="migration-path-三型跨-case-合成-frame">Migration path 三型（跨 case 合成 frame）</h2>
<blockquote>
<p>本段是 <em>跨 case 合成 frame</em>、不是單一 case 揭露 — 從 Coinbase（9.C36）/ Forbes（9.C37）/ Microsoft 365（9.C30）三 case 萃取的共通結構（F2.1）。</p></blockquote>
<p>讀者進來時通常不是綠地、是 <em>既有系統演進</em>。三型遷移路徑的風險、ROI、適用條件完全不同、選錯路徑會推到錯的 vendor。</p>
<h3 id="第一型保留原-db--補周邊工具">第一型：保留原 DB + 補周邊工具</h3>
<p>不換 vendor、加 connection proxy（mongobetween / pgbouncer 類）、加 cache（Memcached + freshness token）、加 predictive scaling — 主資料層不動、應用層跟 ops 層補強。</p>
<ul>
<li><strong>代表 case</strong>：Coinbase（9.C36）保留 MongoDB Atlas、自建 mongobetween 把 60K connections/min 降到 ~2K（一個量級）、用 Memcached + freshness token 撐 1.5M reads/sec、用 ML predictive scaling 把擴容時間從 70 → 25 分鐘提前 60 分鐘</li>
<li><strong>路徑成本</strong>：中（自建工具、需要工程資源 build &amp; operate proxy / cache layer / ML model）</li>
<li><strong>風險</strong>：低（主資料層不動、回滾代價小）</li>
<li><strong>ROI</strong>：保留主資料 schema + access pattern、解 driver / 部署模型 / cache 一致性瓶頸</li>
<li><strong>適合</strong>：MongoDB（或主 DB）資料層撐得住、但應用層 connection storm / cache miss / 擴容慢卡瓶頸；團隊有工程能力 build 跟 maintain 周邊工具</li>
</ul>
<p>延伸實作細節見 MongoDB connection management（per-vendor article、cross-link 待寫稿）。</p>
<h3 id="第二型同-db-換託管">第二型：同 DB 換託管</h3>
<p>自管 → managed（Atlas / Cosmos DB / DocumentDB）、保留 schema 跟 access pattern、遷移期 6 個月量級。</p>
<ul>
<li><strong>代表 case</strong>：Forbes（9.C37）自管 MongoDB → MongoDB Atlas、保留 CMS schema、6 個月遷移、揭露「TCO 改善 25%」</li>
<li><strong>路徑成本</strong>：中（dual-write + shadow read 驗證、driver 行為差異、operation runbook 重寫）</li>
<li><strong>風險</strong>：中（dual-write 期間雙寫一致性、cutover 時點選擇）</li>
<li><strong>ROI</strong>：operation transfer（DBA bandwidth 釋放給 schema design / query tuning）+ TCO 改善</li>
<li><strong>適合</strong>：自管 ops burden 大（DBA bandwidth 被 backup / patching / replica lag 吃光）、不想換 model</li>
</ul>
<p><strong>Scope warning（Forbes 25% TCO）</strong>：「25% TCO 改善」是 Forbes 特定流量規模（120M MAU、70+ Atlas region）下的數字、<em>不普適</em>。引用要帶條件 — 不要寫成「Atlas 比自管便宜 25%」這種 vendor-neutral 結論。實際省多少要看自管當下的 license / hardware / ops 工時分配、跟 Atlas 在你流量規模下的 pricing tier。</p>
<h3 id="第三型換-vendor-保留-model">第三型：換 vendor 保留 model</h3>
<p>MongoDB → Cosmos DB MongoDB API、或 MongoDB → DocumentDB — wire protocol + driver 不變、底層架構整個換、ops 模型整個換。</p>
<ul>
<li><strong>代表 case</strong>：Microsoft 365（9.C30）MongoDB → Cosmos DB MongoDB API、保留 MongoDB driver</li>
<li><strong>路徑成本</strong>：高（dual-write per query pattern 驗證、wire compat ≠ 100% 行為相同、aggregation pipeline 跟 transaction 行為要逐項驗證）</li>
<li><strong>風險</strong>：高（每個 query pattern 都可能踩到不相容 edge case、cutover 點選擇難）</li>
<li><strong>ROI</strong>：跨 vendor 換（Azure 生態 / multi-model API / global distribution）+ 保留應用層 driver code</li>
</ul>
<p><strong>Scope warning（Microsoft 365 dogfood）</strong>：Microsoft 365 是 Microsoft 自家 dogfood、case 沒揭露具體 throughput / latency / cost 數字（F2.17）。dogfood 是 <em>高權重 selection signal</em>（雲商賭自家旗艦產品）、但 <em>不是 production benchmark</em>（沒公開數字可比對）。引用要明示「dogfood signal」而非「production proof」。</p>
<p><strong>Scope warning（100% wire compat）</strong>：Cosmos DB MongoDB API 廣告「100% wire compatibility」是 <em>vendor 行銷話術</em>、實際是「在某些 query pattern 下相容」（F2.9）。遷移時必須 <em>dual-write per query pattern</em> 驗證 — 不是看 vendor 文件 spec list、是用 production query corpus 跑一遍實測行為。Phase 0 audit checklist 應列出 unsupported aggregation stage、transaction edge case、index behavior 差異、change stream 跟 Change Feed 對應關係。</p>
<p>延伸 Cosmos DB MongoDB API vs SQL API 選型見 <a href="/blog/backend/01-database/vendors/cosmosdb/mongodb-api-vs-sql-api/" data-link-title="Cosmos DB MongoDB API vs SQL API：遷移路徑、dogfood signal、multi-model、跨雲 hedging" data-link-desc="從『MongoDB API 跟 SQL API 哪個快』推進到 vendor selection 的四層問題：三型遷移路徑、dogfood signal 怎麼讀、multi-model 差異化、跨雲 hedging — 從 Microsoft 365 dogfood 案例切入">Cosmos DB MongoDB API vs SQL API</a>。</p>
<h3 id="第四型不在-db3-範圍paradigm-shift-換引擎">第四型不在 DB3 範圍：paradigm shift 換引擎</h3>
<p>KV → SQL 或 SQL → distributed SQL 屬 paradigm shift、應進 <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">DB4 entry point: Aurora DSQL / Spanner / CockroachDB decision tree</a>。本文範圍是 DB3 三家內部選型、不展開 paradigm shift。</p>
<h2 id="從-rdb-撞牆來的快速路徑">從 RDB 撞牆來的快速路徑</h2>
<p>讀者若從 PostgreSQL / Aurora connection limit 撞牆過來、想評估 KV 替代、依撞牆訊號直接 route 到對應 article、不必先跑完三軸前置判讀：</p>
<ul>
<li><strong>撞 connection limit</strong>（surge 下 pool 1K-5K 隱性天花板、long-lived TCP 占滿）→ HTTP API 模型（no long-lived connection）的 KV 直接接寫入緩衝、進 <a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">dynamodb/single-table-design-pattern</a> 的「durable queue / write buffer」段（Tixcraft 9.C15 路徑：DynamoDB 接訂單、傳統 server 慢消費）、或評估 <a href="/blog/backend/01-database/vendors/cosmosdb/mongodb-api-vs-sql-api/" data-link-title="Cosmos DB MongoDB API vs SQL API：遷移路徑、dogfood signal、multi-model、跨雲 hedging" data-link-desc="從『MongoDB API 跟 SQL API 哪個快』推進到 vendor selection 的四層問題：三型遷移路徑、dogfood signal 怎麼讀、multi-model 差異化、跨雲 hedging — 從 Microsoft 365 dogfood 案例切入">Cosmos DB Table API</a></li>
<li><strong>撞單 primary 寫入上限</strong>（單 leader 寫吞吐天花板、read replica 無法分擔寫）→ multi-primary distributed SQL 路徑、進 <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">DB4 entry point: Aurora DSQL / Spanner / CockroachDB decision tree</a> 的 Path A（DoorDash 1.636 M QPS 單主寫入撞牆）</li>
<li><strong>撞單一 DB 撐不下 + 多 workload 形狀並存</strong>（read-heavy / write-heavy / analytics 混在一個 DB）→ federated DB 模式、看 <a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a>（MongoDB + DynamoDB + Memcached + mongobetween）+ <a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">9.C29 Lemino</a>（PostgreSQL → DynamoDB 揭露 RDB connection limit 隱性 bottleneck）</li>
</ul>
<p>進 <a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">dynamodb/single-table-design-pattern</a> 前先確認軸 1 / 軸 2 的 access pattern 穩定度跟 PK 天然均勻度 — connection limit 訊號 <em>必要但不充分</em>、KV 適用度 4 軸還是要走完、避免「為了解 connection 把不穩定 access pattern 硬塞 single-table」反模式。</p>
<h2 id="federated-db--system-role-視角跨-case-合成-frame">Federated DB + system role 視角（跨 case 合成 frame）</h2>
<blockquote>
<p>本段也是 <em>跨 case 合成 frame</em>（F2.18 + F1.6）— 三個 rich case（Coinbase / Toyota / Forbes）都揭露 production 系統是 <em>DB + 周邊工具</em> 組合、不是單一 DB monolithic 撐起來。</p></blockquote>
<p>讀者常誤以為「全用 X」是正解 — 全用 MongoDB、或全遷 DynamoDB、或全換 Cosmos DB。真實 production case 揭露兩個更前置的事實：(a) production 系統是 <em>federated</em>（多 DB 按 workload 分流）、不是 monolithic；(b) 每個 vendor 在系統中扮演 <em>特定角色</em>（control plane vs data plane vs cache）、不是 all-purpose store。</p>
<h3 id="federated-db-by-workload">Federated DB by workload</h3>
<p>Coinbase（9.C36）production 配置：MongoDB Atlas（document 主資料、identity service）+ DynamoDB（部分固定 KV workload）+ Memcached（read cache）+ mongobetween（connection proxy）+ Kinesis（event stream）。不是「全用 MongoDB」也不是「全遷 DynamoDB」、是按 workload shape 分流。</p>
<p>Toyota Connected（9.C38）：MongoDB Atlas 20 個 DB（microservice 拆 blast radius）+ Lambda + Kinesis + Redis + Kubernetes。20 個 DB 不是吞吐撐不住（18B txn/月 ≈ 7K txn/sec、單一 cluster 撐得下）、是 <em>microservice ownership</em> + <em>blast radius</em> 切分（F2.6）。</p>
<p>Forbes（9.C37）：MongoDB Atlas + 中介 abstraction layer + 50+ microservice。abstraction layer 隔離 schema 變動、避免 50 個服務都依賴 DB schema 細節（F2.3）。</p>
<p>三 case 揭露的共同 frame 是：<strong>寫 production 系統時假設「DB 一個服務搞定」、忽略 cache / queue / proxy / abstraction layer 跨層責任、會撞 connection limit / cache miss / cross-region replication 等隱性瓶頸</strong>。</p>
<h3 id="system-rolecontrol-plane-vs-data-plane">System role：control plane vs data plane</h3>
<p>DynamoDB 在 surge 場景能撐 nearly infinitely 不是 DynamoDB 自己神奇、是 <em>系統架構解耦</em> 的結果（F1.6）：</p>
<ul>
<li><strong>Control plane（metadata、state、user record）</strong>：DynamoDB / MongoDB / Cosmos DB 適合 — 流量是 small payload + high QPS pattern</li>
<li><strong>Data plane（影音、大型 BLOB、media stream）</strong>：CDN / S3 / object storage、<em>不在 DB3 範圍</em> — 流量是 large payload + bandwidth-bound</li>
<li><strong>Cache layer</strong>：Redis / Memcached / DAX（DynamoDB 補位）— 跟主 DB 形成跨層架構、處理讀峰值 + read-your-own-write 一致性</li>
</ul>
<p>三個 case 揭露同一 frame：Zoom 視訊 metadata 走 DynamoDB、影音走 WebRTC / edge servers；Disney+ watchlist 走 DynamoDB、影片串流走 CDN + S3；Capcom game state 走 DynamoDB + DAX、game server 走 EKS。<strong>把影音串流塞 DynamoDB 是違反 control plane vs data plane 分離、容量規劃會錯</strong>（每筆 1KB 的 KV vs 每筆 100MB 的 media chunk 是不同 workload）。</p>
<h2 id="三-vendor-對比-10-軸">三 vendor 對比 10 軸</h2>
<p>下表是三 vendor 在 selection 階段的 10 軸對比。每個軸後續都有 per-vendor deep article 展開機制、本文不重複展開。</p>
<table>
  <thead>
      <tr>
          <th>軸</th>
          <th>MongoDB</th>
          <th>DynamoDB</th>
          <th>Cosmos DB</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>資料模型核心</strong></td>
          <td>Document（aggregate root）+ aggregation pipeline</td>
          <td>KV with optional document fields + GSI / LSI</td>
          <td>Multi-model（SQL / MongoDB / Cassandra / Gremlin / Table API）</td>
      </tr>
      <tr>
          <td><strong>部署 topology</strong></td>
          <td>跨雲（Atlas AWS / GCP / Azure）+ self-hosted</td>
          <td>AWS-only managed</td>
          <td>Azure-only managed</td>
      </tr>
      <tr>
          <td><strong>跨雲 hedging</strong></td>
          <td>高（Atlas 跨雲、Forbes case）</td>
          <td>無（AWS lock-in）</td>
          <td>無（Azure lock-in）</td>
      </tr>
      <tr>
          <td><strong>Capacity 抽象</strong></td>
          <td>CPU + IOPS + working set RAM 三軸</td>
          <td>WCU/RCU + on-demand/provisioned + adaptive capacity</td>
          <td>RU（Request Unit）+ 5 consistency level</td>
      </tr>
      <tr>
          <td><strong>Contract layer</strong></td>
          <td>DB 層 <code>$jsonSchema</code> validator / app 層 abstraction / 混合</td>
          <td>DynamoDB Stream + app 層 validator</td>
          <td>DB 層 stored procedure + app 層 validator</td>
      </tr>
      <tr>
          <td><strong>Partition / shard key 可逆性</strong></td>
          <td><code>reshardCollection</code> 4.4+ 可改、成本高</td>
          <td>可改用 backfill</td>
          <td>不可改、必 export-recreate</td>
      </tr>
      <tr>
          <td><strong>Consistency model</strong></td>
          <td>Read concern（local / majority / linearizable）+ causal consistency session</td>
          <td>Eventually / strongly consistent reads</td>
          <td>5 level spectrum（Strong / Bounded staleness / Session / Consistent prefix / Eventual）</td>
      </tr>
      <tr>
          <td><strong>Multi-region write</strong></td>
          <td>Atlas 跨 region 手動 conflict 處理</td>
          <td>Global Tables LWW</td>
          <td>Multi-region write（Strong 互斥、見 cosmosdb/multi-region-write-conflict SSoT）</td>
      </tr>
      <tr>
          <td><strong>Dogfood signal</strong></td>
          <td>無（MongoDB 是獨立公司、不適用）</td>
          <td>Amazon 自家高頻使用（9.C5 Amazon Ads / 9.C27 Disney+ etc）</td>
          <td>Microsoft 365 dogfood（9.C30、<strong>Scope warning</strong>：dogfood 數字不公開、是 selection signal 不是 benchmark）</td>
      </tr>
      <tr>
          <td><strong>Multi-model 差異化</strong></td>
          <td>單一 document model</td>
          <td>單一 KV-with-document model</td>
          <td>唯一單服務支援 5 API（差異化價值、F2.16）</td>
      </tr>
  </tbody>
</table>
<h3 id="軸的延伸子段">軸的延伸子段</h3>
<p><strong>部署 topology / 跨雲 hedging</strong>：三家 topology 是 <em>vendor lock-in</em> 跟 <em>跨雲彈性</em> 的硬取捨。Forbes 選 Atlas 不是當下省錢（自管 MongoDB 也可以、TCO 改善是副作用）、是 <em>未來雲商策略尚未底定</em> 的 hedging — Atlas 提供 AWS / GCP / Azure 三家部署、未來換雲不用換 DB（F2.10）。對照 DynamoDB / Cosmos DB / Spanner / Aurora 都是單雲鎖定 — 選了就跟著該雲商生態走。團隊雲商策略已底定（深度用 AWS / Azure / GCP 其一）時、單雲 vendor 通常較划算（更好的 IAM 整合、更深的 ops 工具、單一 support 通道）。跨雲價值真正成立是 <em>策略不確定</em> 或 <em>合規要求多雲</em> 場景。</p>
<p><strong>Capacity 抽象</strong>：三家 capacity 抽象的 <em>思維遷移成本</em> 可能高過 vendor 廣告的價差（F2.12）。MongoDB 用 CPU + IOPS + working set RAM 三軸思維、跟自管 PostgreSQL / MySQL 類似、團隊轉換成本低。DynamoDB 用 WCU/RCU 抽象、要學「估每個操作消耗多少 unit」、加上 on-demand / provisioned / adaptive capacity 三模式選擇。Cosmos DB 用 <a href="/blog/backend/knowledge-cards/request-unit/" data-link-title="Request Unit" data-link-desc="Cosmos DB 的容量抽象單位、1 RU = 1KB document strong-consistent read 的 CPU &#43; memory &#43; IOPS 綜合 cost、寫 ~5 RU、複雜 query 數百 RU">Request Unit</a>（RU）抽象、1 RU ≈ 1 KB document 的 strong read 成本、寫 ~5 RU、複雜 query 數百 RU — 工程師要學會用 RU 思考、不是用 CPU 思考、團隊知識遷移成本可能高。容量規劃延伸見對應 vendor 的 sizing article。</p>
<p><strong>Partition / shard key 可逆性</strong>：三家 <em>不在同一光譜</em>、是選 vendor 前必做的 access pattern audit 重點（F2.15）。MongoDB <code>reshardCollection</code>（4.4+）可改、但成本高、需要 cluster downtime 或長時間 background migration。DynamoDB partition key 技術上可改、實作上用 backfill（建新 table、新 PK、雙寫舊新、cutover）— ops 工作量大但可逆。Cosmos DB partition key <em>不可改</em>、改 partition key 等於 export-recreate-import — 對 1TB+ 資料是大型 migration 工程。三家不可逆性遞增、選 Cosmos DB 前必須前期完整 access pattern audit、不能「先上 production 之後再調」。</p>
<p><strong>Consistency model</strong>：三家機制設計哲學不同。MongoDB read concern 是 <em>per-operation</em> 選擇（同一 client connection 可以混用）；DynamoDB strong vs eventual 是 <em>per-read</em> 選項（write 端統一強一致）；Cosmos DB 5 個 level 是 <em>account-level default + per-request override</em>、且 Strong 跟 multi-region write 互斥（CAP 硬約束）。設計上 MongoDB 最 flexible、Cosmos DB 最 explicit、DynamoDB 介於中間。延伸機制細節見 <a href="/blog/backend/01-database/vendors/cosmosdb/consistency-levels-engineering/" data-link-title="Cosmos DB 5 Consistency Levels：Session 預設、Bounded staleness、Strong 邊界跟跨 collection 分流策略" data-link-desc="Cosmos DB 5 個 consistency level 的工程選擇邏輯、Session 為何是 production 預設、per-request override 跟跨 collection 分流的進階策略、Strong &#43; multi-region 互斥的 cross-link — 從 Minecraft Earth &#43; ASOS 切入">Cosmos DB consistency levels engineering</a>、<a href="/blog/backend/01-database/vendors/cosmosdb/multi-region-write-conflict/" data-link-title="Cosmos DB Multi-Region Write：active-active、LWW、custom merge、Strong &#43; multi-region 互斥的 AP 取捨" data-link-desc="Multi-region active-active write 的 conflict resolution（LWW / custom merge / conflict feed）、Strong 跟 multi-region write 為什麼互斥、廣告 SLA vs 實測可用性鏈路拆解 — 從 Minecraft Earth &#43; Toyota Connected 切入">Cosmos DB multi-region write conflict</a>（SSoT 主寫位置）。</p>
<p><strong>Multi-model 差異化</strong>：Cosmos DB 是 <em>唯一單一服務支援 5 API</em> 的雲商 DB（SQL / MongoDB / Cassandra / Gremlin / Table）— 對照 AWS 走多產品覆蓋（DynamoDB KV + DocumentDB MongoDB-compat + Neptune graph + Keyspaces Cassandra-compat）、GCP 走多產品覆蓋（Firestore + Spanner + Bigtable）。multi-model 的差異化價值是 <em>減少多 DB 並存運維</em> — 一個產品團隊只養一個 service、一套 IAM、一套 backup / DR、一套 monitoring。但 <em>是否真用上 multi-model</em> 要看團隊實際 workload — 多數團隊只用 1-2 個 API、單一 model 的競品（DynamoDB / MongoDB）可能更專注（F2.16）。</p>
<h2 id="失敗模式cross-vendor-反模式">失敗模式（cross-vendor 反模式）</h2>
<p>下列七條是三 vendor 都會踩、跨 case 共通的反模式。Per-vendor 特定反模式（例如 DynamoDB on-demand 隱性 hot partition、MongoDB schema 三代並存）在 per-vendor deep article。</p>
<h3 id="反模式-1把-dynamodb-當-oltp">反模式 1：把 DynamoDB 當 OLTP</h3>
<p>訊號：access pattern 還在探索期、5+ 種 query 還會增加、強一致 cross-partition transaction 是產品契約。應回 PostgreSQL / Aurora、不是繼續加碼 DynamoDB single-table design。</p>
<p>DynamoDB 的 <em>正確</em> 用法包含 control plane KV（Zoom / Disney+ / Capcom）跟 durable queue / write buffer（Tixcraft 9.C15 揭露的非 OLTP 正向用例、F1.3）— DynamoDB 接「訂單」寫入、不是即時生效、是讓 traditional server（金流 / 票庫）用自己能承受的速度消費。這層解耦讓「前端可以擴 130 倍、後端不用同步擴」。</p>
<h3 id="反模式-2把-mongodb-當-kv">反模式 2：把 MongoDB 當 KV</h3>
<p>訊號：access pattern 固定、PK 天然均勻、不需要 aggregation pipeline、document 內部從不展開（只查 root 欄位）。</p>
<p>應改 DynamoDB / Cosmos DB Table API。MongoDB 在這場景的 overhead（document overhead / connection model / aggregation engine 未用上）不划算 — KV vendor 的單筆讀寫成本更低、scaling 模型更簡單。</p>
<h3 id="反模式-3把-cosmos-db-當跨雲服務">反模式 3：把 Cosmos DB 當跨雲服務</h3>
<p>訊號：團隊評估 multi-cloud DR / 跨雲 portability、看到 Cosmos DB 文件強調「global distribution」就以為支援跨雲。</p>
<p>Cosmos DB 是 <em>Azure-only</em>、global distribution 指 Azure 內跨 region。想跨雲應改 MongoDB Atlas。multi-model 差異化是 <em>Azure 生態內</em> 的價值（F2.16）— 一旦離開 Azure、Cosmos DB 的所有獨特優勢都不存在。</p>
<h3 id="反模式-4federated-db-假設全用-x">反模式 4：federated DB 假設「全用 X」</h3>
<p>訊號：寫架構設計時假設「DB 一個服務搞定」、不規劃 cache / queue / proxy / abstraction layer。</p>
<p>Production 真實系統都是 federated（Coinbase / Toyota / Forbes 都是）。寫架構時假設一個 DB 搞定會撞 connection limit（surge 下 RDB 第一個爆點、F1.7）/ cache miss（單靠 DB 撐不住讀峰值）/ cross-region replication（跨 region 一致性處理錯）等隱性瓶頸。預先設計 federated topology + 跨層責任分配、不是事後補。</p>
<h3 id="反模式-5誤判-dogfood-case-數字">反模式 5：誤判 dogfood case 數字</h3>
<p>訊號：引用 Microsoft 365 / Amazon Prime Day 等 dogfood case 時、把它當 production benchmark、抄具體數字當 sizing 依據。</p>
<p>Dogfood case 數字常 <em>不公開</em> 或 <em>不適用 customer-facing</em>（F2.17 + F1.10）— Amazon Prime Day 「90M reads/sec」是年度峰值最高一秒不是平均、Microsoft 365 直接沒給數字、Google Spanner「10 億 req/sec」是 Google 全使用者加總不是單客戶配額。寫架構時引用要明示 selection signal（雲商賭身家、值得當高權重 vendor 訊號）vs production benchmark（具體 sizing 數字）— 兩者不可混為一談。</p>
<h3 id="反模式-6partition-key-一上-production-才發現不可逆">反模式 6：partition key 一上 production 才發現不可逆</h3>
<p>訊號：選 Cosmos DB / DynamoDB 時、partition key 設計沒做完整 access pattern audit、上 production 一段時間後發現 hot partition、想改 PK。</p>
<p>三家不在同一光譜（見前段對比表）— MongoDB shard key 4.4+ 可改但成本高、DynamoDB 可 backfill 改、Cosmos DB <em>不可改</em> 必 export-recreate。選 Cosmos DB 前要前期完整 access pattern audit、列所有預期 query 跟對應 PK 訪問頻率、確認最熱 PK 流量在單一 partition 容量上限內（F2.15）。</p>
<h3 id="反模式-7wire-compatibility-當-100-行為相同">反模式 7：wire compatibility 當 100% 行為相同</h3>
<p>訊號：選 Cosmos DB MongoDB API 或 DocumentDB、看到「MongoDB compatible」就假設 MongoDB driver 跑得起來就是相容、跳過 query pattern 驗證。</p>
<p>Wire compat ≠ 行為 100% 相同（F2.9）。Cosmos DB MongoDB API 廣告「100% wire compatibility」是行銷話術、實際是「在某些 query pattern 下相容」— aggregation pipeline 某些 stage 不支援、transaction edge case 行為差異、index 行為差異都會踩到。遷移必須 dual-write per query pattern 驗證、不是看 vendor spec list。</p>
<h2 id="不該選-db3-的訊號升-sql--升-distributed-sql-路徑">不該選 DB3 的訊號（升 SQL / 升 distributed SQL 路徑）</h2>
<p>下列四條訊號出現時、選擇應跳出 DB3 範圍。</p>
<ul>
<li><strong>JOIN-heavy + 強 normalize workload</strong>：應留 PostgreSQL（包括 PostgreSQL + JSONB 混合方案）、不該塞 NoSQL 再 <code>$lookup</code>。aggregation pipeline 的 <code>$lookup</code> 性能遠不如 SQL JOIN、在 sharded cluster 還有限制。</li>
<li><strong>強一致 cross-region transaction 是產品契約</strong>：應進 <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">DB4 entry point</a> 評估 distributed SQL（CockroachDB / Spanner / Aurora DSQL）。三家 NoSQL 的 cross-region transaction 都有 limitation、不該當主路徑。</li>
<li><strong>大流量 + 跨業務 fleet 治理</strong>：Aurora 200 cluster 模式（9.C4 DraftKings 揭露的 business sharding fleet）可能更合適、進 Aurora fleet 治理。NoSQL 的 fleet 治理工具鏈（cluster lifecycle / cross-cluster query / unified IAM）通常不如 managed SQL 成熟。</li>
<li><strong>資料模型還在探索 + access pattern 變動快</strong>：暫緩 NoSQL 選型、用 PostgreSQL + JSONB 過渡。JSONB 給 document-like flexibility、SQL 給 ad-hoc query power、未來釐清穩定 access pattern 後再選 NoSQL 不遲。</li>
</ul>
<h2 id="下一步路由per-vendor-outline-子組">下一步路由（per-vendor outline 子組）</h2>
<p>讀者識別 workload type（軸 1-3）+ migration path（三型）+ system role（federated / control plane）後、進對應 per-vendor 子組繼續深化。</p>
<h3 id="mongodb-子組">MongoDB 子組</h3>
<ul>
<li>入門：<a href="/blog/backend/01-database/vendors/mongodb/schema-design-pattern/" data-link-title="MongoDB Schema Design Pattern：contract layer 在哪 vs embedded / reference" data-link-desc="MongoDB document schema 真正的 production 議題不是 embedded vs reference 二選一、是 schema contract 該放 DB 層 validator 還是 app 層 abstraction；含 Toyota polymorphic governance、Forbes abstraction layer、time-series collection 邊界">schema design pattern</a>（contract layer 三選一：DB 層 validator / app 層 abstraction / 混合）</li>
<li>容量：<a href="/blog/backend/01-database/vendors/mongodb/shard-key-selection/" data-link-title="MongoDB Shard Key Selection：hashed vs ranged、單 cluster 切 shard vs 多 cluster 切 blast radius" data-link-desc="MongoDB sharded cluster shard key 選型（hashed / ranged / compound）、單 cluster 分 shard vs 多 cluster 分 blast radius 對照、跟 DynamoDB / Cosmos DB partition key 可逆性的跨 vendor 紀律">shard key selection</a>（單 cluster vs 多 cluster blast radius、Toyota 20 DB 模式）</li>
<li>Migration：<a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">migrate to Atlas</a>（同 DB 換託管型）</li>
</ul>
<h3 id="dynamodb-子組">DynamoDB 子組</h3>
<ul>
<li>入門：<a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">single-table design pattern</a>（access pattern 設計 + 適用度前置判讀）</li>
<li>機制：<a href="/blog/backend/01-database/vendors/dynamodb/consistency-model-optimization/" data-link-title="DynamoDB Strongly Consistent → Eventually Consistent：same protocol, different contract" data-link-desc="DynamoDB consistency model 從 strongly consistent read 改 eventually consistent read 是 50% cost 優化但風險集中在 application contract — 同 vendor / 同 protocol / 同 table / 不同 read consistency；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 consistency axis 候選；涵蓋 read pattern audit / 5 個 production 踩雷">consistency model optimization</a>（strong vs eventually consistent 取捨）</li>
</ul>
<h3 id="cosmos-db-子組">Cosmos DB 子組</h3>
<ul>
<li>入門：<a href="/blog/backend/01-database/vendors/cosmosdb/mongodb-api-vs-sql-api/" data-link-title="Cosmos DB MongoDB API vs SQL API：遷移路徑、dogfood signal、multi-model、跨雲 hedging" data-link-desc="從『MongoDB API 跟 SQL API 哪個快』推進到 vendor selection 的四層問題：三型遷移路徑、dogfood signal 怎麼讀、multi-model 差異化、跨雲 hedging — 從 Microsoft 365 dogfood 案例切入">MongoDB API vs SQL API</a>（API model 選型、四層 framing）</li>
</ul>
<h3 id="跨層架構federated-db--cache--proxy">跨層架構（federated DB / cache / proxy）</h3>
<p>跨層架構的延伸內容見對應 per-vendor connection management / cache layer article（後續會寫）— 本文只在軸 2 / federated frame 點到、不展開機制。</p>
<h3 id="進-db4-evaluation">進 DB4 evaluation</h3>
<p>若需要強一致 cross-region SQL / paradigm shift（KV → distributed SQL 或 SQL → distributed SQL）、進 <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">DB4 entry point: Aurora DSQL / Spanner / CockroachDB decision tree</a>。</p>
<h2 id="knowledge-card-路由">Knowledge card 路由</h2>
<p>本文涉及的 knowledge card：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/document-store/" data-link-title="Document Store" data-link-desc="說明以 JSON 文件與彈性 schema 提供資料存取的模式，以及它仍需的治理邊界">document-store</a> — document model 的核心概念跟 aggregate root 邊界</li>
<li><a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot-partition</a> — KV vendor 的 partition 容量上限機制</li>
<li><a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database-sharding</a> — shard key 跟 partition key 設計</li>
<li><a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">consistency-level</a> — strong / eventual / session 三類取捨</li>
<li><a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">vendor-lock-in</a> — 單雲 vs 跨雲的 hedging 取捨</li>
<li><a href="/blog/backend/knowledge-cards/distributed-sql/" data-link-title="Distributed SQL" data-link-desc="把 SQL 與交易語意延伸到多節點與多區域的資料庫形態">distributed-sql</a> — 跳出 DB3 進 DB4 的概念入口</li>
</ul>
]]></content:encoded></item><item><title>MongoDB</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/</guid><description>&lt;p>MongoDB 是 document database 的事實標準。schema flexibility、aggregation pipeline、跨雲 managed（Atlas）讓它成為許多 startup 的 default 選擇。Microsoft 365、Disney+ 早期、Uber 等大規模平台都從 MongoDB 起家，後來依 workload 壓力把部分路徑遷移到 KV / 雲商專屬服務（Cosmos DB、DynamoDB）。&lt;/p>
&lt;h2 id="教學路線document-shape-與-schema-governance">教學路線：Document shape 與 schema governance&lt;/h2>
&lt;p>MongoDB 服務頁的教學目標是把 document model、schema flexibility、index、aggregation pipeline 與 sharding 放回資料形狀治理。讀者讀完後要能判斷資料是否適合 aggregate root，並知道 schema governance 如何影響長期維護成本。&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>Document shape&lt;/td>
 &lt;td>哪些資料適合 aggregate root 與 nested document&lt;/td>
 &lt;td>定位、適用場景&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Schema governance&lt;/td>
 &lt;td>schema flexibility 如何搭配 validation、版本與 migration&lt;/td>
 &lt;td>容量規劃要點、預計實作話題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query / index&lt;/td>
 &lt;td>index、aggregation pipeline、ad-hoc query 如何影響成本&lt;/td>
 &lt;td>容量特性、常見陷阱&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sharding&lt;/td>
 &lt;td>shard key、chunk、balancer 如何把資料形狀變容量問題&lt;/td>
 &lt;td>容量規劃要點、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">Database Sharding&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>替代路由&lt;/td>
 &lt;td>何時轉 PostgreSQL、DynamoDB、Cosmos DB 或 search&lt;/td>
 &lt;td>不適用場景、跟其他 vendor 的取捨&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="定位json-document--跨雲彈性">定位：JSON document + 跨雲彈性&lt;/h2>
&lt;p>MongoDB 是以 document model 為主體的 DB。PostgreSQL JSONB 適合「SQL 為主、少量半結構化欄位」；MongoDB 則把 BSON document、aggregation pipeline、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database sharding&lt;/a> 與 schema governance 放在核心設計裡。近年版本加入 time series、change streams、queryable encryption、CSFLE 等能力。&lt;/p>
&lt;p>選 MongoDB 的核心訴求：document model 是主要 use case、需要跨雲 managed（Atlas）、想避免 vendor lock-in（也可自管）。&lt;/p>
&lt;h2 id="容量特性">容量特性&lt;/h2>
&lt;p>&lt;strong>單一 instance 吞吐&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>一般 m5.4xlarge：5K-15K WPS（依 doc size、index）&lt;/li>
&lt;li>高階 instance + tuning：30K-50K WPS&lt;/li>
&lt;li>超過此級別 → sharding&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Sharding&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>MongoDB 原生支援 sharded cluster&lt;/li>
&lt;li>mongos router + config servers + shard&lt;/li>
&lt;li>MongoDB sharding 要主動設計 shard key，並和 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition&lt;/a> 風險一起看&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Replication&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>Replica set（primary + secondary、async）&lt;/li>
&lt;li>跨 region 通常 async&lt;/li>
&lt;li>自動 failover &amp;lt; 30 秒（mongod 內建）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Storage&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>單一 collection 沒有官方上限、但 shard key resharding 過去版本是大手術（4.4+ 支援 reshardCollection）&lt;/li>
&lt;/ul>
&lt;h2 id="適用場景">適用場景&lt;/h2>
&lt;p>&lt;strong>1. Document model 主要 workload&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<p>MongoDB 是 document database 的事實標準。schema flexibility、aggregation pipeline、跨雲 managed（Atlas）讓它成為許多 startup 的 default 選擇。Microsoft 365、Disney+ 早期、Uber 等大規模平台都從 MongoDB 起家，後來依 workload 壓力把部分路徑遷移到 KV / 雲商專屬服務（Cosmos DB、DynamoDB）。</p>
<h2 id="教學路線document-shape-與-schema-governance">教學路線：Document shape 與 schema governance</h2>
<p>MongoDB 服務頁的教學目標是把 document model、schema flexibility、index、aggregation pipeline 與 sharding 放回資料形狀治理。讀者讀完後要能判斷資料是否適合 aggregate root，並知道 schema governance 如何影響長期維護成本。</p>
<table>
  <thead>
      <tr>
          <th>學習段</th>
          <th>核心問題</th>
          <th>對應段落</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Document shape</td>
          <td>哪些資料適合 aggregate root 與 nested document</td>
          <td>定位、適用場景</td>
      </tr>
      <tr>
          <td>Schema governance</td>
          <td>schema flexibility 如何搭配 validation、版本與 migration</td>
          <td>容量規劃要點、預計實作話題</td>
      </tr>
      <tr>
          <td>Query / index</td>
          <td>index、aggregation pipeline、ad-hoc query 如何影響成本</td>
          <td>容量特性、常見陷阱</td>
      </tr>
      <tr>
          <td>Sharding</td>
          <td>shard key、chunk、balancer 如何把資料形狀變容量問題</td>
          <td>容量規劃要點、<a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">Database Sharding</a></td>
      </tr>
      <tr>
          <td>替代路由</td>
          <td>何時轉 PostgreSQL、DynamoDB、Cosmos DB 或 search</td>
          <td>不適用場景、跟其他 vendor 的取捨</td>
      </tr>
  </tbody>
</table>
<h2 id="定位json-document--跨雲彈性">定位：JSON document + 跨雲彈性</h2>
<p>MongoDB 是以 document model 為主體的 DB。PostgreSQL JSONB 適合「SQL 為主、少量半結構化欄位」；MongoDB 則把 BSON document、aggregation pipeline、<a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database sharding</a> 與 schema governance 放在核心設計裡。近年版本加入 time series、change streams、queryable encryption、CSFLE 等能力。</p>
<p>選 MongoDB 的核心訴求：document model 是主要 use case、需要跨雲 managed（Atlas）、想避免 vendor lock-in（也可自管）。</p>
<h2 id="容量特性">容量特性</h2>
<p><strong>單一 instance 吞吐</strong>：</p>
<ul>
<li>一般 m5.4xlarge：5K-15K WPS（依 doc size、index）</li>
<li>高階 instance + tuning：30K-50K WPS</li>
<li>超過此級別 → sharding</li>
</ul>
<p><strong>Sharding</strong>：</p>
<ul>
<li>MongoDB 原生支援 sharded cluster</li>
<li>mongos router + config servers + shard</li>
<li>MongoDB sharding 要主動設計 shard key，並和 <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a> 風險一起看</li>
</ul>
<p><strong>Replication</strong>：</p>
<ul>
<li>Replica set（primary + secondary、async）</li>
<li>跨 region 通常 async</li>
<li>自動 failover &lt; 30 秒（mongod 內建）</li>
</ul>
<p><strong>Storage</strong>：</p>
<ul>
<li>單一 collection 沒有官方上限、但 shard key resharding 過去版本是大手術（4.4+ 支援 reshardCollection）</li>
</ul>
<h2 id="適用場景">適用場景</h2>
<p><strong>1. Document model 主要 workload</strong>：</p>
<ul>
<li>schema 變化頻繁的早期產品</li>
<li>nested document 自然表達領域模型（訂單含多個 item、用戶含多個 preference）</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — 從 MongoDB 遷移到 Cosmos DB MongoDB API、保留 document model</li>
</ul>
<p><strong>2. Aggregation pipeline 重 workload</strong>：</p>
<ul>
<li>複雜的 $group / $match / $project chain</li>
<li>報表、analytics、ETL prep</li>
<li>比 RDBMS 寫複雜 query 更直觀（對某些 team）</li>
</ul>
<p><strong>3. 跨雲 managed（Atlas）</strong>：</p>
<ul>
<li>MongoDB Atlas 跨 AWS / GCP / Azure</li>
<li>跟 DynamoDB（AWS only）、Cosmos DB（Azure only）、Spanner（GCP only）相反</li>
<li>適合多雲策略、避免單一 vendor lock-in</li>
</ul>
<p><strong>4. Time series workload（6.0+）</strong>：</p>
<ul>
<li>time series collection 專屬優化</li>
<li>不過 InfluxDB / TimescaleDB 仍是更專業選擇</li>
</ul>
<p><strong>5. 已有 MongoDB 生態 + 想轉移操作責任</strong>：</p>
<ul>
<li>Atlas 提供 backup、failover、monitoring、auto-scale</li>
<li>想把 MongoDB DBA / SRE 操作責任交給 Atlas</li>
</ul>
<h2 id="不適用場景">不適用場景</h2>
<p><strong>1. 強 ACID multi-document transaction</strong>：</p>
<ul>
<li>MongoDB Transaction 支援多 document、但跨 shard 有性能影響</li>
<li>高頻金融交易仍建議 SQL 系統</li>
<li>替代：PostgreSQL、Aurora、Spanner</li>
</ul>
<p><strong>2. 複雜 JOIN</strong>：</p>
<ul>
<li>MongoDB <code>$lookup</code> 適合少量相鄰資料，JOIN-heavy workload 應回 SQL 系統</li>
<li>schema design 階段要把常用讀取路徑 denormalize 成 document shape</li>
<li>替代：SQL 系統做 JOIN-heavy workload</li>
</ul>
<p><strong>3. 純 KV + sub-ms latency</strong>：</p>
<ul>
<li>MongoDB document model 比 KV 多一層 BSON parsing</li>
<li>替代：Redis、DynamoDB、Bigtable</li>
</ul>
<p><strong>4. 大規模 OLAP</strong>：</p>
<ul>
<li>aggregation 對中等資料量還行、TB 級不適合</li>
<li>替代：ClickHouse、BigQuery、Spark on Delta Lake</li>
</ul>
<p><strong>5. 嚴格資料模型 + schema enforcement</strong>：</p>
<ul>
<li>MongoDB schema flexibility 可能導致 production data inconsistency</li>
<li>替代：SQL DB（schema 強制）+ JSONB column 處理半結構化</li>
</ul>
<h2 id="跟其他-vendor-的取捨">跟其他 vendor 的取捨</h2>
<p><strong>vs Cosmos DB MongoDB API</strong>：</p>
<ul>
<li>MongoDB Atlas：跨雲、原生 MongoDB 行為</li>
<li>Cosmos DB MongoDB API：Azure-only、global distribution + 5 <a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">consistency level</a>s</li>
<li>選 MongoDB Atlas：跨雲、需要原生 MongoDB features</li>
<li>選 Cosmos DB：Azure 生態、需要更好 global distribution</li>
<li>對應案例：<a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — 從 MongoDB 遷到 Cosmos DB MongoDB API，主要保留 document model</li>
</ul>
<p><strong>vs DynamoDB</strong>：</p>
<ul>
<li>MongoDB：document model、aggregation 強、跨雲</li>
<li>DynamoDB：KV / single-table design、AWS 整合、5 個 9 SLA</li>
<li>選 MongoDB：document 為主、跨雲</li>
<li>選 DynamoDB：KV 為主、AWS 生態</li>
<li>詳見 <a href="/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB vendor page</a> 對比段</li>
</ul>
<p><strong>vs PostgreSQL JSONB</strong>：</p>
<ul>
<li>MongoDB：document 為主、schema-less</li>
<li>PostgreSQL：SQL 為主、JSONB 補充</li>
<li>選 MongoDB：document 占主要 schema</li>
<li>選 PostgreSQL JSONB：主要結構化、少量半結構化欄位</li>
</ul>
<p><strong>vs Couchbase / Couchdb / Firestore</strong>：</p>
<ul>
<li>Couchbase：MongoDB 替代、有 N1QL（SQL-like）</li>
<li>CouchDB：偏小規模、master-master replication</li>
<li>Firestore：GCP-only、realtime updates</li>
<li>MongoDB 在這群裡是生態最廣的</li>
</ul>
<p><strong>vs Elasticsearch 作為 search 替代</strong>：</p>
<ul>
<li>兩者分屬不同類別：MongoDB 是 OLTP / document、Elasticsearch 是 search + analytics</li>
<li>通常搭配用：MongoDB 主、Elasticsearch 處理 full-text search</li>
</ul>
<h2 id="容量規劃要點">容量規劃要點</h2>
<p><strong>1. Shard key 設計是命脈</strong>：</p>
<ul>
<li>跟 DynamoDB partition key 同樣關鍵</li>
<li>不均勻 → hot shard、實際容量達不到名義</li>
<li>4.4+ 可以 reshard、但仍是大手術</li>
</ul>
<p><strong>2. Replica set 是 HA 基礎</strong>：</p>
<ul>
<li>至少 3 個 member（1 primary + 2 secondary）</li>
<li>secondary 可 read（read preference）但要注意 lag</li>
<li>failover 通常 &lt; 30 秒</li>
</ul>
<p><strong>3. Atlas managed 服務</strong>：</p>
<ul>
<li>提供 auto-scaling、auto-backup、跨雲部署</li>
<li>Tier 從 M0（free）到 M700（高階）</li>
<li>Atlas Online Archive 自動把舊資料移到便宜 storage</li>
</ul>
<p><strong>4. Index 限制</strong>：</p>
<ul>
<li>單 collection 最多 64 個 index</li>
<li>compound index 有順序敏感（{a:1, b:1} 跟 {b:1, a:1} 不同）</li>
<li>TTL index 自動 expire 過期 document</li>
</ul>
<p><strong>5. Change streams（CDC）</strong>：</p>
<ul>
<li>4.0+ 提供原生 change streams</li>
<li>對接 Kafka / event bus 做 event sourcing</li>
</ul>
<h2 id="anti-recommendation-與升級路由">Anti-recommendation 與升級路由</h2>
<p>MongoDB 的 schema flexibility 會降低早期建模成本，也會把 schema governance 延後到 production。這一段先說何時維持 document model，再說何時升級 Atlas、sharding、Cosmos DB、DynamoDB 或 SQL。</p>
<table>
  <thead>
      <tr>
          <th>機制 / 路線</th>
          <th>維持簡單設計的條件</th>
          <th>升級訊號</th>
          <th>主要引用路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單一 replica set</td>
          <td>document size 穩定、working set 可控、primary 寫入足夠</td>
          <td>storage / write / working set 接近上限、failover 演練不足</td>
          <td><a href="/blog/backend/knowledge-cards/replication-lag/" data-link-title="Replication Lag" data-link-desc="說明資料副本落後正式來源多久，以及它如何影響讀取正確性">Replication Lag</a>、<a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a></td>
      </tr>
      <tr>
          <td>Atlas managed</td>
          <td>團隊仍能管理 backup、upgrade、monitoring 與 scaling</td>
          <td>DBA / SRE 責任想轉交平台、跨雲部署與 backup 成為主要壓力</td>
          <td><a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a>、<a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a></td>
      </tr>
      <tr>
          <td>Sharded cluster</td>
          <td>single replica set 還能承擔容量與維護窗口</td>
          <td>shard key 穩定、tenant / user / region 可分、hot shard 可觀測</td>
          <td><a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">Database Sharding</a>、<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a></td>
      </tr>
      <tr>
          <td>Cosmos DB MongoDB API</td>
          <td>Azure 只是部署選項，原生 MongoDB 行為仍重要</td>
          <td>Azure global distribution、multi-region write 或 RU governance 成主題</td>
          <td><a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor</a></td>
      </tr>
      <tr>
          <td>DynamoDB / KV</td>
          <td>query 仍需要 document traversal 與 aggregation</td>
          <td>access pattern 固定、sub-10ms p99、connection-free scaling 成主題</td>
          <td><a href="/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB vendor</a></td>
      </tr>
      <tr>
          <td>PostgreSQL</td>
          <td>document 是主要資料形狀</td>
          <td>JOIN-heavy、transaction-heavy、schema 約束是主要價值</td>
          <td><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL vendor</a></td>
      </tr>
  </tbody>
</table>
<p>MongoDB 的簡單路徑是先把 document boundary 寫清楚。資料可以彈性演進，但 application 仍要知道哪些欄位是正式契約、哪些欄位只是相容期，並用 validation、migration 與 data quality check 管住版本漂移。</p>
<p>Sharding 的升級路徑要等 shard key 與 query shape 足夠穩定。過早切 shard 會把 aggregation、transaction 與 index 成本提前放大；過晚切 shard 則會讓 resharding、chunk migration 與 balancer 壓力進入 production 高峰期。</p>
<h2 id="deep-article已完成">Deep article（已完成）</h2>
<p>本批 6 篇 deep article 已完成、覆蓋 MongoDB 從 schema 設計到 production 跨層架構的核心 production 議題：</p>
<table>
  <thead>
      <tr>
          <th>主題</th>
          <th>文章</th>
          <th>對應 production 議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema contract 該放 DB 層 validator 還是 app 層 abstraction</td>
          <td><a href="schema-design-pattern/">schema-design-pattern</a></td>
          <td>Toyota polymorphic governance、Forbes abstraction layer</td>
      </tr>
      <tr>
          <td>Shard key 選型 + 單 cluster vs 多 cluster blast radius</td>
          <td><a href="shard-key-selection/">shard-key-selection</a></td>
          <td>Toyota 20 DB blast radius、跟 DynamoDB 可逆性對比</td>
      </tr>
      <tr>
          <td>Read preference + causal session 跟 cache 層 freshness token</td>
          <td><a href="replica-set-read-preference/">replica-set-read-preference</a></td>
          <td>DB 層 + cache 層讀後一致性兩層合用</td>
      </tr>
      <tr>
          <td>Aggregation pipeline 順序 / index / memory boundary</td>
          <td><a href="aggregation-pipeline-optimization/">aggregation-pipeline-optimization</a></td>
          <td>report dashboard 跑爆 primary 的 anti-pattern 治理</td>
      </tr>
      <tr>
          <td>Change streams resume token + Kafka connector 治理</td>
          <td><a href="change-streams-kafka/">change-streams-kafka</a></td>
          <td>at-least-once 語義 + idempotency + resume token 過期防護</td>
      </tr>
      <tr>
          <td>Driver × deployment × cache × predictive scaling 三層協作</td>
          <td><a href="connection-management-and-cache-layer/">connection-management-and-cache-layer</a></td>
          <td>Coinbase mongobetween + freshness token + ML 預測擴容三件套</td>
      </tr>
  </tbody>
</table>
<p>跨 vendor entry：先看 <a href="../db3-vendor-selection/">DB3 vendor selection</a>（MongoDB / DynamoDB / Cosmos DB 三方選型 + workload shape 前置判讀），再進本 vendor 的 deep article。</p>
<h2 id="後續擴充仍待補">後續擴充（仍待補）</h2>
<ul>
<li>Index 設計跟覆蓋</li>
<li>從自管 MongoDB 遷到 Atlas</li>
<li>從 MongoDB 遷到 Cosmos DB MongoDB API（保留 document model）</li>
<li>從 MongoDB 遷到 DynamoDB（access pattern 需要重設計）</li>
<li>Queryable encryption（CSFLE）</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>跟 MongoDB 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a></td>
          <td>從 MongoDB 遷到 Cosmos DB MongoDB API、planet-scale analytics</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a></td>
          <td>MongoDB 為主資料層、自建 mongobetween 解決 Ruby 連線爆炸、users 服務 1.5M reads/sec</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a></td>
          <td>自管 MongoDB → Atlas on GCP、6 個月遷完、build 25→9 分鐘、120M MAU</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a></td>
          <td>Atlas 撐 900 萬車 telematics、月 180 億 transaction、緊急訊號 3 秒內到 agent</td>
      </tr>
  </tbody>
</table>
<p>MongoDB case 的讀法分三組：</p>
<ul>
<li><strong>作為 production 主角持續演進</strong>（Coinbase、Toyota Connected）：document model 撐住核心 OLTP / IoT、配 connection proxy / cache / event-driven 處理擴展周邊。</li>
<li><strong>自管 → managed 遷移</strong>（Forbes）：同 document model、換託管模式、ROI 集中在 DBA 責任轉移跟跨雲彈性、不是性能改善。</li>
<li><strong>遷出 MongoDB 保留 API</strong>（Microsoft 365）：document model 保留、底層換到 Cosmos DB MongoDB API、換取 Azure global distribution。</li>
</ul>
<p>讀 case 時要區分 MongoDB 在「主角 / 遷入 / 遷出」三種位置的差異，三種位置揭露的工程議題完全不同。</p>
<h2 id="常見陷阱">常見陷阱</h2>
<ul>
<li><strong>schema 長期 schema-less</strong>：production 出現 data inconsistency、難 query</li>
<li><strong>shard key 用 _id（自增）</strong>：寫入全集中在最後一個 shard</li>
<li><strong>$lookup 過度使用</strong>：跨 collection JOIN-heavy workload 應在 schema design 時 denormalize 或回 SQL</li>
<li><strong>index 太多</strong>：寫吞吐被拖垮、定期 review 未用 index</li>
<li><strong>secondary read 不檢查 lag</strong>：用戶讀到 stale data</li>
<li><strong>不規劃 Atlas tier upgrade 路徑</strong>：流量上來才發現 tier 跟不上、緊急升級費用高</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>完整 T1 對照：<a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">01-database vendors index</a></li>
<li>平行：<a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor</a>（MongoDB API replacement）、<a href="/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB vendor</a>（KV alternative）</li>
<li>上游：<a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design</a>、<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a></li>
<li>下游：<a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a>（MongoDB 遷出範例）</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>、<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>（shard key 跟 hot shard）</li>
<li>官方：<a href="https://www.mongodb.com/docs/manual/">MongoDB Manual</a>、<a href="https://www.mongodb.com/atlas">MongoDB Atlas</a></li>
</ul>
]]></content:encoded></item><item><title>MongoDB → Atlas：Atlas 不是 MongoDB + managed、是另一個 product</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration&lt;/a> playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB&lt;/a> 跟 MongoDB Atlas。本文是 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology&lt;/a> Type C operational redesign hybrid 的標準形態實證。每階段切換用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate&lt;/a> 把關 — 4 phase 之間的驗證條件就是 gate。&lt;/p>&lt;/blockquote>
&lt;h2 id="atlas-不是-mongodb--managed是另一個-product">Atlas 不是 MongoDB + managed、是另一個 product&lt;/h2>
&lt;p>「MongoDB Atlas 是 MongoDB 的 managed 版本」這個 framing 看似合理、實際誤導：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Protocol 相容&lt;/strong>：MongoDB wire protocol 一致、driver 不改、&lt;code>mongosh&lt;/code> 連線跟 self-managed 一樣&lt;/li>
&lt;li>&lt;strong>Storage 一致&lt;/strong>：WiredTiger storage engine 一樣、document model 一樣&lt;/li>
&lt;li>&lt;strong>API 一致&lt;/strong>：Aggregation framework、indexing、change stream 都一樣&lt;/li>
&lt;/ul>
&lt;p>但 &lt;em>operational surface 完全不同&lt;/em>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Operational concept&lt;/th>
 &lt;th>Self-managed MongoDB&lt;/th>
 &lt;th>Atlas&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Cluster bootstrap&lt;/td>
 &lt;td>mongod + replica set config + cfgsvr + shard 手動&lt;/td>
 &lt;td>UI / API 一鍵建集群、全自動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>HA&lt;/td>
 &lt;td>Replica set 自管 + arbiter + priority&lt;/td>
 &lt;td>自動跨 AZ replica + automatic failover&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backup&lt;/td>
 &lt;td>mongodump + S3 archive 自管&lt;/td>
 &lt;td>內建 cloud backup + PITR（按 region 設）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Network access&lt;/td>
 &lt;td>VPC + security group + IP whitelist 自管&lt;/td>
 &lt;td>Atlas private endpoint / VPC peering / IP access list&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Authentication&lt;/td>
 &lt;td>mongod 內部 user / x.509 自管&lt;/td>
 &lt;td>Atlas Database User + 整合 LDAP / SSO / AWS IAM&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Monitoring&lt;/td>
 &lt;td>Self-deploy Prometheus + grafana&lt;/td>
 &lt;td>Atlas Performance Advisor + APM 內建&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sizing&lt;/td>
 &lt;td>Manual instance class + scale&lt;/td>
 &lt;td>Auto-tier scaling + tier-based pricing&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Patching&lt;/td>
 &lt;td>Manual + outage window&lt;/td>
 &lt;td>Automatic（可配置 maintenance window）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Migration 主要工作不在 &lt;em>資料層&lt;/em> — protocol drop-in 已 cover；是 &lt;em>operational stack 全換&lt;/em>：SRE runbook、monitoring dashboard、access control、IAM 整合、cost 預估全要重做。「Atlas 是 managed MongoDB」這個 framing 低估了 operational 工作量。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration</a> playbook、cross-link 到 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB</a> 跟 MongoDB Atlas。本文是 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> Type C operational redesign hybrid 的標準形態實證。每階段切換用 <a href="/blog/backend/knowledge-cards/migration-gate/" data-link-title="Migration Gate" data-link-desc="說明遷移流程何時可以進入下一階段或正式切換">migration gate</a> 把關 — 4 phase 之間的驗證條件就是 gate。</p></blockquote>
<h2 id="atlas-不是-mongodb--managed是另一個-product">Atlas 不是 MongoDB + managed、是另一個 product</h2>
<p>「MongoDB Atlas 是 MongoDB 的 managed 版本」這個 framing 看似合理、實際誤導：</p>
<ul>
<li><strong>Protocol 相容</strong>：MongoDB wire protocol 一致、driver 不改、<code>mongosh</code> 連線跟 self-managed 一樣</li>
<li><strong>Storage 一致</strong>：WiredTiger storage engine 一樣、document model 一樣</li>
<li><strong>API 一致</strong>：Aggregation framework、indexing、change stream 都一樣</li>
</ul>
<p>但 <em>operational surface 完全不同</em>：</p>
<table>
  <thead>
      <tr>
          <th>Operational concept</th>
          <th>Self-managed MongoDB</th>
          <th>Atlas</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cluster bootstrap</td>
          <td>mongod + replica set config + cfgsvr + shard 手動</td>
          <td>UI / API 一鍵建集群、全自動</td>
      </tr>
      <tr>
          <td>HA</td>
          <td>Replica set 自管 + arbiter + priority</td>
          <td>自動跨 AZ replica + automatic failover</td>
      </tr>
      <tr>
          <td>Backup</td>
          <td>mongodump + S3 archive 自管</td>
          <td>內建 cloud backup + PITR（按 region 設）</td>
      </tr>
      <tr>
          <td>Network access</td>
          <td>VPC + security group + IP whitelist 自管</td>
          <td>Atlas private endpoint / VPC peering / IP access list</td>
      </tr>
      <tr>
          <td>Authentication</td>
          <td>mongod 內部 user / x.509 自管</td>
          <td>Atlas Database User + 整合 LDAP / SSO / AWS IAM</td>
      </tr>
      <tr>
          <td>Monitoring</td>
          <td>Self-deploy Prometheus + grafana</td>
          <td>Atlas Performance Advisor + APM 內建</td>
      </tr>
      <tr>
          <td>Sizing</td>
          <td>Manual instance class + scale</td>
          <td>Auto-tier scaling + tier-based pricing</td>
      </tr>
      <tr>
          <td>Patching</td>
          <td>Manual + outage window</td>
          <td>Automatic（可配置 maintenance window）</td>
      </tr>
  </tbody>
</table>
<p>Migration 主要工作不在 <em>資料層</em> — protocol drop-in 已 cover；是 <em>operational stack 全換</em>：SRE runbook、monitoring dashboard、access control、IAM 整合、cost 預估全要重做。「Atlas 是 managed MongoDB」這個 framing 低估了 operational 工作量。</p>
<p>跑 <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>MongoDB protocol / API 完全相容</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>HA / backup / monitoring / IAM / network 全換</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>同 document DB</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>同 1 個 cluster</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>Connection string / IAM 整合改、application logic 不改</td>
          <td>Low/Medium</td>
      </tr>
  </tbody>
</table>
<p>主導維度 Operational = High、Schema / Paradigm 都 Low — 對映 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Type C operational redesign hybrid</a>。</p>
<h2 id="結構4-phase-operational--drop-in-cutover">結構：4-phase operational + drop-in cutover</h2>
<p>跟 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a> 結構對齊（同 Type C）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">Phase 0：Pre-migration audit（1-2 週）
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  - Workload sizing（IOPS / connection / storage）
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  - Application connection pattern audit
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  - Compliance requirement audit
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">Phase 1：Operational infrastructure 準備（2-3 週）
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  - Atlas cluster 建立
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  - VPC peering / private endpoint
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  - IAM role + Atlas Database User
</span></span><span class="line"><span class="ln">10</span><span class="cl">  - Monitoring + alert
</span></span><span class="line"><span class="ln">11</span><span class="cl">  - Backup retention 設定
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">Phase 2：Data migration（取決於 dataset 大小）
</span></span><span class="line"><span class="ln">14</span><span class="cl">  - mongomirror / Atlas Live Migration tool
</span></span><span class="line"><span class="ln">15</span><span class="cl">  - 或 mongodump → mongorestore（小 DB）
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl">Phase 3：Cutover 跟 verification
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">Phase 4：Cleanup（self-managed decommission）</span></span></code></pre></div><p>整體 4-12 週、依 dataset 大小跟 organization 流程複雜度。</p>
<h2 id="phase-0pre-migration-audit">Phase 0：Pre-migration audit</h2>
<h3 id="workload-sizing--atlas-tier">Workload sizing → Atlas tier</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">Self-managed observations:
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">- Peak IOPS: 8000
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">- P99 read latency: 5ms
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">- Connection count peak: 1500
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">- Storage: 800GB
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">- Cross-region replication needed: yes
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">Atlas tier mapping:
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">- M40 (8 vCPU, 16GB RAM): IOPS 3000、不夠
</span></span><span class="line"><span class="ln">10</span><span class="cl">- M60 (16 vCPU, 64GB RAM): IOPS 6000、邊界
</span></span><span class="line"><span class="ln">11</span><span class="cl">- M80 (32 vCPU, 128GB RAM): IOPS 9000、安全（選此）
</span></span><span class="line"><span class="ln">12</span><span class="cl">- Storage: 1TB tier（足夠 800GB + 25% buffer）
</span></span><span class="line"><span class="ln">13</span><span class="cl">- Cross-region replication add-on</span></span></code></pre></div><p>Atlas 不是 <em>自由 instance class</em>、是 <em>固定 tier</em>；workload 跨 tier 邊界時要選 <em>上一級</em> 而不是 push 下一級。</p>
<h3 id="connection-pattern-audit">Connection pattern audit</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// Application connection pool config
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MongoClient</span><span class="p">(</span><span class="nx">uri</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">maxPoolSize</span><span class="o">:</span> <span class="mi">100</span><span class="p">,</span>     <span class="c1">// ← Atlas 端 tier-specific connection limit
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="nx">minPoolSize</span><span class="o">:</span> <span class="mi">10</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nx">maxIdleTimeMS</span><span class="o">:</span> <span class="mi">60000</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>Atlas tier 對 <em>single user connection</em> 有限制（M40 ~1500、M80 ~3000）；多 application instance 跑同帳號連 Atlas 可能撞 limit。預先計算 total connection = <code>pod_count × maxPoolSize</code>、對照 tier limit。</p>
<h3 id="compliance-audit">Compliance audit</h3>
<ul>
<li><strong>Data residency</strong>：Atlas 部署 region 是否符合 GDPR / 客戶合約</li>
<li><strong>Encryption at rest</strong>：Atlas 預設 enable、但 <em>encryption key 是 Atlas-managed</em> — 合規嚴格要用 CMK / BYOK</li>
<li><strong>Audit log</strong>：Atlas 提供 audit log、export 到 S3 / Splunk</li>
</ul>
<h2 id="phase-1operational-infrastructure-準備">Phase 1：Operational infrastructure 準備</h2>
<h3 id="atlas-cluster-配置">Atlas cluster 配置</h3>





<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="c"># 用 Terraform mongodbatlas provider</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="l">resource &#34;mongodbatlas_cluster&#34; &#34;production&#34; {</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="l">project_id   = var.project_id</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="l">name         = &#34;production-cluster&#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="l">cluster_type = &#34;REPLICASET&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="l">provider_name         = &#34;AWS&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="l">provider_region_name  = &#34;US_EAST_1&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="l">provider_instance_size_name = &#34;M80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="l">backup_enabled         = true</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="l">pit_enabled            = true  </span><span class="w"> </span><span class="c"># PITR</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="l">mongo_db_major_version = &#34;7.0&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">  </span><span class="l">advanced_configuration {</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="l">javascript_enabled                   = false</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span><span class="l">minimum_enabled_tls_protocol         = &#34;TLS1_2&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="l">no_table_scan                        = false</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">    </span><span class="l">oplog_size_mb                        = 51200</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">  </span>}<span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w"></span>}<span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w"></span><span class="c"># Backup retention</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w"></span><span class="l">resource &#34;mongodbatlas_cloud_backup_schedule&#34; &#34;production&#34; {</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">  </span><span class="l">project_id   = var.project_id</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">  </span><span class="l">cluster_name = mongodbatlas_cluster.production.name</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">  </span><span class="l">reference_hour_of_day    = 3</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">  </span><span class="l">reference_minute_of_hour = 0</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">  </span><span class="l">restore_window_days      = 7</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">  </span><span class="l">policy_item_daily {</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w">    </span><span class="l">frequency_interval = 1</span><span class="w">
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="w">    </span><span class="l">retention_unit     = &#34;days&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="w">    </span><span class="l">retention_value    = 7</span><span class="w">
</span></span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="w">  </span>}<span class="w">
</span></span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="w"></span>}</span></span></code></pre></div><h3 id="vpc-peering--private-endpoint">VPC peering / private endpoint</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">Pattern A: VPC Peering
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  AWS VPC &lt;──peering──&gt; Atlas project VPC
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  - 跨 region 跑、routing table 對齊
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  - 適合中型 / 大型 workload、stable network topology
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">Pattern B: Private Endpoint (Atlas private link)
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  AWS VPC ──private link──&gt; Atlas
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  - 不需要 routing table 改
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  - 適合 multi-account / multi-region 複雜場景
</span></span><span class="line"><span class="ln">10</span><span class="cl">  - Cost 略高</span></span></code></pre></div><p>production default 走 Private Endpoint、設定簡單跟 IAM 整合好。</p>
<h3 id="atlas-database-user-跟-iam-整合">Atlas Database User 跟 IAM 整合</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Pattern A: 傳統 username / password
</span></span><span class="line"><span class="ln">2</span><span class="cl">  - 設 Database User、application 用 SCRAM-SHA-256 連
</span></span><span class="line"><span class="ln">3</span><span class="cl">  - 適合 legacy application
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">Pattern B: AWS IAM authentication（推薦）
</span></span><span class="line"><span class="ln">6</span><span class="cl">  - Atlas Database User type: &#34;AWS IAM&#34;
</span></span><span class="line"><span class="ln">7</span><span class="cl">  - Application 用 AWS IAM role + Atlas SDK
</span></span><span class="line"><span class="ln">8</span><span class="cl">  - Token 15 分鐘輪換、application 自管 refresh</span></span></code></pre></div><p>cutover 時間表內加 IAM authentication migration、不要事後補。</p>
<h2 id="phase-2data-migration">Phase 2：Data migration</h2>
<h3 id="atlas-live-migration-tool小到中型">Atlas Live Migration tool（小到中型）</h3>
<p>Atlas UI 內建 Live Migration tool：</p>
<ol>
<li>Source cluster URI（self-managed MongoDB）</li>
<li>Atlas target cluster</li>
<li>tool 自動 full sync + oplog tailing</li>
<li>Cutover window 內 final cutover</li>
</ol>
<p>支援 dataset &lt; 100GB 簡單；100GB-1TB 需要分批 / collection 順序設計。</p>
<h3 id="mongomirror大型">mongomirror（大型）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Mongomirror: source → atlas</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">mongomirror <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --host source-replicaset/host1:27017,host2:27017 <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --destination atlas-cluster-host:27017 <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --destinationUsername admin <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --destinationPassword <span class="nv">$ATLAS_PASSWORD</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --ssl</span></span></code></pre></div><p>mongomirror 分兩段：</p>
<ol>
<li>Initial sync（full dump + restore）</li>
<li>Oplog tailing（continuous CDC）</li>
</ol>
<p>Cutover 期間 application 切 connection string、mongomirror 跟著 stream 收尾。</p>
<h2 id="phase-3cutover--verification">Phase 3：Cutover + verification</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">1. Application 端設 maintenance mode（block write）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. Wait mongomirror catch up（oplog gap → 0）
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 驗證 Atlas 端 collection count + sample query
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. Application connection string 切到 Atlas
</span></span><span class="line"><span class="ln">5</span><span class="cl">5. 解除 maintenance、monitor 24-48 小時
</span></span><span class="line"><span class="ln">6</span><span class="cl">6. Self-managed mongo read-only standby 1-2 週</span></span></code></pre></div><h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1atlas-tier-connection-limit-撞牆">Case 1：Atlas tier connection limit 撞牆</h3>
<p><strong>徵兆</strong>：cutover 後 application 流量高峰時大量 <code>Connection refused</code>、Atlas 端顯示 connection limit reached；self-managed 階段沒有這問題。</p>
<p><strong>根因</strong>：M80 tier connection limit ~3000、application 100 個 pod × maxPoolSize=50 = 5000 connection；超出 limit。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-migration 計算</strong>：total connection 對照 Atlas tier、超出選上一級 tier</li>
<li><strong>降 maxPoolSize</strong>：100 pod × 30 = 3000、剛好 cap；但 burst 仍可能撞</li>
<li><strong>加 connection proxy</strong>：在 application 跟 Atlas 之間放 connection pooler（如 mongos sharded 或 ProxySQL-style proxy）</li>
</ol>
<h3 id="case-2ip-whitelist-漏-application-vpccutover-後完全連不上">Case 2：IP whitelist 漏 application VPC、cutover 後完全連不上</h3>
<p><strong>徵兆</strong>：cutover 後 application 直接報 <code>connection timeout</code>、Atlas dashboard 顯示 zero traffic；troubleshooting 1 小時才發現是 IP access list 漏掉某 application VPC CIDR。</p>
<p><strong>根因</strong>：Atlas IP access list 預設 deny all、必須明示加 application VPC；Phase 1 設定漏看某個 VPC（如 multi-account organization 內的 staging account）。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-cutover 連線測試</strong>：每個 application VPC 跑 sample MongoDB 連線、確認 ping 通</li>
<li><strong>改 Private Endpoint</strong>：不靠 IP whitelist、用 PrivateLink 自動 routing</li>
<li><strong>Backup access</strong>：保留 bastion host with whitelisted IP、incident 期間能直連</li>
</ol>
<h3 id="case-3backup-retention-設不夠compliance-audit-抓到">Case 3：Backup retention 設不夠、compliance audit 抓到</h3>
<p><strong>徵兆</strong>：cutover 3 個月後 SOX audit 發現 backup retention 設 7 天、合規要求 90 天；急忙改 Atlas config 設 90 天、但 <em>過去 3 個月 backup 已不可恢復</em>。</p>
<p><strong>根因</strong>：Atlas backup retention 是 <em>向前生效</em>、不能回追加；Phase 1 預設配置漏對合規 review。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-Phase 1 跑 compliance review</strong>：跟 legal / security team 確認 retention / data residency / audit log</li>
<li><strong>預設 retention 設保守值</strong>（30 / 60 天）、之後可降不能升</li>
<li><strong>PITR 跟 backup retention 分開設</strong>：PITR window 7-30 天、full backup 90-365 天</li>
</ol>
<h3 id="case-4iam-token-過期application-端-reconnect-storm">Case 4：IAM token 過期、application 端 reconnect storm</h3>
<p><strong>徵兆</strong>：production 切到 IAM authentication 後、每 15 分鐘出現一波 connection failure；Atlas log 顯示「auth token expired」。</p>
<p><strong>根因</strong>：AWS IAM token 15 分鐘輪換、application 用舊 token 重連失敗；token refresh 邏輯沒寫對。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 用 Atlas SDK + AWS SDK 整合、自動 token refresh
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="p">{</span> <span class="nx">MongoClient</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;mongodb&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kr">const</span> <span class="p">{</span> <span class="nx">fromIni</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;@aws-sdk/credential-providers&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="kr">const</span> <span class="nx">credentials</span> <span class="o">=</span> <span class="nx">fromIni</span><span class="p">({</span> <span class="nx">profile</span><span class="o">:</span> <span class="s1">&#39;production&#39;</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="kr">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MongoClient</span><span class="p">(</span><span class="nx">uri</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="nx">authMechanism</span><span class="o">:</span> <span class="s1">&#39;MONGODB-AWS&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="c1">// SDK 自動 refresh token
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p>不要自管 token rotation、用 vendor SDK 抽象掉。</p>
<h3 id="case-5billing-暴漲iops-跟-backup-storage-超預估">Case 5：Billing 暴漲、IOPS 跟 backup storage 超預估</h3>
<p><strong>徵兆</strong>：第一個月 Atlas 帳單 $15K USD、預估 $8K；Atlas dashboard 顯示 backup storage 跟 IOPS 各超 1.5-2x 預估。</p>
<p><strong>根因</strong>：</p>
<ul>
<li>Atlas backup 預設 <em>跨 region replicated</em>、storage cost 2x</li>
<li>IOPS-heavy workload 在 M tier 內可能撞 burst credit、auto-tier-up 暫時觸發更貴 tier</li>
<li>Data transfer 跨 region / 跨 cloud 計費沒算</li>
</ul>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-migration cost estimate</strong>：用 self-managed metrics 估 IOPS / bandwidth、套 Atlas pricing</li>
<li><strong>Backup region 設單一</strong>：若不要跨 region DR、設 same-region backup 省 50%</li>
<li><strong>Reserved Instance</strong>：穩定 workload 預付 1-3 年、省 30-40%</li>
<li><strong>Performance Advisor 早用</strong>：第一週就跑、找 inefficient query 降 IOPS</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Self-managed MongoDB</th>
          <th>Atlas</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cluster cost (M80)</td>
          <td>EC2 r6g.4xlarge × 3 ≈ $1.5K / mo</td>
          <td>M80 + storage + backup ≈ $3K / mo</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.5-1.5 FTE</td>
          <td>0.1-0.3 FTE</td>
      </tr>
      <tr>
          <td>Backup cost</td>
          <td>S3 + tooling 自管</td>
          <td>內建 + tiered storage</td>
      </tr>
      <tr>
          <td>Cross-region DR cost</td>
          <td>Manual + 2x infrastructure</td>
          <td>1-click + 1.5-2x billing</td>
      </tr>
      <tr>
          <td>Time to value</td>
          <td>1-3 個月（HA + ops setup）</td>
          <td>1-2 週（cluster ready + IAM）</td>
      </tr>
      <tr>
          <td>Migration cost</td>
          <td>-</td>
          <td>1-3 FTE × 2-3 個月</td>
      </tr>
  </tbody>
</table>
<p><strong>Break-even</strong>：~200GB / 中型 workload、Atlas operational savings 平攤 1-2 年後比 self-managed cheaper；TB+ 大型 workload self-managed 仍可能便宜、但需要 ops team。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-postgresql--aurora-migration-對照">跟 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora migration</a> 對照</h3>
<p>兩篇都是 Type C operational redesign hybrid、模板共用、細節差：</p>
<ul>
<li>Aurora 端 RDS Proxy 是推薦做法、Atlas 端 Private Endpoint 更標準</li>
<li>Aurora 端 IAM authentication 是 <em>optional best practice</em>、Atlas IAM 是 <em>推薦預設</em></li>
<li>兩家 cost model 都複雜、I/O cost 是 surprise 主要來源</li>
</ul>
<h3 id="跟-application-端-iam-token-rotation-整合">跟 <a href="/blog/backend/07-security-data-protection/vendors/hashicorp-vault/dynamic-credential/" data-link-title="HashiCorp Vault Dynamic Credential：lease 治理跟 application 整合的實作層" data-link-desc="Vault database secrets engine 怎麼配、application 怎麼 renew lease、production 五大踩雷（lease 過期 race、DB max_connections 撞牆、Vault sealed、token expire、scope 過寬）、容量規劃跟 vault-agent injector 整合">Application 端 IAM token rotation</a> 整合</h3>
<p>Vault dynamic credential 可 issue Atlas Database User credential、lease lifecycle 對齊 application；對 high-stakes workload 是好做法、但 setup 複雜。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Atlas Data Federation</strong>：跨 Atlas 集群 query S3 / 跨 region；如果走 multi-region 評估這 feature</li>
<li><strong>Atlas Online Archive</strong>：cold data 自動 archive 到 S3、查 query 透明；對 retention 重的 workload 省 storage cost</li>
<li><strong>Atlas Serverless</strong>：burst workload 適合、steady 不划算</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB</a></li>
<li>平行 migration playbook (Type C)：<a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a></li>
<li>平行 migration playbook：<a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic</a>（Type A schema 差） / <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a>（Type E paradigm shift）</li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a>（本文驗證 Type C 標準形態）</li>
</ul>
]]></content:encoded></item><item><title>MongoDB Shard Expansion + Multi-DC：Type F「不需要 parallel run」的 multi-region 例外</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/shard-expansion-multi-dc/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/shard-expansion-multi-dc/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB&lt;/a> overview 的 implementation-layer deep article。對應 &lt;a href="https://tarrragon.github.io/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 Type F「Topology re-layout」&lt;/a> 第 3 個 dogfood、特別驗證 self-aware limitation 第 3 點「不需要 parallel run」claim 的 &lt;em>multi-region rollout 例外&lt;/em> — 本文是反例的具體實證。&lt;/p>&lt;/blockquote>
&lt;h2 id="reviewer-d-的質疑type-f-一定不需要-parallel-run-嗎">Reviewer D 的質疑：Type F 一定不需要 parallel run 嗎&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 Self-aware limitation&lt;/a> 第 3 點承認：&lt;/p>
&lt;blockquote>
&lt;p>「不需要 parallel run」claim 部分不成立：multi-region rollout（#128 列為 Type F 情境）必須 parallel run — 兩 region 同時跑然後切流量、不然就是停機切換、跟 Type A phase 3 機制相同。&lt;/p>&lt;/blockquote>
&lt;p>本文是該 claim 的 &lt;em>正面實證&lt;/em> — MongoDB sharded cluster 從 single-DC 加 shard + 加 secondary DC、確實需要 parallel run + 流量切換、跟 Type A phased migration 局部同構：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB</a> overview 的 implementation-layer deep article。對應 <a href="/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 Type F「Topology re-layout」</a> 第 3 個 dogfood、特別驗證 self-aware limitation 第 3 點「不需要 parallel run」claim 的 <em>multi-region rollout 例外</em> — 本文是反例的具體實證。</p></blockquote>
<h2 id="reviewer-d-的質疑type-f-一定不需要-parallel-run-嗎">Reviewer D 的質疑：Type F 一定不需要 parallel run 嗎</h2>
<p><a href="/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 Self-aware limitation</a> 第 3 點承認：</p>
<blockquote>
<p>「不需要 parallel run」claim 部分不成立：multi-region rollout（#128 列為 Type F 情境）必須 parallel run — 兩 region 同時跑然後切流量、不然就是停機切換、跟 Type A phase 3 機制相同。</p></blockquote>
<p>本文是該 claim 的 <em>正面實證</em> — MongoDB sharded cluster 從 single-DC 加 shard + 加 secondary DC、確實需要 parallel run + 流量切換、跟 Type A phased migration 局部同構：</p>
<table>
  <thead>
      <tr>
          <th>Type F 假設</th>
          <th>Single-DC re-sharding（Redis case）</th>
          <th><strong>Multi-DC expansion（本文）</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同 cluster 不同 state</td>
          <td>yes</td>
          <td>yes（同 MongoDB cluster）</td>
      </tr>
      <tr>
          <td>不需 schema translation</td>
          <td>yes</td>
          <td>yes</td>
      </tr>
      <tr>
          <td>不需 parallel run</td>
          <td>yes（slot migration 內部完成）</td>
          <td><strong>no — 兩 DC 同跑後切流量</strong></td>
      </tr>
      <tr>
          <td>不需 cleanup phase</td>
          <td>yes</td>
          <td>partial（舊 DC 角色降為 standby）</td>
      </tr>
      <tr>
          <td>Step-by-step + rollback boundary</td>
          <td>yes</td>
          <td>yes</td>
      </tr>
  </tbody>
</table>
<p>→ Type F anatomy 仍適用、但「不需 parallel run」是 <em>子情境條件</em>、不是 universal claim。</p>
<h2 id="兩個操作合併shard-加--dc-加">兩個操作合併：shard 加 + DC 加</h2>
<p>實務上中型公司常 <em>同時</em> 跑兩個 topology 變動：</p>
<ol>
<li><strong>Shard expansion</strong>：現有 3-shard cluster 加到 5-shard、chunk migration 平均分佈</li>
<li><strong>Multi-DC</strong>：從 single-DC（us-east-1）加到 multi-DC（us-east-1 + us-west-2）</li>
</ol>
<p>兩個操作的 <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Shard 加（單獨）</th>
          <th>Multi-DC（單獨）</th>
          <th>兩者同跑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>Low</td>
          <td>Low</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>Low</td>
          <td>Medium（跨 DC ops）</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Low</td>
          <td>Low</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Low（加 shard、同 cluster）</td>
          <td>Low</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>Low</td>
          <td>Low-Medium（cross-DC latency aware）</td>
          <td>Low-Medium</td>
      </tr>
      <tr>
          <td><strong>Data topology</strong></td>
          <td><strong>High</strong>（sharding strategy）</td>
          <td><strong>High</strong>（replication + region）</td>
          <td><strong>High</strong>（雙變、複合 topology）</td>
      </tr>
  </tbody>
</table>
<p>兩者主導維度都是 topology = High、組合走 Type F multi-axis 子情境。</p>
<h2 id="pre-layout-analysis當前--目標-topology">Pre-layout analysis：當前 + 目標 topology</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 1. 當前 shard 分佈
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">sh</span><span class="p">.</span><span class="nx">status</span><span class="p">({</span><span class="nx">verbose</span><span class="o">:</span> <span class="kc">false</span><span class="p">});</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">// 期望輸出: 3 shard、每個 ~33% chunks、no migration in progress
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">printShardingStatus</span><span class="p">({</span><span class="nx">verbose</span><span class="o">:</span> <span class="kc">false</span><span class="p">});</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">// 找 hot shard、imbalanced chunk distribution
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">// 2. Replication topology
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="nx">rs</span><span class="p">.</span><span class="nx">status</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">// 各 replica set primary/secondary 健康度、replication lag
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1">// 3. Cross-DC network baseline (在 add DC 前測)
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1">// us-east-1 → us-west-2 RTT、bandwidth
</span></span></span></code></pre></div><p>Pre-layout 階段 output：</p>
<ul>
<li><strong>當前</strong>：3 shard × 1 replica set per shard (3 member) = 9 node、全在 us-east-1</li>
<li><strong>目標</strong>：5 shard × 1 replica set per shard (5 member: 3 us-east + 2 us-west) = 25 node</li>
<li><strong>Migration scope</strong>：加 2 shard + 加 2 DC member 每 shard、共 +16 node</li>
<li><strong>Chunk migration estimate</strong>：30% chunk 需重分（從 33% × 3 變 20% × 5）</li>
</ul>
<h2 id="re-layout-機制">Re-layout 機制</h2>
<p>兩個 mechanism 平行進行：</p>
<h3 id="shard-expansion-mechanism">Shard expansion mechanism</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 1. 新增 shard 到 cluster
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">sh</span><span class="p">.</span><span class="nx">addShard</span><span class="p">(</span><span class="s2">&#34;rs-shard4/host10:27017,host11:27017,host12:27017&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nx">sh</span><span class="p">.</span><span class="nx">addShard</span><span class="p">(</span><span class="s2">&#34;rs-shard5/host13:27017,host14:27017,host15:27017&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">// 2. balancer 自動 chunk migration
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="nx">sh</span><span class="p">.</span><span class="nx">startBalancer</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">// 觀察 progress: db.adminCommand({balancerStatus: 1})
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">// 3. 完成後 verify shard distribution
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="nx">sh</span><span class="p">.</span><span class="nx">status</span><span class="p">();</span></span></span></code></pre></div><p>Chunk migration 是 <em>background</em> job、balancer 控制 throttle；不阻塞 production query、但 CPU / network 上升 30-50%。</p>
<h3 id="multi-dc-expansion-mechanism">Multi-DC expansion mechanism</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 1. 對每 shard 的 replica set 加 us-west-2 member (priority 0)
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">rs</span><span class="p">.</span><span class="nx">add</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">host</span><span class="o">:</span> <span class="s2">&#34;us-west-2-host:27017&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nx">priority</span><span class="o">:</span> <span class="mi">0</span><span class="p">,</span>           <span class="c1">// 不能當 primary
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>  <span class="nx">votes</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>              <span class="c1">// 參與投票
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>  <span class="nx">hidden</span><span class="o">:</span> <span class="kc">false</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">// 2. 等 initial sync 完成（依資料量 1 小時 - 1 天）
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="nx">rs</span><span class="p">.</span><span class="nx">printReplicationInfo</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1">// 3. 確認 secondary 健康後、提升 priority 或 votes
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1">// 不要立刻設 priority 1、避免 unintended failover
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1">// 4. Cross-DC routing 透過 readPreference 在 application 設
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MongoClient</span><span class="p">(</span><span class="nx">uri</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="nx">readPreference</span><span class="o">:</span> <span class="s1">&#39;secondaryPreferred&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="nx">readPreferenceTags</span><span class="o">:</span> <span class="p">[{</span> <span class="nx">region</span><span class="o">:</span> <span class="s1">&#39;us-west-2&#39;</span> <span class="p">},</span> <span class="p">{}],</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>關鍵：multi-DC 是 <em>漸進加 member</em>、不是 atomic switch；每 shard 獨立加、整體耗時 = shard 數 × initial sync time。</p>
<h2 id="execution-flow含-parallel-run--流量切換">Execution flow（含 parallel run + 流量切換）</h2>
<p>8 step、包含 <em>parallel run + 切流量</em> 段——驗證 <a href="/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 self-aware limitation</a> 第 3 點：</p>
<table>
  <thead>
      <tr>
          <th>Step</th>
          <th>動作</th>
          <th>Parallel run?</th>
          <th>Rollback boundary</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1 Pre-check</td>
          <td>量化當前 topology、確認 cluster 健康</td>
          <td>no</td>
          <td>-</td>
      </tr>
      <tr>
          <td>2 加 us-east shard</td>
          <td>sh.addShard、balancer migrate chunk</td>
          <td>no（cluster 內）</td>
          <td>removeShard、chunk migrate 回</td>
      </tr>
      <tr>
          <td>3 加 us-west member</td>
          <td>對每 shard rs.add 跨 DC member</td>
          <td>no</td>
          <td>rs.remove、initial sync 投入廢棄</td>
      </tr>
      <tr>
          <td>4 <strong>Initial sync wait</strong></td>
          <td>等所有 us-west member catch up</td>
          <td><strong>parallel run starts</strong>：兩 DC 同時 serve</td>
          <td>-</td>
      </tr>
      <tr>
          <td>5 <strong>Cross-DC dual-serve</strong></td>
          <td>兩 DC 都跑 read traffic（不切 write）</td>
          <td><strong>yes、parallel run</strong>：app 用 secondary preferred us-west</td>
          <td>readPref 切回 us-east primary</td>
      </tr>
      <tr>
          <td>6 <strong>流量切換</strong></td>
          <td>application us-west traffic 走 us-west read</td>
          <td><strong>yes</strong></td>
          <td>DNS / readPref 切回</td>
      </tr>
      <tr>
          <td>7 Promote us-west（optional）</td>
          <td>一個 shard 的 us-west member priority 提到 1</td>
          <td>post-cutover</td>
          <td>demote priority 回 0</td>
      </tr>
      <tr>
          <td>8 Cleanup</td>
          <td>Verify、archive log、document new topology</td>
          <td>no</td>
          <td>-</td>
      </tr>
  </tbody>
</table>
<p>Step 4-6 是 <em>parallel run + 切流量</em> — <strong>Type F 有此例外、跟 Type A phase 3 機制同構</strong>；anatomy 中「Execution flow per-step」段必須含 parallel run 子段。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1balancer-跑-chunk-migration-撞-production-peak">Case 1：Balancer 跑 chunk migration 撞 production peak</h3>
<p><strong>徵兆</strong>：加 shard 後 balancer 開始 migrate chunk、production write latency p99 從 10ms 跳到 100ms；application 端 timeout 大量。</p>
<p><strong>根因</strong>：MongoDB balancer 預設 24×7 跑、chunk migrate 是 <em>blocking</em> 操作（migration lock 期間阻塞 write 到該 chunk）；產線高峰時間 balancer 不會自動暫停。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 限 balancer 跑在 low-traffic window
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">sh</span><span class="p">.</span><span class="nx">setBalancerState</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">settings</span><span class="p">.</span><span class="nx">update</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="p">{</span> <span class="nx">_id</span><span class="o">:</span> <span class="s2">&#34;balancer&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="p">{</span> <span class="nx">$set</span><span class="o">:</span> <span class="p">{</span> <span class="nx">activeWindow</span><span class="o">:</span> <span class="p">{</span> <span class="nx">start</span><span class="o">:</span> <span class="s2">&#34;02:00&#34;</span><span class="p">,</span> <span class="nx">stop</span><span class="o">:</span> <span class="s2">&#34;06:00&#34;</span> <span class="p">}</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="p">{</span> <span class="nx">upsert</span><span class="o">:</span> <span class="kc">true</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">);</span></span></span></code></pre></div><p>且設 <code>chunkSize</code> 較小（128MB → 64MB）讓 migration 步驟細、單次 lock 時間短。</p>
<h3 id="case-2cross-dc-initial-sync-期間-oplog-跑出窗口">Case 2：Cross-DC initial sync 期間 oplog 跑出窗口</h3>
<p><strong>徵兆</strong>：加 us-west member 後、initial sync 跑 4 小時、結束時 member 顯示「too stale to catch up」、需要 full re-sync。</p>
<p><strong>根因</strong>：MongoDB oplog 是 capped collection、預設 size 5% disk；4 小時 initial sync 期間 primary 寫入量超出 oplog 保留範圍、member 拿到的 oplog start point 已被覆蓋。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>預先擴 oplog size</strong>：<code>db.adminCommand({replSetResizeOplog: 1, size: 51200})</code> 加到 50GB、覆蓋 sync window</li>
<li><strong>Off-peak initial sync</strong>：跑在低流量時間、oplog 寫入較慢</li>
<li><strong>Manual initial sync via snapshot</strong>：用 <code>mongodump</code> 從 primary snapshot、restore 到 new member、跳過 oplog tail catch-up</li>
</ol>
<h3 id="case-3跨-dc-read-路由錯誤stale-data-影響業務">Case 3：跨 DC read 路由錯誤、stale data 影響業務</h3>
<p><strong>徵兆</strong>：切流量到 us-west 後、application 偶爾抓到 5-30 秒前的 stale data；customer 報告「明明剛改了 setting、refresh 又變回去」。</p>
<p><strong>根因</strong>：us-west member 是 secondary、replication lag 5-30 秒；application readPreference 設 <code>secondaryPreferred</code> 但沒 <code>maxStalenessSeconds</code>、可能讀到嚴重 stale member。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kr">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MongoClient</span><span class="p">(</span><span class="nx">uri</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nx">readPreference</span><span class="o">:</span> <span class="s1">&#39;secondaryPreferred&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">readPreferenceTags</span><span class="o">:</span> <span class="p">[{</span> <span class="nx">region</span><span class="o">:</span> <span class="s1">&#39;us-west-2&#39;</span> <span class="p">},</span> <span class="p">{}],</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nx">maxStalenessSeconds</span><span class="o">:</span> <span class="mi">90</span><span class="p">,</span>  <span class="c1">// 限 stale 不超過 90 秒
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="p">});</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">// 對 strict consistency 場景強制 primary
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">client_strict</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MongoClient</span><span class="p">(</span><span class="nx">uri</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="nx">readPreference</span><span class="o">:</span> <span class="s1">&#39;primary&#39;</span><span class="p">,</span>  <span class="c1">// 強制讀 us-east primary
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p>Application-level read pattern 必須區分「accept stale read」vs「require fresh read」、不是 cluster-level 統一配置。</p>
<h3 id="case-4shard-tag-aware-routing-沒設cross-dc-traffic-爆-cost">Case 4：Shard tag-aware routing 沒設、cross-DC traffic 爆 cost</h3>
<p><strong>徵兆</strong>：multi-DC 跑了 1 個月、AWS egress cost 從 $500 / month 漲到 $8000 / month；99% 流量還是 us-east → us-west 跨 DC。</p>
<p><strong>根因</strong>：sharded cluster 沒設 <em>zone sharding</em>、application 不知道哪些 chunk 在哪個 DC、所有 query 預設打 us-east primary、跨 DC bandwidth 爆。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 注意: MongoDB 4.2+ API、舊版 sh.addShardTag / sh.addTagRange 已 deprecated
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">// 對應改 sh.addShardToZone / sh.updateZoneKeyRange
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">// 1. 給 shard 加 zone (MongoDB 4.2+)
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="nx">sh</span><span class="p">.</span><span class="nx">addShardToZone</span><span class="p">(</span><span class="s2">&#34;rs-shard1&#34;</span><span class="p">,</span> <span class="s2">&#34;us-east&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nx">sh</span><span class="p">.</span><span class="nx">addShardToZone</span><span class="p">(</span><span class="s2">&#34;rs-shard2&#34;</span><span class="p">,</span> <span class="s2">&#34;us-east&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nx">sh</span><span class="p">.</span><span class="nx">addShardToZone</span><span class="p">(</span><span class="s2">&#34;rs-shard3&#34;</span><span class="p">,</span> <span class="s2">&#34;us-east&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nx">sh</span><span class="p">.</span><span class="nx">addShardToZone</span><span class="p">(</span><span class="s2">&#34;rs-shard4&#34;</span><span class="p">,</span> <span class="s2">&#34;us-west&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nx">sh</span><span class="p">.</span><span class="nx">addShardToZone</span><span class="p">(</span><span class="s2">&#34;rs-shard5&#34;</span><span class="p">,</span> <span class="s2">&#34;us-west&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">// 2. 對 collection 加 zone range
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="nx">sh</span><span class="p">.</span><span class="nx">updateZoneKeyRange</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="s2">&#34;myapp.events&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="p">{</span> <span class="nx">region</span><span class="o">:</span> <span class="s2">&#34;us-east&#34;</span><span class="p">,</span> <span class="nx">_id</span><span class="o">:</span> <span class="nx">MinKey</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="p">{</span> <span class="nx">region</span><span class="o">:</span> <span class="s2">&#34;us-east&#34;</span><span class="p">,</span> <span class="nx">_id</span><span class="o">:</span> <span class="nx">MaxKey</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="s2">&#34;us-east&#34;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">);</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="nx">sh</span><span class="p">.</span><span class="nx">updateZoneKeyRange</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="s2">&#34;myapp.events&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="p">{</span> <span class="nx">region</span><span class="o">:</span> <span class="s2">&#34;us-west&#34;</span><span class="p">,</span> <span class="nx">_id</span><span class="o">:</span> <span class="nx">MinKey</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="p">{</span> <span class="nx">region</span><span class="o">:</span> <span class="s2">&#34;us-west&#34;</span><span class="p">,</span> <span class="nx">_id</span><span class="o">:</span> <span class="nx">MaxKey</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="s2">&#34;us-west&#34;</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="p">);</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="c1">// 3. balancer 重新分配 chunk 到對應 zone
</span></span></span></code></pre></div><p>Zone sharding 是 multi-DC 必要設計、不設等於白付 egress cost。</p>
<h3 id="case-5failover-後跨-dc-primary-切換application-連線中斷">Case 5：Failover 後跨 DC primary 切換、application 連線中斷</h3>
<p><strong>徵兆</strong>：production 跑 6 個月後、us-east-1 outage、某 shard primary 切到 us-west member；application 5-10 秒內大量 connection error。</p>
<p><strong>根因</strong>：MongoDB driver 預設 election timeout 10 秒、application 沒設 server selection retry；primary 切換期間 client 沒重連。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MongoClient</span><span class="p">(</span><span class="nx">uri</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">serverSelectionTimeoutMS</span><span class="o">:</span> <span class="mi">30000</span><span class="p">,</span>    <span class="c1">// 等 30 秒給 election
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="nx">retryWrites</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">retryReads</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nx">heartbeatFrequencyMS</span><span class="o">:</span> <span class="mi">5000</span><span class="p">,</span>         <span class="c1">// 更頻繁 detect topology 變動
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p>且 multi-DC primary 應該設 <em>priority asymmetry</em>：us-east member priority 2、us-west priority 1；正常情況不切換、災難時自動切。</p>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Single-DC 3-shard</th>
          <th>Multi-DC 5-shard</th>
          <th>Trade-off</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Node count</td>
          <td>9</td>
          <td>25</td>
          <td>~3x infrastructure cost</td>
      </tr>
      <tr>
          <td>Storage redundancy</td>
          <td>3 replica</td>
          <td>5 replica (3 east + 2 west)</td>
          <td>+2 copy、storage cost +66%</td>
      </tr>
      <tr>
          <td>Network egress</td>
          <td>內部 VPC、低</td>
          <td>Cross-DC、高（需 zone sharding）</td>
          <td>$500 → $8000 / month if no zone sharding</td>
      </tr>
      <tr>
          <td>Latency p99 (write)</td>
          <td>5-10ms</td>
          <td>5-15ms（primary 仍 us-east）</td>
          <td>略升</td>
      </tr>
      <tr>
          <td>Latency p99 (read)</td>
          <td>5-10ms</td>
          <td>2-5ms (local DC)</td>
          <td>Multi-DC 區域 read 加快</td>
      </tr>
      <tr>
          <td>Disaster recovery</td>
          <td>RTO 30 分鐘（rebuild）</td>
          <td>RTO &lt; 1 分鐘（auto failover）</td>
          <td>顯著改善</td>
      </tr>
      <tr>
          <td>Operational complexity</td>
          <td>低</td>
          <td>高（zone sharding / DR drill）</td>
          <td>+1 SRE FTE 維護</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：multi-DC 是 <em>DR 投資</em>、不是 cost optimization；只在 <em>availability SLA &gt; 99.9% 或合規要求</em> 場景值得。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-mongodb--atlas-migration-對位">跟 <a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">MongoDB → Atlas migration</a> 對位</h3>
<p>Self-managed multi-DC 複雜度高、Atlas 把 multi-cluster + cross-region 簡化成 UI 配置；如果走 multi-DC、考慮直接遷 Atlas。</p>
<h3 id="跟-application-read-pattern-整合">跟 Application read pattern 整合</h3>
<p>zone sharding + readPreference 跟 application logic 緊密耦合；不能事後補、應在 multi-DC 設計階段就設計 application 端的 region-aware routing。</p>
<h3 id="跟-cassandra-keyspace-re-balance-對比">跟 <a href="https://cassandra.apache.org/">Cassandra keyspace re-balance</a> 對比</h3>
<p>Cassandra 是另一個 Type F multi-DC 典型 case；用 <em>NetworkTopologyStrategy + replication factor per DC</em>、跟 MongoDB zone sharding 概念對等但 mechanism 完全不同。Reviewer D 把 Cassandra 列為 Type F 反例 — 本文以 MongoDB 替代驗證。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Cross-region active-active</strong>：MongoDB 不支援 multi-primary、cross-region active-active 需要 application-level conflict resolution</li>
<li><strong>PostgreSQL Citus / CockroachDB multi-region</strong> 對比：distributed SQL 對 multi-region 有不同設計</li>
<li><strong>Cost optimization</strong>：跨 DC egress 是 long-term concern、zone sharding 設好後仍要 quarterly review</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB</a></li>
<li>平行 migration playbook：<a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">MongoDB → Atlas</a></li>
<li>平行 Type F dogfood：<a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster Re-sharding</a>（dogfood #1）/ <a href="/blog/backend/01-database/vendors/postgresql/partition-redesign/" data-link-title="PostgreSQL Partition Redesign：當 monthly partition 越跑越慢" data-link-desc="PostgreSQL partition redesign 是 Type F「topology re-layout」第 2 個 dogfood — 從 monthly partition 改 daily / 從 range 改 list / 從單軸改 sub-partition；6 維 audit 皆 Low &#43; topology 軸 High；涵蓋 partition 不平衡偵測、ATTACH/DETACH 線上重劃、5 個 production 踩雷、跟 partition_pruning &#43; autovacuum 整合">PostgreSQL Partition Redesign</a>（dogfood #2）</li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> / <a href="/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 Data topology 是第 6 audit 維度</a>（本文驗證 self-aware limitation 第 3 點）</li>
</ul>
]]></content:encoded></item><item><title>MongoDB Schema Design Pattern：contract layer 在哪 vs embedded / reference</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/schema-design-pattern/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/schema-design-pattern/</guid><description>&lt;p>MongoDB schema design 的初學討論常停在「embedded vs reference 二選一」。真實 production 議題遠不止此：document model 給的 schema flexibility 在第一年是紅利、跑半年後同 collection 開始混三代 schema、application code 三層 if-else 處理欄位缺失與型別漂移。這時候讀者要解的不是「embed 還是 reference」、是 &lt;strong>schema contract 該由誰守、守在哪一層&lt;/strong>。本文把這個議題拆成三條 contract layer 路徑（DB-layer validator / app-layer abstraction / 混合）、配合 embedded / reference / polymorphic 機制與 time-series collection 邊界一起討論。&lt;/p>
&lt;p>本文不重複 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview&lt;/a> 已寫過的 document model 適用條件 — 而是 production 部署 + schema governance + 失敗修復 的實作層教學。&lt;/p>
&lt;h2 id="問題情境document-自由的後座力">問題情境：document 自由的後座力&lt;/h2>
&lt;p>MongoDB 適用度的前置判讀有三件事要確認：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>document shape 是否主導資料&lt;/strong>：sensor signal / CMS article / order aggregate 這類「形狀本來就多型 + 隨產品演進」適合 document model；access pattern 固定 + 欄位定型的反而該回 KV 系統或 SQL&lt;/li>
&lt;li>&lt;strong>contract layer 該放哪&lt;/strong>：DB-layer validator 適合 schema 穩定 / 跨服務共用 collection 的場景；app-layer abstraction 適合 schema 演進快 / 微服務獨立 owner；混合適合大型 production&lt;/li>
&lt;li>&lt;strong>跨雲 hedging 是否需要&lt;/strong>：若團隊未來雲商策略不確定、Atlas 跨雲是 selection 訊號；只在單雲跑就不必為 hedging 多付代價&lt;/li>
&lt;/ul>
&lt;p>確認 MongoDB 該用之後，讀者真正在 production 撞到的徵兆：&lt;/p>
&lt;ul>
&lt;li>Document model 早期 schema-less 紅利、跑半年後 collection 同時混三代 schema、application 寫 if-else 處理欄位缺失與型別漂移&lt;/li>
&lt;li>子文件越塞越深、單 document 突破 1-2MB、partial update 仍要把整顆 document load + write、IO 跟 working set 雙重壓力&lt;/li>
&lt;li>反向過度 normalize：訂單跟訂單 item 拆兩個 collection、單一查詢得 N+1 &lt;code>$lookup&lt;/code>、aggregation cost 飆&lt;/li>
&lt;li>IoT / sensor / event log workload 寫進 regular collection、寫入吞吐撞牆但沒考慮 time-series collection&lt;/li>
&lt;li>&lt;code>$lookup&lt;/code> 出現在 hot path、document size warning（16MB 上限預警）、partial update 卻產生大量 disk write、schema validation 報錯比例突然爬升&lt;/li>
&lt;/ul>
&lt;p>Case anchor：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected&lt;/a> 揭露車載 sensor schema 隨車型 / 年份 / 規範演進、polymorphic document 與 schema governance 並存；&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes&lt;/a> 揭露 CMS 50+ 微服務透過自建中介 abstraction layer 隔離 schema 變動；&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365&lt;/a> 揭露 document model 保留 + 跨 vendor 形狀治理。早期 startup MongoDB 三代 schema 並存的具體 incident 細節需未來 case 補完、本文先以「常見 failure pattern」處理。&lt;/p></description><content:encoded><![CDATA[<p>MongoDB schema design 的初學討論常停在「embedded vs reference 二選一」。真實 production 議題遠不止此：document model 給的 schema flexibility 在第一年是紅利、跑半年後同 collection 開始混三代 schema、application code 三層 if-else 處理欄位缺失與型別漂移。這時候讀者要解的不是「embed 還是 reference」、是 <strong>schema contract 該由誰守、守在哪一層</strong>。本文把這個議題拆成三條 contract layer 路徑（DB-layer validator / app-layer abstraction / 混合）、配合 embedded / reference / polymorphic 機制與 time-series collection 邊界一起討論。</p>
<p>本文不重複 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> 已寫過的 document model 適用條件 — 而是 production 部署 + schema governance + 失敗修復 的實作層教學。</p>
<h2 id="問題情境document-自由的後座力">問題情境：document 自由的後座力</h2>
<p>MongoDB 適用度的前置判讀有三件事要確認：</p>
<ul>
<li><strong>document shape 是否主導資料</strong>：sensor signal / CMS article / order aggregate 這類「形狀本來就多型 + 隨產品演進」適合 document model；access pattern 固定 + 欄位定型的反而該回 KV 系統或 SQL</li>
<li><strong>contract layer 該放哪</strong>：DB-layer validator 適合 schema 穩定 / 跨服務共用 collection 的場景；app-layer abstraction 適合 schema 演進快 / 微服務獨立 owner；混合適合大型 production</li>
<li><strong>跨雲 hedging 是否需要</strong>：若團隊未來雲商策略不確定、Atlas 跨雲是 selection 訊號；只在單雲跑就不必為 hedging 多付代價</li>
</ul>
<p>確認 MongoDB 該用之後，讀者真正在 production 撞到的徵兆：</p>
<ul>
<li>Document model 早期 schema-less 紅利、跑半年後 collection 同時混三代 schema、application 寫 if-else 處理欄位缺失與型別漂移</li>
<li>子文件越塞越深、單 document 突破 1-2MB、partial update 仍要把整顆 document load + write、IO 跟 working set 雙重壓力</li>
<li>反向過度 normalize：訂單跟訂單 item 拆兩個 collection、單一查詢得 N+1 <code>$lookup</code>、aggregation cost 飆</li>
<li>IoT / sensor / event log workload 寫進 regular collection、寫入吞吐撞牆但沒考慮 time-series collection</li>
<li><code>$lookup</code> 出現在 hot path、document size warning（16MB 上限預警）、partial update 卻產生大量 disk write、schema validation 報錯比例突然爬升</li>
</ul>
<p>Case anchor：<a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a> 揭露車載 sensor schema 隨車型 / 年份 / 規範演進、polymorphic document 與 schema governance 並存；<a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a> 揭露 CMS 50+ 微服務透過自建中介 abstraction layer 隔離 schema 變動；<a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> 揭露 document model 保留 + 跨 vendor 形狀治理。早期 startup MongoDB 三代 schema 並存的具體 incident 細節需未來 case 補完、本文先以「常見 failure pattern」處理。</p>
<h2 id="核心機制aggregate-rootembeddedreferencepolymorphic">核心機制：aggregate root、embedded、reference、polymorphic</h2>
<p>MongoDB schema design 的第一層是 <em>aggregate root 決定 atomicity 邊界</em>。MongoDB 把寫入 atomicity 限制在「單 document 內」、跨 document 要 multi-document transaction（5.0+ 在 replica set / sharded cluster 都支援、但跨 shard 有性能成本）。aggregate root 是 DDD 概念落地到 MongoDB 的具體實作 — 把「一起讀、一起寫、一致性邊界一致」的資料塞同一個 document。</p>
<ul>
<li><strong>Embedded（subdocument / array）</strong>：寫入 atomic、讀取一次到位；代價是 update sub-element 仍要 rewrite 整顆 document，sub-element 寫頻很高時不適合</li>
<li><strong>Reference（手動 <code>_id</code> foreign key + <code>$lookup</code>）</strong>：document 大小可控，但 join 在 application 或 aggregation 階段做；JOIN-heavy workload 跑這條路徑會 N+1</li>
<li><strong>Polymorphic pattern</strong>：同 collection 用 <code>type</code> discriminator 存多型實體；MongoDB 沒 inheritance、靠 schema validator 與 partial index 維持邊界</li>
<li><strong>16MB document hard limit</strong>：是 MongoDB 機制邊界；working set 在 RAM 的隱性軟限制（單 doc 大小直接影響 page cache 效率）更早就會出問題</li>
</ul>
<h3 id="contract-layer-三條路徑">Contract layer 三條路徑</h3>
<p>跨 case 合成 frame（本章合成、Toyota + Forbes 共同揭露）：document model 的 schema flexibility 在 production 必須以 schema governance 對沖、否則「schema 自由」變「production data inconsistency」（Toyota case 明示）。讀者要選的不是「要不要做 schema governance」、是「contract 守在哪一層」。三條路徑：</p>
<table>
  <thead>
      <tr>
          <th>路徑</th>
          <th>實作機制</th>
          <th>適用條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DB-layer contract</td>
          <td>MongoDB <code>$jsonSchema</code> validator + <code>validationLevel</code> + <code>validationAction</code></td>
          <td>Schema 穩定、多服務共用 collection、要 DB 擋髒資料</td>
      </tr>
      <tr>
          <td>App-layer contract</td>
          <td>自建 API abstraction + middleware schema 驗證</td>
          <td>Schema 演進快、微服務獨立 owner、跨雲彈性需求</td>
      </tr>
      <tr>
          <td>混合</td>
          <td>DB 層擋型別 / 必填、app 層擋業務語意 / 版本</td>
          <td>大型 production、多 owner、跨團隊</td>
      </tr>
  </tbody>
</table>
<p><strong>DB-layer 路徑</strong>：<code>$jsonSchema</code> validator 在 production 是「契約 enforcement」工具、不是 dev-time linter。設 <code>validationAction: &quot;error&quot;</code> 寫入直接擋；設 <code>&quot;warn&quot;</code> 只記 log。<code>validationLevel: &quot;moderate&quot;</code> 對既有 doc 放行、對新寫入嚴格；<code>&quot;strict&quot;</code> 對所有寫入都嚴格。適合 schema 穩定到「跨服務共用 collection」的程度。</p>
<p><strong>App-layer 路徑</strong>：9.C37 Forbes 揭露的模式 — 50+ 微服務透過自建中介 abstraction layer 看到穩定的 contract API、DB schema 變動限制在 owner microservice 內。Forbes 跨雲彈性能用起來、核心原因是 abstraction layer 把 schema 治理收斂到單點、跨雲遷移時 abstraction layer 不變、微服務不知道底層 DB 換 cluster 換雲。</p>
<p><strong>混合路徑</strong>：Atlas Application Services、enterprise schema registry 屬此類。DB 層 validator 守底線（欄位型別、必填欄位）、app 層 abstraction 守業務（版本欄位 / 相容處理 / cross-document 一致性）。代價是兩層都要維護、版本同步成本高、適合 production 規模真的撐住這個複雜度的團隊。</p>
<p>讀者選哪條路徑要看：team 規模 / collection 跨服務程度 / schema 演進速度。</p>
<h3 id="time-series-collection60">Time-series collection（6.0+）</h3>
<p>Time-series collection 是 MongoDB 為 IoT / sensor / event log / metrics 設計的 vendor-specific 機制 — 比 regular collection 寫入吞吐高 3-5x、storage 壓縮率更好。資料形狀必須是 <code>{ timestamp, metadata, measurement }</code> 三段式、timestamp 主導。</p>
<p>適用情境：sensor signal 高頻寫入、metrics 系統的 time series、application event log。<strong>不適用情境</strong>：schema 不以 timestamp 為主、需要跨 document update、需要 polymorphic discriminator。</p>
<p>9.C38 Toyota Connected 自承「20 個 Atlas database 沒明確說有沒有用 time series collection — 對 IoT 案例這是重要區分、但 case study 沒揭露」。寫進 production 時必須明示：IoT / sensor 場景該考慮 time-series collection、Toyota case 未揭露實際使用情況、不可寫成「Toyota 使用 time-series collection」。</p>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/document-store/" data-link-title="Document Store" data-link-desc="說明以 JSON 文件與彈性 schema 提供資料存取的模式，以及它仍需的治理邊界">document-store</a>、<a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction-boundary</a>（aggregate boundary = transaction boundary）、<a href="/blog/backend/knowledge-cards/data-inconsistency/" data-link-title="Data Inconsistency" data-link-desc="說明多份資料暫時不同步時如何判斷產品後果與修復責任">data-inconsistency</a>。</p>
<h2 id="操作流程">操作流程</h2>
<p><strong>Step 1：access pattern 盤點</strong>。列出 top 10 query / write、標 read together / write together 集合 — 這份清單決定 embedded vs reference vs polymorphic 的候選。</p>
<p><strong>Step 2：contract layer 決策</strong>。</p>
<table>
  <thead>
      <tr>
          <th>條件</th>
          <th>路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Collection 跨多服務 + schema 穩定</td>
          <td>DB-layer validator</td>
      </tr>
      <tr>
          <td>Schema 演進快 + 微服務獨立 owner</td>
          <td>App-layer abstraction</td>
      </tr>
      <tr>
          <td>大型 production + 多 owner + 跨團隊</td>
          <td>混合（兩者並用）</td>
      </tr>
      <tr>
          <td>IoT / sensor / event log + timestamp 主導</td>
          <td>Time-series collection（取代 regular collection）</td>
      </tr>
  </tbody>
</table>
<p><strong>Step 3：embed 判準</strong> — 1:few、life-cycle 同步、&lt; 1MB 預期上限；<strong>reference 判準</strong> — 1:many 寫頻不對稱、跨 aggregate 引用。</p>
<p><strong>Step 4：DB-layer 路徑 validator 配置</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">runCommand</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nx">collMod</span><span class="o">:</span> <span class="s2">&#34;orders&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">validator</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">$jsonSchema</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">      <span class="nx">bsonType</span><span class="o">:</span> <span class="s2">&#34;object&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      <span class="nx">required</span><span class="o">:</span> <span class="p">[</span><span class="s2">&#34;_id&#34;</span><span class="p">,</span> <span class="s2">&#34;tenantId&#34;</span><span class="p">,</span> <span class="s2">&#34;createdAt&#34;</span><span class="p">,</span> <span class="s2">&#34;items&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      <span class="nx">properties</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">tenantId</span><span class="o">:</span> <span class="p">{</span> <span class="nx">bsonType</span><span class="o">:</span> <span class="s2">&#34;string&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nx">createdAt</span><span class="o">:</span> <span class="p">{</span> <span class="nx">bsonType</span><span class="o">:</span> <span class="s2">&#34;date&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="nx">items</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">          <span class="nx">bsonType</span><span class="o">:</span> <span class="s2">&#34;array&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">          <span class="nx">minItems</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">          <span class="nx">items</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">            <span class="nx">bsonType</span><span class="o">:</span> <span class="s2">&#34;object&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">            <span class="nx">required</span><span class="o">:</span> <span class="p">[</span><span class="s2">&#34;sku&#34;</span><span class="p">,</span> <span class="s2">&#34;qty&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">            <span class="nx">properties</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">              <span class="nx">sku</span><span class="o">:</span> <span class="p">{</span> <span class="nx">bsonType</span><span class="o">:</span> <span class="s2">&#34;string&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">              <span class="nx">qty</span><span class="o">:</span> <span class="p">{</span> <span class="nx">bsonType</span><span class="o">:</span> <span class="s2">&#34;int&#34;</span><span class="p">,</span> <span class="nx">minimum</span><span class="o">:</span> <span class="mi">1</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">          <span class="p">}</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">  <span class="nx">validationLevel</span><span class="o">:</span> <span class="s2">&#34;moderate&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">  <span class="nx">validationAction</span><span class="o">:</span> <span class="s2">&#34;warn&#34;</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><p>灰度策略：先 <code>validationLevel: &quot;moderate&quot;</code> + <code>validationAction: &quot;warn&quot;</code> 觀察兩週、確認 application 不寫違規 doc、再切 <code>&quot;strict&quot;</code> + <code>&quot;error&quot;</code> 封死。</p>
<p><strong>Step 5：App-layer 路徑 abstraction 介面</strong>。9.C37 Forbes 揭露的模式 — middleware 攔截 microservice 寫入、驗 schema、套版本欄位、把 owner microservice 的 schema 變動隔離在 abstraction 內。</p>
<p><strong>Step 6：Polymorphic + partial index</strong> — <code>partialFilterExpression</code> 避免冷分支吃 index 成本：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">events</span><span class="p">.</span><span class="nx">createIndex</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="p">{</span> <span class="nx">type</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">timestamp</span><span class="o">:</span> <span class="o">-</span><span class="mi">1</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="p">{</span> <span class="nx">partialFilterExpression</span><span class="o">:</span> <span class="p">{</span> <span class="nx">type</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$in</span><span class="o">:</span> <span class="p">[</span><span class="s2">&#34;click&#34;</span><span class="p">,</span> <span class="s2">&#34;purchase&#34;</span><span class="p">]</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">)</span></span></span></code></pre></div><p><strong>Step 7：量測 doc 形狀</strong>。用 <code>bsondump</code> + <code>$bsonSize</code> + <code>collStats</code> 量測：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">coll</span><span class="p">.</span><span class="nx">aggregate</span><span class="p">([</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="p">{</span> <span class="nx">$group</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">      <span class="nx">_id</span><span class="o">:</span> <span class="kc">null</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">      <span class="nx">avg</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$avg</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$bsonSize</span><span class="o">:</span> <span class="s2">&#34;$$ROOT&#34;</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">      <span class="nx">max</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$max</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$bsonSize</span><span class="o">:</span> <span class="s2">&#34;$$ROOT&#34;</span> <span class="p">}</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="p">}}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">])</span></span></span></code></pre></div><p>驗證點：avgObjSize 在預期範圍、validator failure rate &lt; SLO、abstraction layer schema mismatch rate 可追溯。</p>
<p><strong>Rollback boundary</strong>：validator 從 <code>strict</code> 退回 <code>moderate</code> 是 single-command、application code 不必改；abstraction layer 換版需 application code 灰度；已 embed 進去的 schema 變更要靠 backfill migration script、無法 in-place 還原。</p>
<h2 id="失敗模式">失敗模式</h2>
<p><strong>Unbounded array growth</strong>：把「使用者所有訊息」embed 進 user document、document 撞 16MB → 寫入直接 reject。修法是改 reference、訊息獨立 collection、用 <code>userId</code> 索引。</p>
<p><strong>Hot subdocument update</strong>：所有寫都打同一個 nested field、wiredTiger document-level lock 退化成熱點，concurrency 看似多核卻被序列化。修法是把熱寫欄位拆 reference document、或改 sharded collection 把寫散開（見 <a href="../shard-key-selection/">shard key selection</a>）。</p>
<p><strong><code>$lookup</code> 在 hot path</strong>：reference 沒設好變 join、p99 latency 隨 collection 大小線性退化。修法是 schema design 階段 denormalize、把 read-together 資料 embed 回 aggregate root；或 <code>$merge</code> 寫 materialized view（見 <a href="../aggregation-pipeline-optimization/">aggregation pipeline optimization</a>）。</p>
<p><strong>Schema 三代並存（缺 contract layer）</strong>：缺 validator 跟 abstraction layer、舊版欄位殘留、application code 三層 fallback、新 dev onboarding 看不懂哪個欄位是現役。9.C38 Toyota 揭露：document model 的彈性「成本是 production 必須做 schema governance」、否則「schema 自由」變「production data inconsistency」。</p>
<p><strong>Abstraction layer 變成 lock-in</strong>：app-layer contract 寫得太重、跨 vendor 遷移時 abstraction 本身要重寫。該層應該薄、只做 schema 隔離、不做業務邏輯。</p>
<p><strong>Polymorphic 全表掃描</strong>：discriminator 沒進 index、<code>type: &quot;rare&quot;</code> 查詢全表 scan。修法用 partial index 把熱類型蓋住、冷類型走全表也只是冷路徑。</p>
<p><strong>Time-series collection 用錯場景</strong>：把非 timestamp 主導資料塞進 time-series collection、失去 flexibility 又拿不到吞吐紅利。Time-series collection 是專屬優化、不是普適 collection 升級。</p>
<p>Anti-recommendation：</p>
<ul>
<li>access pattern 還沒穩定的早期 MVP 不需要鎖死 schema validator；先用 app-layer abstraction、production 穩定後再決定 DB 層該不該封死</li>
<li>JOIN-heavy / 強 normalize workload 一開始就該回 PostgreSQL JSONB 或 SQL、不是塞進 MongoDB 再 <code>$lookup</code></li>
<li>跨案合成 frame：「不是所有資料都該進 MongoDB」、document-shaped + 形狀變化頻繁的進、access pattern 固定的 KV 走 KV（9.C36 Coinbase 揭露 MongoDB + DynamoDB 按 workload 分流）</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<p>關鍵 metric：</p>
<ul>
<li><strong>Document 形狀</strong>：<code>collStats.avgObjSize</code>、<code>collStats.size</code> vs <code>storageSize</code>（壓縮比）</li>
<li><strong>Contract 健康</strong>：document validation failure rate、abstraction layer schema mismatch rate</li>
<li><strong>Working set 壓力</strong>：<code>wiredTiger.cache.bytes currently in the cache</code> 對比 working set 估算</li>
<li><strong>Aggregation 副作用</strong>：profiler slow op、<code>$lookup</code> / <code>$unwind</code> 在 hot path 出現位置</li>
</ul>
<p>Mongo command：</p>
<ul>
<li><code>db.coll.stats()</code> 看 document 平均 / 最大 size、storage / index size</li>
<li><code>db.runCommand({collMod: ..., validator: ...})</code> 改 validator</li>
<li><code>db.setProfilingLevel(1, {slowms: 100})</code> 抓 slow op</li>
</ul>
<p>回到 <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</a>：把 doc size 分布、validator failure rate、abstraction layer schema mismatch、<code>$lookup</code> 出現位置列為 evidence 三件套。</p>
<p>回到 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 bottleneck localization</a>：working set 撐爆 RAM 時的 page fault 信號、跟 doc size 異常增長強相關。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<p>Sibling deep articles：</p>
<ul>
<li><a href="../shard-key-selection/">shard key selection</a> — document 形狀決定 shard key 候選空間</li>
<li><a href="../aggregation-pipeline-optimization/">aggregation pipeline optimization</a> — <code>$lookup</code> 與 schema reference 互相牽動</li>
<li><a href="../connection-management-and-cache-layer/">connection management and cache layer</a> — abstraction layer 跟 cache 層協作</li>
</ul>
<p>Migration playbook：</p>
<ul>
<li>document 形狀走樣到無法治理時的 <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">→ MongoDB → PostgreSQL 拆 normalize</a> 路徑</li>
<li>保留 document model 換 vendor 三型對照 — 保留主 DB 補周邊（Coinbase）/ 同 DB 換託管（Forbes Atlas）/ 同 model 換 vendor（<a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Microsoft 365 Cosmos DB MongoDB API</a>）</li>
</ul>
<p>跟 1.x 互引：<a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design</a> 處理通用 schema 演進原則、本文是 MongoDB-specific 落地；<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction boundary</a> 對齊 aggregate = atomic 邊界。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> — 本文是該頁尾「schema design pattern」backlog 的深度展開</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a> — polymorphic + governance</li>
<li><a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a> — abstraction layer 模式</li>
<li>官方：<a href="https://www.mongodb.com/docs/manual/core/data-modeling-introduction/">MongoDB Data Modeling</a>、<a href="https://www.mongodb.com/docs/manual/core/schema-validation/">Schema Validation</a>、<a href="https://www.mongodb.com/docs/manual/core/timeseries-collections/">Time Series Collections</a></li>
</ul>
]]></content:encoded></item><item><title>MongoDB Shard Key Selection：hashed vs ranged、單 cluster 切 shard vs 多 cluster 切 blast radius</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/shard-key-selection/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/shard-key-selection/</guid><description>&lt;p>MongoDB shard key 是 sharded cluster 上線時最難回頭的決策。Shard key 一旦設定錯、5.0 之前完全不可逆、5.0+ 用 &lt;code>reshardCollection&lt;/code> 可改但仍是長時間運算 + 額外磁碟 + 寫入暫停窗口。但 shard key 不是 production 唯一的橫向擴展選項 — 還有「多 cluster」這條路徑（Toyota Connected 揭露），兩者解的問題完全不同。本文把 shard key 三特性（cardinality / frequency / monotonicity）跟「單 cluster vs 多 cluster」對照在一起、配合跨 vendor partition key 可逆性紀律一起討論。&lt;/p>
&lt;p>本文不重複 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview&lt;/a> 已寫過的 sharding 簡介 — 而是 production 設計 + 失敗修復的實作層教學。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>MongoDB 適用度前置判讀&lt;/strong>：進到 shard key 設計前先確認 workload 在 MongoDB 適用區（document shape 主導 / contract layer 該放哪 / 跨雲 hedging 是否需要）— 詳見 &lt;a href="../schema-design-pattern/#%e5%95%8f%e9%a1%8c%e6%83%85%e5%a2%83document-%e8%87%aa%e7%94%b1%e7%9a%84%e5%be%8c%e5%ba%a7%e5%8a%9b">schema-design-pattern 開頭 3 軸前置判讀&lt;/a>、本篇不重複展開。Sharded cluster 是 &lt;em>已選 MongoDB 後&lt;/em> 的容量決策、不是 vendor 選型決策。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境橫向擴展不是只有-sharded-cluster-一條路">問題情境：橫向擴展不是只有 sharded cluster 一條路&lt;/h2>
&lt;p>典型觸發場景：single replica set 撐到上限、writes 已經把 primary 推到 CPU 90% / disk IO 飽和、working set 超出 RAM。讀者下意識會想到「分 shard」、但同時還有「分 cluster」這條路徑、兩者 trigger 完全不同：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>單 cluster 切 shard&lt;/strong>：解的是 &lt;em>單一資料域寫入飽和&lt;/em>、collection 大到單 replica set 撐不住&lt;/li>
&lt;li>&lt;strong>多 cluster 切 DB&lt;/strong>：解的是 &lt;em>&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> / ownership / 合規邊界&lt;/em>、不一定是吞吐問題&lt;/li>
&lt;/ul>
&lt;p>混淆兩者的後果：吞吐沒撞牆但 blast radius 是議題、強行分 shard → aggregation / transaction / &lt;code>$lookup&lt;/code> 成本全部跳一級、業務 ownership 仍混在一起。或反過來：吞吐撞牆但選了分 cluster → 跨 cluster transaction 不存在、單一 collection 跨多 cluster 要在 application 層拼。&lt;/p>
&lt;p>讀者徵兆：&lt;/p>
&lt;ul>
&lt;li>&lt;code>mongos&lt;/code> 的 &lt;code>targeted query / scatter-gather query&lt;/code> 比例失衡&lt;/li>
&lt;li>單一 shard CPU 遠高其他 shard、balancer 移 chunk 跟不上寫入速度&lt;/li>
&lt;li>&lt;code>chunkMigrated&lt;/code> 異常頻繁、&lt;code>sh.status()&lt;/code> 顯示 chunk 分布偏斜&lt;/li>
&lt;li>微服務 ownership 跟 collection 邊界不對齊、某 microservice 故障打到其他服務&lt;/li>
&lt;/ul>
&lt;p>Case anchor：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected&lt;/a> 揭露「20 個 Atlas database 是業務邊界切分、不是吞吐切分」（單 cluster vs 多 cluster 對照）；hot shard 在 e-commerce flash sale / 遊戲開新區 / B2B 大客戶獨佔 chunk 的具體 incident 細節需未來 case 補完、本文以「常見 failure pattern」處理、不憑空編造 incident 數字。&lt;/p></description><content:encoded><![CDATA[<p>MongoDB shard key 是 sharded cluster 上線時最難回頭的決策。Shard key 一旦設定錯、5.0 之前完全不可逆、5.0+ 用 <code>reshardCollection</code> 可改但仍是長時間運算 + 額外磁碟 + 寫入暫停窗口。但 shard key 不是 production 唯一的橫向擴展選項 — 還有「多 cluster」這條路徑（Toyota Connected 揭露），兩者解的問題完全不同。本文把 shard key 三特性（cardinality / frequency / monotonicity）跟「單 cluster vs 多 cluster」對照在一起、配合跨 vendor partition key 可逆性紀律一起討論。</p>
<p>本文不重複 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> 已寫過的 sharding 簡介 — 而是 production 設計 + 失敗修復的實作層教學。</p>
<blockquote>
<p><strong>MongoDB 適用度前置判讀</strong>：進到 shard key 設計前先確認 workload 在 MongoDB 適用區（document shape 主導 / contract layer 該放哪 / 跨雲 hedging 是否需要）— 詳見 <a href="../schema-design-pattern/#%e5%95%8f%e9%a1%8c%e6%83%85%e5%a2%83document-%e8%87%aa%e7%94%b1%e7%9a%84%e5%be%8c%e5%ba%a7%e5%8a%9b">schema-design-pattern 開頭 3 軸前置判讀</a>、本篇不重複展開。Sharded cluster 是 <em>已選 MongoDB 後</em> 的容量決策、不是 vendor 選型決策。</p></blockquote>
<h2 id="問題情境橫向擴展不是只有-sharded-cluster-一條路">問題情境：橫向擴展不是只有 sharded cluster 一條路</h2>
<p>典型觸發場景：single replica set 撐到上限、writes 已經把 primary 推到 CPU 90% / disk IO 飽和、working set 超出 RAM。讀者下意識會想到「分 shard」、但同時還有「分 cluster」這條路徑、兩者 trigger 完全不同：</p>
<ul>
<li><strong>單 cluster 切 shard</strong>：解的是 <em>單一資料域寫入飽和</em>、collection 大到單 replica set 撐不住</li>
<li><strong>多 cluster 切 DB</strong>：解的是 <em><a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> / ownership / 合規邊界</em>、不一定是吞吐問題</li>
</ul>
<p>混淆兩者的後果：吞吐沒撞牆但 blast radius 是議題、強行分 shard → aggregation / transaction / <code>$lookup</code> 成本全部跳一級、業務 ownership 仍混在一起。或反過來：吞吐撞牆但選了分 cluster → 跨 cluster transaction 不存在、單一 collection 跨多 cluster 要在 application 層拼。</p>
<p>讀者徵兆：</p>
<ul>
<li><code>mongos</code> 的 <code>targeted query / scatter-gather query</code> 比例失衡</li>
<li>單一 shard CPU 遠高其他 shard、balancer 移 chunk 跟不上寫入速度</li>
<li><code>chunkMigrated</code> 異常頻繁、<code>sh.status()</code> 顯示 chunk 分布偏斜</li>
<li>微服務 ownership 跟 collection 邊界不對齊、某 microservice 故障打到其他服務</li>
</ul>
<p>Case anchor：<a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a> 揭露「20 個 Atlas database 是業務邊界切分、不是吞吐切分」（單 cluster vs 多 cluster 對照）；hot shard 在 e-commerce flash sale / 遊戲開新區 / B2B 大客戶獨佔 chunk 的具體 incident 細節需未來 case 補完、本文以「常見 failure pattern」處理、不憑空編造 incident 數字。</p>
<h2 id="核心機制shard-keychunkbalancer">核心機制：shard key、chunk、balancer</h2>
<p>Shard key 三特性決定 sharded cluster 行為：</p>
<ul>
<li><strong>Cardinality（基數）</strong>：shard key 的不同值數量。<code>status: &quot;active&quot; | &quot;inactive&quot;</code> 只有兩個值、cardinality = 2、不能分到多 chunk</li>
<li><strong>Frequency（頻率分布）</strong>：值的分布是否平均。<code>country</code> 在全球流量中通常一兩個國家佔 80%</li>
<li><strong>Monotonicity（單調性）</strong>：值是否單調遞增。<code>_id</code>（ObjectId）/ 時間戳 / 自增 ID 都是單調</li>
</ul>
<p>三特性決定 shard key 行為：</p>
<ul>
<li><strong>Hashed shard key</strong>：hash function 把 key 打散、寫入分布均勻、但 range query 變 scatter-gather（每個 shard 都問）</li>
<li><strong>Ranged shard key</strong>：相同 key 相近 → 同 chunk → range query 高效；但單調 key + ranged → 所有寫打最後 chunk</li>
<li><strong>Compound shard key</strong>（5.0+ 是常用做法、對應 <a href="/blog/backend/knowledge-cards/composite-partition-key/" data-link-title="Composite Partition Key" data-link-desc="多欄位合成 partition key 把單一 logical hot key 拆成多個物理 shard、寫入分散讀取 fan-out">Composite Partition Key</a> 的 MongoDB 實作）：例如 <code>{ tenantId: 1, _id: &quot;hashed&quot; }</code> — 先 tenant 隔離、再 hash 避免 tenant 內熱點</li>
<li><strong>Zone sharding</strong>：把特定 chunk 釘到特定 shard（地域 / 合規 / 硬體分層）</li>
</ul>
<p>Chunk 是 MongoDB 在 collection 上劃出的 64MB（預設）邏輯區塊。Balancer 在 shard 間搬 chunk 達成均衡。<strong>Chunk 不可 split 的條件</strong>是 shard key 在該範圍只有一個值（low cardinality / 大 tenant 獨佔範圍）— chunk split 不了、balancer 也搬不開。</p>
<p><code>reshardCollection</code>（4.4+）：透過 temporary collection + chunk 重切 + 雙寫 + cutover、耗時等比於資料量、需額外 ~1.2x 磁碟。是「設計錯了還有補救機會」但不是 free lunch。</p>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/database-sharding/" data-link-title="Database Sharding" data-link-desc="說明資料庫如何依 shard key 分散資料、路由請求與承擔跨 shard 查詢成本">database-sharding</a>、<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot-partition</a>、<a href="/blog/backend/knowledge-cards/partition/" data-link-title="Partition" data-link-desc="說明事件流如何切分成多個可並行處理的有序片段">partition</a>。</p>
<h3 id="單-cluster-切-shard-vs-多-cluster-切-blast-radius">單 cluster 切 shard vs 多 cluster 切 blast radius</h3>
<p>跨案合成 frame（本章合成、9.C38 Toyota 揭露事實但 case 原文沒提這個 frame）：橫向擴展不是只有「sharded cluster 一條路」、多 cluster 是另一條路。</p>
<p>9.C38 Toyota Connected 揭露事實：</p>
<ul>
<li>18B transactions / 月 ÷ 30 天 ÷ 86400 秒 ≈ 7K txn/sec（口徑：月度滾動平均、非瞬時尖峰）</li>
<li>單一 MongoDB cluster 完全撐得下這個吞吐</li>
<li>Toyota 切 20 個 Atlas database <strong>不是吞吐切分</strong>、是 <em>microservice ownership</em> + <em>blast radius</em> 切分</li>
<li>「每個 microservice 擁有自己的 DB、單一 DB 故障不影響其他服務」</li>
</ul>
<p>兩條路徑的判讀條件不同：</p>
<table>
  <thead>
      <tr>
          <th>路徑</th>
          <th>Trigger</th>
          <th>代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Sharded cluster（分 shard）</td>
          <td>單一 collection 寫入飽和、storage 撐爆單 replica set、access pattern 在同一個資料域內</td>
          <td>aggregation / transaction / <code>$lookup</code> 成本全部跳一級</td>
      </tr>
      <tr>
          <td>多 cluster（分 DB）</td>
          <td>微服務 ownership 邊界、blast radius 隔離、合規 boundary、不同 workload shape 共處風險</td>
          <td>跨 cluster transaction 不存在、跨 DB join 必須在 application 層做</td>
      </tr>
  </tbody>
</table>
<p>兩者可以同時用：每個 microservice 有獨立 cluster、cluster 內部該分 shard 還是分。寫設計文件時要避免讓讀者以為「sharded cluster 是唯一橫向擴展選項」。</p>
<h3 id="partition-key-可逆性跨-vendor-對照">Partition key 可逆性跨 vendor 對照</h3>
<blockquote>
<p><strong>跨 vendor 可逆性對照 SSoT</strong>：MongoDB / DynamoDB / Cosmos DB 三家可逆性不在同一光譜、跨 vendor 對照的 SSoT 主寫位置在 <a href="/blog/backend/01-database/vendors/db3-vendor-selection/#%e4%b8%89-vendor-%e5%b0%8d%e6%af%94-10-%e8%bb%b8" data-link-title="DB3 Vendor Selection：document / KV / multi-model 三方選型 &#43; workload shape 前置判讀" data-link-desc="MongoDB / DynamoDB / Cosmos DB 三家 NoSQL 選型 entry point：workload shape × access pattern × consistency 三軸前置判讀、migration path 三型、federated DB 視角、三 vendor 對比 10 軸">DB3 entry — 三 vendor 對比 10 軸</a> + 對應的<a href="/blog/backend/01-database/vendors/db3-vendor-selection/#%e8%bb%b8%e7%9a%84%e5%bb%b6%e4%bc%b8%e5%ad%90%e6%ae%b5" data-link-title="DB3 Vendor Selection：document / KV / multi-model 三方選型 &#43; workload shape 前置判讀" data-link-desc="MongoDB / DynamoDB / Cosmos DB 三家 NoSQL 選型 entry point：workload shape × access pattern × consistency 三軸前置判讀、migration path 三型、federated DB 視角、三 vendor 對比 10 軸">軸的延伸子段</a>。本段聚焦 MongoDB 5.0+ <code>reshardCollection</code> 對 shard key 設計的影響、不重複展開三 vendor 全光譜比較。</p></blockquote>
<p>不同 vendor 對 partition key 可逆性紀律完全不在同一光譜：</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>機制</th>
          <th>可逆性</th>
          <th>成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>MongoDB</td>
          <td>Shard key（<code>shardCollection</code>）</td>
          <td>4.4+ <code>reshardCollection</code> 可改、5.0 前完全不可逆</td>
          <td>等比資料量、~1.2x 磁碟、雙寫 + cutover</td>
      </tr>
      <tr>
          <td>DynamoDB</td>
          <td>Partition key</td>
          <td>可改（用 backfill 到新 table）</td>
          <td>重設計 access pattern、流量切換成本</td>
      </tr>
      <tr>
          <td>Cosmos DB</td>
          <td>Partition key</td>
          <td>不可改（必須 export-recreate-import）</td>
          <td>全量重灌、雙寫驗證、最大遷移成本</td>
      </tr>
  </tbody>
</table>
<p>寫進設計文件時必須附 vendor + 版本、避免讓讀者把三家當「partition key 都不可改」、也避免把 MongoDB 5.0+ 的 <code>reshardCollection</code> 當「便宜遷移」。</p>
<h2 id="操作流程">操作流程</h2>
<p><strong>Step 1：横向擴展路徑決策</strong>。先問「我要解的是 <em>單一資料域寫入飽和</em> 還是 <em>blast radius / ownership</em>」、選分 shard 或分 cluster。若兩者都要、決定 cluster 邊界後再在 cluster 內分 shard。</p>
<p><strong>Step 2：access pattern audit</strong>。列出所有讀寫 query、標出哪些 query 必須走 single shard（targeted），哪些 query 不在意 scatter-gather。</p>
<p><strong>Step 3：候選 key 評估表</strong>。對每個候選打 cardinality / frequency / monotonicity 三項評分：</p>
<table>
  <thead>
      <tr>
          <th>候選 key</th>
          <th>Cardinality</th>
          <th>Frequency</th>
          <th>Monotonicity</th>
          <th>適合？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>_id</code>（ObjectId）</td>
          <td>極高</td>
          <td>均勻</td>
          <td>單調</td>
          <td>否（單調寫熱）</td>
      </tr>
      <tr>
          <td><code>tenantId</code></td>
          <td>中</td>
          <td>偏斜</td>
          <td>否</td>
          <td>視 tenant 分布</td>
      </tr>
      <tr>
          <td><code>{ tenantId: 1, _id: &quot;hashed&quot; }</code></td>
          <td>高</td>
          <td>均勻</td>
          <td>否</td>
          <td>通常合適</td>
      </tr>
      <tr>
          <td><code>country</code></td>
          <td>極低（~200）</td>
          <td>嚴重偏斜</td>
          <td>否</td>
          <td>否</td>
      </tr>
  </tbody>
</table>
<p><strong>Step 4：dry-run 採樣</strong>。對既有資料採樣，跑 <code>db.coll.aggregate([{$sample:{size:100000}}, {$group:{_id:&quot;$candidateKey&quot;, c:{$sum:1}}}, {$sort:{c:-1}}])</code> 看分布、確認沒有單一 key value 吃掉 &gt; 20% 流量。</p>
<p><strong>Step 5：shardCollection</strong>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">sh</span><span class="p">.</span><span class="nx">enableSharding</span><span class="p">(</span><span class="s2">&#34;shop&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">sh</span><span class="p">.</span><span class="nx">shardCollection</span><span class="p">(</span><span class="s2">&#34;shop.orders&#34;</span><span class="p">,</span> <span class="p">{</span> <span class="nx">tenantId</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">_id</span><span class="o">:</span> <span class="s2">&#34;hashed&#34;</span> <span class="p">})</span></span></span></code></pre></div><p>先在 staging 跑流量重放、確認 chunk 分布平均、targeted query 比例 &gt; 90%。</p>
<p><strong>Step 6：監控</strong>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">sh</span><span class="p">.</span><span class="nx">status</span><span class="p">()</span>                              <span class="c1">// 看 cluster 狀態
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">db</span><span class="p">.</span><span class="nx">orders</span><span class="p">.</span><span class="nx">getShardDistribution</span><span class="p">()</span>         <span class="c1">// 看 chunk 分布
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="nx">db</span><span class="p">.</span><span class="nx">adminCommand</span><span class="p">({</span> <span class="nx">balancerStatus</span><span class="o">:</span> <span class="mi">1</span> <span class="p">})</span>   <span class="c1">// 看 balancer 狀態
</span></span></span></code></pre></div><p><strong>Step 7：若已上錯 key</strong>。評估 <code>reshardCollection</code>（4.4+）vs application-level 雙寫遷移：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">adminCommand</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">reshardCollection</span><span class="o">:</span> <span class="s2">&#34;shop.orders&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">key</span><span class="o">:</span> <span class="p">{</span> <span class="nx">tenantId</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">region</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">_id</span><span class="o">:</span> <span class="s2">&#34;hashed&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><p><code>reshardCollection</code> 進入 cutover 後不能回退、必須 dry-run 估完時間 + 磁碟 + IO 影響再上。</p>
<p>驗證點：targeted query 比例 &gt; 90%、單 shard QPS 變異係數 &lt; 20%、balancer migration 速率追上寫入速率。</p>
<p>Rollback boundary：<code>shardCollection</code> 是不可逆操作（5.0 前完全不可逆、5.0+ 透過 reshardCollection 可改但需重做）；<code>reshardCollection</code> 進入 cutover 後不能回退。</p>
<h2 id="失敗模式">失敗模式</h2>
<p><strong>單調 key 寫熱點</strong>：<code>_id</code>（ObjectId）/ 時間戳 / 自增 ID 當 ranged shard key → 所有寫進最後 chunk，scale-out 等於零。修法是 hashed key 或 compound key 把單調軸拌散。</p>
<p><strong>低 cardinality key</strong>：用 <code>country</code> 當 shard key、某個 country 佔 80% 流量、chunk 無法繼續 split、該 shard 永久熱。修法是加一個高 cardinality 軸（compound key）讓 chunk 可繼續分。</p>
<p><strong>Tenant skew</strong>：B2B 場景大客戶獨佔 chunk、且該 tenant 的 chunk 還會繼續長大、balancer 搬不走。修法 compound key <code>{ tenantId: 1, _id: &quot;hashed&quot; }</code> — tenant 隔離但 tenant 內 hash 散開。</p>
<p><strong>Scatter-gather 過多</strong>：選了 hashed <code>_id</code> 但業務查詢主要是 <code>tenantId</code> 範圍查、每筆 query 打所有 shard、p99 隨 shard 數線性退化。修法 compound key 把常用查詢軸放第一位、targeted query 才能對 single shard。</p>
<p><strong>Resharding 卡在 build 階段</strong>：磁碟不夠（需 1.2x source size）、IO 飽和影響線上 workload、預期 4 小時實際跑 14 小時。修法是先擴磁碟、staging 跑 dry-run 量實際耗時、production 在低峰期啟動。</p>
<p><strong>Zone sharding 規則打架</strong>：合規規則（資料必須留在某 region）跟負載平衡規則衝突、balancer 無法移動 chunk → 熱點固化。修法是 zone 規則 vs balancer 設計階段就劃清、不要事後加 zone。</p>
<p><strong>誤把多 cluster 當分 shard 解</strong>：blast radius 議題塞到 sharded cluster、單 cluster 故障仍打掉全部 microservice。該分 cluster 的就分 cluster、不是塞到 shard。9.C38 Toyota 揭露：7K txn/sec 仍切 20 DB 的 trigger 是 microservice ownership、不是吞吐。</p>
<p><strong>Cluster 擴容時間估計太樂觀</strong>：MongoDB cluster 擴容是天級議題、不是 console 點點就好。9.C36 Coinbase 揭露 cluster 擴容要 70 分鐘（口徑：Coinbase 特定環境 cluster tier / 資料量 / Atlas API 條件下、reactive scaling 起點到完成、非 MongoDB 普遍承諾）；預測性流量必須走 predictive / scheduled scaling、不能只靠 sharded cluster 動態橫向擴展接住 surge（見 <a href="../connection-management-and-cache-layer/">connection management and cache layer</a>）。</p>
<p>Anti-recommendation：</p>
<ul>
<li>寫入 &lt; 5K WPS、storage &lt; 1TB、single replica set 還能撐就不該分 shard；分了之後 aggregation、transaction、<code>$lookup</code>、index 成本全部跳一級</li>
<li><strong>shard vs 多 cluster 對照</strong>：吞吐沒撞牆但 blast radius / ownership 是議題、走多 cluster 不是強行分 shard（9.C38 Toyota 7K txn/sec 仍切 20 DB 的 trigger）</li>
<li>跨 case 合成 frame：「不是所有資料都該進同一個 MongoDB cluster」、按 microservice ownership / blast radius / 合規邊界切</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<p>關鍵 metric：</p>
<ul>
<li><strong>Shard 分布健康</strong>：每 shard QPS / CPU / disk usage 變異係數（&lt; 20% 合理）</li>
<li><strong>Query 路由</strong>：targeted vs scatter-gather query 比例（targeted &gt; 90% 合理）</li>
<li><strong>Balancer 健康</strong>：chunk migration rate、balancer round duration</li>
<li><strong>Cluster 邊界</strong>：cluster-to-cluster ownership 邊界、跨 cluster query 比例</li>
</ul>
<p>Mongo command：</p>
<ul>
<li><code>sh.status()</code>：cluster 整體狀態</li>
<li><code>db.coll.getShardDistribution()</code>：collection 在各 shard 的分布</li>
<li><code>db.adminCommand({balancerStatus:1})</code>：balancer 狀態</li>
<li><code>db.serverStatus().sharding</code>：sharding metric</li>
</ul>
<p><code>mongos</code> profiler：每 query 帶 <code>executionStats.executionStages.shards[]</code>、看是否 single shard。</p>
<p>回到 <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</a>：把 shard distribution、targeted ratio、resharding 進度列為 evidence 三件套。</p>
<p>回到 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 saturation discovery</a>：hot shard 是 partition-level saturation 的典型例子。</p>
<p>回到 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 bottleneck localization</a>：當整 cluster CPU 看似只用 25%、實際是 1/4 shard 在 100%。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<p>Sibling deep articles：</p>
<ul>
<li><a href="../schema-design-pattern/">schema design pattern</a> — document 形狀決定 shard key 選擇空間</li>
<li><a href="../aggregation-pipeline-optimization/">aggregation pipeline optimization</a> — cross-shard aggregation 的 <code>$out</code> / <code>$merge</code> 限制</li>
<li><a href="../change-streams-kafka/">change streams + Kafka</a> — cluster-wide vs collection-level change stream 在 sharded cluster 的差異</li>
<li><a href="../connection-management-and-cache-layer/">connection management and cache layer</a> — cluster 擴容時間是天級議題、必須跟 predictive scaling / proxy 層配合</li>
</ul>
<p>Migration playbook：</p>
<ul>
<li>避免自管 sharding 走 <a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">→ Atlas</a> 用 managed shard tier</li>
<li>徹底重新分區走 <a href="/blog/backend/01-database/vendors/mongodb/shard-expansion-multi-dc/" data-link-title="MongoDB Shard Expansion &#43; Multi-DC：Type F「不需要 parallel run」的 multi-region 例外" data-link-desc="MongoDB sharded cluster 加 shard &#43; 跨 DC expansion 是 Type F「topology re-layout」第 3 個 dogfood — 同時改 sharding &#43; replication topology &#43; region distribution；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 第 3 點「Type F 不需要 parallel run」claim 的例外（multi-region rollout 必須 parallel run &#43; 切流量）；涵蓋 chunk migration / replica set add member / cross-DC routing">shard expansion + multi-DC</a></li>
</ul>
<p>跟 1.x 互引：<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> 把 shard key 列為 capacity 決策；<a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a> 收 resharding 失敗 retrospective。</p>
<p>跨 vendor 對照：<a href="/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB vendor page</a>（partition key + adaptive capacity + backfill 可改）、<a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor page</a>（partition key 不可改）。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> — 本文是該頁尾「shard key 選型」backlog 的深度展開</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a> — 20 個 Atlas DB 切 blast radius</li>
<li><a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a> — cluster 擴容 70 分鐘特定環境數字</li>
<li>官方：<a href="https://www.mongodb.com/docs/manual/sharding/">MongoDB Sharding</a>、<a href="https://www.mongodb.com/docs/manual/core/sharding-shard-key/">Choosing a Shard Key</a>、<a href="https://www.mongodb.com/docs/manual/core/sharding-reshard-a-collection/">Resharding</a></li>
</ul>
]]></content:encoded></item><item><title>MongoDB Replica Set Read Preference：DB 層 causal session vs cache 層 freshness token</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/replica-set-read-preference/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/replica-set-read-preference/</guid><description>&lt;p>MongoDB replica set 在小規模時 read preference 五擇一就夠用、&lt;code>primary&lt;/code> 走預設、想分擔 primary 改 &lt;code>secondary&lt;/code> — 直觀但會在 production 反噬。讀者真正撞到的議題分兩層：DB 層的 read-your-own-write（同 client 寫完馬上讀讀不到）跟跨層的 read-after-write（write 進 MongoDB、cache 還是舊資料）。前者用 causal consistency session 解、後者要走 freshness token 跨層協議。Coinbase 1.5M reads/sec 不是純 MongoDB 撐出來、是 DB + cache 跨層合成。本文把 read preference 機制 + 跨層協作講清楚。&lt;/p>
&lt;p>本文不重複 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview&lt;/a> 已寫過的 replica set 簡介 — 而是 production 部署 + 跨層協作 + 失敗修復的實作層教學。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>進本文前先確認 MongoDB 已通過適配判讀&lt;/strong>：workload 是否落在 MongoDB 適用區（document shape 主導 / contract layer 該放哪 / 跨雲 hedging 是否需要）— 判讀軸見 &lt;a href="../schema-design-pattern/#%e5%95%8f%e9%a1%8c%e6%83%85%e5%a2%83document-%e8%87%aa%e7%94%b1%e7%9a%84%e5%be%8c%e5%ba%a7%e5%8a%9b">schema-design-pattern 開頭 3 軸前置判讀&lt;/a>。Read scaling 是 &lt;em>已選 MongoDB 後&lt;/em> 的容量決策、判讀通不過時 read preference 修補無法救回 vendor 選錯。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境read-scaling-撞牆的兩種長相">問題情境：read scaling 撞牆的兩種長相&lt;/h2>
&lt;p>典型觸發場景：primary 寫入飽和、TL 提議「讀都打 secondary」想橫向擴容。改完後幾個 production 徵兆連環出現：&lt;/p>
&lt;ul>
&lt;li>User 看到「我剛下的訂單怎麼還沒出現」— write 進 primary、立刻 read 打 secondary、secondary 還沒 apply 該寫入、user 看到 stale data&lt;/li>
&lt;li>跨 region replica set：app server 在 Tokyo、primary 在 Singapore、每筆讀走 70ms 跨海 RTT；改 &lt;code>nearest&lt;/code> 後 latency 降但 stale read 出現&lt;/li>
&lt;li>Replication lag 在 backup 期間飆到分鐘級、&lt;code>secondary&lt;/code> read 拿到幾分鐘前的資料、前端報表時間軸對不上&lt;/li>
&lt;li>Failover 期間 read preference 沒寫好、client 一直連舊 primary、&lt;code>SocketTimeout&lt;/code> 直到 driver retry 邏輯介入&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>第二類議題、規模更大&lt;/strong>：把所有 read 打 secondary、replica 數量加到 5-7 仍撐不住 sustained 高 read（&amp;gt;500K reads/sec）；replication lag 升 + secondary CPU 飽和。這時 read preference 已不夠、必須加 cache + 跨層 freshness 機制。&lt;/p>
&lt;p>讀者徵兆：&lt;code>rs.printSecondaryReplicationInfo()&lt;/code> 顯示 lag 分鐘級、application log 出現「我剛寫的資料讀不到」客訴、failover 演練後 connection error 持續 30s+、cache hit rate 跟 read latency 反向相關。&lt;/p></description><content:encoded><![CDATA[<p>MongoDB replica set 在小規模時 read preference 五擇一就夠用、<code>primary</code> 走預設、想分擔 primary 改 <code>secondary</code> — 直觀但會在 production 反噬。讀者真正撞到的議題分兩層：DB 層的 read-your-own-write（同 client 寫完馬上讀讀不到）跟跨層的 read-after-write（write 進 MongoDB、cache 還是舊資料）。前者用 causal consistency session 解、後者要走 freshness token 跨層協議。Coinbase 1.5M reads/sec 不是純 MongoDB 撐出來、是 DB + cache 跨層合成。本文把 read preference 機制 + 跨層協作講清楚。</p>
<p>本文不重複 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> 已寫過的 replica set 簡介 — 而是 production 部署 + 跨層協作 + 失敗修復的實作層教學。</p>
<blockquote>
<p><strong>進本文前先確認 MongoDB 已通過適配判讀</strong>：workload 是否落在 MongoDB 適用區（document shape 主導 / contract layer 該放哪 / 跨雲 hedging 是否需要）— 判讀軸見 <a href="../schema-design-pattern/#%e5%95%8f%e9%a1%8c%e6%83%85%e5%a2%83document-%e8%87%aa%e7%94%b1%e7%9a%84%e5%be%8c%e5%ba%a7%e5%8a%9b">schema-design-pattern 開頭 3 軸前置判讀</a>。Read scaling 是 <em>已選 MongoDB 後</em> 的容量決策、判讀通不過時 read preference 修補無法救回 vendor 選錯。</p></blockquote>
<h2 id="問題情境read-scaling-撞牆的兩種長相">問題情境：read scaling 撞牆的兩種長相</h2>
<p>典型觸發場景：primary 寫入飽和、TL 提議「讀都打 secondary」想橫向擴容。改完後幾個 production 徵兆連環出現：</p>
<ul>
<li>User 看到「我剛下的訂單怎麼還沒出現」— write 進 primary、立刻 read 打 secondary、secondary 還沒 apply 該寫入、user 看到 stale data</li>
<li>跨 region replica set：app server 在 Tokyo、primary 在 Singapore、每筆讀走 70ms 跨海 RTT；改 <code>nearest</code> 後 latency 降但 stale read 出現</li>
<li>Replication lag 在 backup 期間飆到分鐘級、<code>secondary</code> read 拿到幾分鐘前的資料、前端報表時間軸對不上</li>
<li>Failover 期間 read preference 沒寫好、client 一直連舊 primary、<code>SocketTimeout</code> 直到 driver retry 邏輯介入</li>
</ul>
<p><strong>第二類議題、規模更大</strong>：把所有 read 打 secondary、replica 數量加到 5-7 仍撐不住 sustained 高 read（&gt;500K reads/sec）；replication lag 升 + secondary CPU 飽和。這時 read preference 已不夠、必須加 cache + 跨層 freshness 機制。</p>
<p>讀者徵兆：<code>rs.printSecondaryReplicationInfo()</code> 顯示 lag 分鐘級、application log 出現「我剛寫的資料讀不到」客訴、failover 演練後 connection error 持續 30s+、cache hit rate 跟 read latency 反向相關。</p>
<p>Case anchor：<a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a> 揭露「document model 撐 1.5M reads/sec 靠 cache + freshness token」、含警示「1.5M reads/sec 是 users 服務 <em>加上 cache</em> 的數字、不是 MongoDB cluster 純讀取數字」。跨 region read preference 改 <code>nearest</code> 後 stale read 的具體 incident 細節需未來 case 補完、本文以「常見 failure pattern」處理。</p>
<h2 id="核心機制">核心機制</h2>
<h3 id="mongodb-read-preference--read-concern-兩軸">MongoDB read preference + read concern 兩軸</h3>
<p>Read preference 五種：</p>
<ul>
<li><strong><code>primary</code></strong>（預設）：只打 primary、強一致、primary 飽和時無路可走</li>
<li><strong><code>primaryPreferred</code></strong>：先 primary、primary 不可用 fallback secondary</li>
<li><strong><code>secondary</code></strong>：只打 secondary、永遠拒 primary、failover 期間若所有 secondary 都不行就拋錯</li>
<li><strong><code>secondaryPreferred</code></strong>：先 secondary、secondary 不可用 fallback primary</li>
<li><strong><code>nearest</code></strong>：不是「最近的 secondary」、是「ping latency 最低的 member」（可能是 primary）；driver 用 latency window（預設 15ms）內隨機挑</li>
</ul>
<p>Read concern 是另一軸：</p>
<ul>
<li><strong><code>local</code></strong>：讀本地最新（含未確認）、效能最佳、可能讀到後來 rollback 的資料</li>
<li><strong><code>available</code></strong>：跟 <code>local</code> 類似但對 sharded cluster 有差異</li>
<li><strong><code>majority</code></strong>：讀到「已寫到多數 member」的資料、寫入 commit 後在多數 member 確認後才看得到</li>
<li><strong><code>linearizable</code></strong>：強制最新、必須打 primary、最高 latency</li>
</ul>
<p>Write concern <code>w: &quot;majority&quot;</code> 保證寫入確認後在多數 member 上、但不保證 secondary 馬上 visible — 兩個概念分開。</p>
<h3 id="causal-consistency-sessiondb-層機制">Causal consistency session（DB 層機制）</h3>
<p>Causal consistency session 解的是 <em>單 client</em> 在 <em>MongoDB cluster 內部</em> 的因果一致：</p>
<ul>
<li>Client session 帶 <code>clusterTime</code> + <code>operationTime</code></li>
<li>Driver 把 read 路由到「已 apply 該 operationTime」的 member</li>
<li>實現 read-your-own-write（自己剛寫的、自己讀得到）</li>
</ul>
<p>機制只在「同一 client session」內生效。跨 client 的因果一致（A 寫 → B 讀）不在範圍內。</p>
<p>其他輔助機制：</p>
<ul>
<li><strong>Tag set</strong>：member 標 <code>{region: &quot;ap-tokyo&quot;, role: &quot;analytics&quot;}</code>、read preference 帶 tag 把流量路由到特定 member</li>
<li><strong>Hidden / delayed secondary</strong>：不參與 election、不接 client read、做 backup / DR 用</li>
<li><strong>Election</strong>：primary 失聯後 majority 投票選新 primary、預設 10s 內完成；election 期間所有 primary read 失敗</li>
</ul>
<h3 id="freshness-tokencache-層機制">Freshness token（cache 層機制）</h3>
<p>9.C36 Coinbase 揭露的 <em>跨層</em> 機制 — 解的是 <em>MongoDB + cache 跨層</em> 的 read-after-write、不是 cluster 內部。對應 <a href="/blog/backend/knowledge-cards/freshness-token/" data-link-title="Freshness Token" data-link-desc="DB write 後返回的版本 token、後續 read 帶 token、保證 read 看到的資料 ≥ token 版本、解 DB &#43; cache 跨層 read-after-write">Freshness Token</a> 卡片的 application-level 版本協議定義：</p>
<p><strong>觸發條件</strong>：直接打 MongoDB 不可能撐 1.5M reads/sec（口徑：users 服務應用層觀察、含 cache、非 MongoDB cluster 純讀取）。Coinbase 在 users 服務前加 Memcached query cache、單 document query 先查 cache。</p>
<p><strong>跨層一致性問題</strong>：write 進 MongoDB primary、cache 還是舊資料、client 下次 read 從 cache 拿到舊版。</p>
<p><strong>freshness token 機制</strong>：</p>
<ol>
<li>Write 成功後、server 給 client 一個 token（包含 OCC version / clusterTime）</li>
<li>Client 之後 read 帶這個 token</li>
<li>Server 保證返回的資料版本 ≥ token</li>
<li>若 cache 的版本 &lt; token、bypass cache 直接打 DB</li>
</ol>
<p><strong>跟 causal consistency session 的關係</strong>：兩者解決同一類問題（read-after-write）但作用範圍不同。Causal session 是 DB 層、保證在同一 cluster 內 read-your-own-write；freshness token 是 <em>DB + cache 兩層共用的版本協議</em>、保證跨層 read-your-own-write。</p>
<h3 id="跨層協作三選一">跨層協作三選一</h3>
<p>讀者真實系統的 read 一致性需求要選哪層處理：</p>
<table>
  <thead>
      <tr>
          <th>路徑</th>
          <th>適用情境</th>
          <th>代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>只用 DB 層（causal session）</td>
          <td>無 cache 層、讀寫都直接打 MongoDB cluster</td>
          <td>replica scaling 上限約幾十萬 reads/sec</td>
      </tr>
      <tr>
          <td>只用 cache 層（freshness token）</td>
          <td>有 cache、跨層一致性要求高、application 願改</td>
          <td>需設計 token 協議 + cache bypass 邏輯</td>
      </tr>
      <tr>
          <td>兩層並用</td>
          <td>大規模 OLTP、cluster 內也要 causal、跨 cache 也要 freshness</td>
          <td>複雜度最高、但 Coinbase 規模必走此路</td>
      </tr>
  </tbody>
</table>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale-read</a>、<a href="/blog/backend/knowledge-cards/replication-lag/" data-link-title="Replication Lag" data-link-desc="說明資料副本落後正式來源多久，以及它如何影響讀取正確性">replication-lag</a>、<a href="/blog/backend/knowledge-cards/session-consistency/" data-link-title="Session Consistency" data-link-desc="同一使用者工作階段內維持讀寫一致、跨工作階段允許短暫不一致">session-consistency</a>、<a href="/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual-consistency</a>。</p>
<h2 id="操作流程">操作流程</h2>
<p><strong>Step 1：read shape 分類</strong>。把所有 read 分成四類：</p>
<ul>
<li>(a) 強一致必須 read-your-own-write（訂單詳情、帳戶餘額）</li>
<li>(b) 容忍秒級 lag（個人資料、商品詳情）</li>
<li>(c) 容忍分鐘級 lag（報表、analytics）</li>
<li>(d) 大規模 read scaling 需 cache + freshness token（用戶資料 / 高頻 product query）</li>
</ul>
<p><strong>Step 2：依分類對映機制</strong>。</p>
<table>
  <thead>
      <tr>
          <th>分類</th>
          <th>Read preference</th>
          <th>Read concern</th>
          <th>跨層機制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>(a)</td>
          <td>primary</td>
          <td>majority</td>
          <td>causal consistency session</td>
      </tr>
      <tr>
          <td>(b)</td>
          <td>secondaryPreferred</td>
          <td>local</td>
          <td>monitoring lag alarm</td>
      </tr>
      <tr>
          <td>(c)</td>
          <td>secondary（tag set）</td>
          <td>available</td>
          <td>無</td>
      </tr>
      <tr>
          <td>(d)</td>
          <td>secondaryPreferred</td>
          <td>majority</td>
          <td>cache + freshness token + bypass</td>
      </tr>
  </tbody>
</table>
<p><strong>Step 3：driver config</strong>（Node.js / Java / Python 都類似）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">mongodb://host1:27017,host2:27017,host3:27017/db?
</span></span><span class="line"><span class="ln">2</span><span class="cl">  replicaSet=rs0&amp;
</span></span><span class="line"><span class="ln">3</span><span class="cl">  readPreference=secondaryPreferred&amp;
</span></span><span class="line"><span class="ln">4</span><span class="cl">  readPreferenceTags=region:ap-tokyo&amp;
</span></span><span class="line"><span class="ln">5</span><span class="cl">  readPreferenceTags=&amp;
</span></span><span class="line"><span class="ln">6</span><span class="cl">  maxStalenessSeconds=90&amp;
</span></span><span class="line"><span class="ln">7</span><span class="cl">  readConcernLevel=majority</span></span></code></pre></div><p><code>readPreferenceTags</code> 寫多個 = fallback chain（先 tokyo 失敗 fallback 任意）。<code>maxStalenessSeconds=90</code> 拒絕 lag &gt; 90s 的 secondary。</p>
<p><strong>Step 4：causal consistency session</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">with</span> <span class="n">client</span><span class="o">.</span><span class="n">start_session</span><span class="p">(</span><span class="n">causal_consistency</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span> <span class="k">as</span> <span class="n">s</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">coll</span><span class="o">.</span><span class="n">insert_one</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="n">session</span><span class="o">=</span><span class="n">s</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="c1"># 下面這個 find 自動路由到能讀到剛才寫的 member</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">coll</span><span class="o">.</span><span class="n">find_one</span><span class="p">({</span><span class="s2">&#34;_id&#34;</span><span class="p">:</span> <span class="n">doc</span><span class="p">[</span><span class="s2">&#34;_id&#34;</span><span class="p">]},</span> <span class="n">session</span><span class="o">=</span><span class="n">s</span><span class="p">)</span></span></span></code></pre></div><p>Session 結束後因果關係結束、下個 session 不繼承。</p>
<p><strong>Step 5：freshness token 設計</strong>（9.C36 Coinbase 模式）：</p>
<ul>
<li>Write API 返回 <code>{result, version_token}</code> — token 含 OCC version 或 MongoDB clusterTime</li>
<li>Read API 接受 optional <code>If-Version-≥</code> header / parameter</li>
<li>Cache lookup 比對 cache entry version 跟 token、低於 token 就 invalidate + bypass 到 MongoDB</li>
<li>DB 層 read 用 <code>readConcern: &quot;majority&quot;</code> 保證返回的 version ≥ token</li>
</ul>
<p><strong>Step 6：staging 驗證</strong>。灌入 replication lag（暫停 secondary apply）驗證 application 行為；灌入 stale cache 驗證 token bypass 邏輯；模擬 failover 驗證 driver retry。</p>
<p>驗證點：</p>
<ul>
<li><code>rs.printSecondaryReplicationInfo()</code> lag &lt; SLO</li>
<li>driver metric <code>readPreferenceUsageCount</code> 分布符合預期</li>
<li>failover drill 後 read recovery &lt; 15s</li>
<li>cache hit rate vs freshness bypass rate 比例監控</li>
</ul>
<p>Rollback boundary：read preference 是 driver-side config、可以 hot-swap；causal consistency session 需 application code 改、需灰度；freshness token 是 application + cache + DB 三方協議、回退需協調。</p>
<h2 id="失敗模式">失敗模式</h2>
<p><strong>Read-after-write 不一致（DB 層）</strong>：寫 primary → 立刻 secondary read、應用 race condition 顯示「資料消失」。修法是 causal consistency session、driver 自動路由到已 apply 該寫入的 member。</p>
<p><strong>Read-after-write 不一致（跨層）</strong>：寫 primary → cache 還是舊資料 → user 看到舊資料。causal session 解不了（cache 在 MongoDB 外）、必須走 freshness token 跨層協議。</p>
<p><strong>Stale read 在 lag 高峰</strong>：backup / DDL / 大量寫入導致 secondary lag 分鐘級、<code>secondary</code> read 拿到舊資料。修法設 <code>maxStalenessSeconds</code> 拒舊 member、driver 自動轉到較新的 member 或 primary。</p>
<p><strong><code>nearest</code> 在跨 region 不穩</strong>：latency 抖動讓 driver 在 primary / secondary 跳、寫一致性與 read latency 同時惡化。修法是不要用 <code>nearest</code> 解跨 region 議題、應該用 tag set 明確路由。</p>
<p><strong>Failover 期間 primary read 全失敗</strong>：election 10s 內所有 primary read 拋錯。修法改 <code>primaryPreferred</code> + driver retry 邏輯吃掉短暫失敗、application 端配 retry policy。</p>
<p><strong>Tag set 失準</strong>：把 <code>region: &quot;ap-tokyo&quot;</code> 的流量路由到 tag 為 tokyo 的 member、但該 member 故障時沒 fallback、流量直接停。修法是 tag 設多層 fallback chain、最後一層留空 tag 表示「任意 member」。</p>
<p><strong>Analytical query 跑 OLTP secondary</strong>：<code>secondaryPreferred</code> 把報表打 OLTP secondary、報表 query 拖垮 OLTP read latency。修法是 analytical workload 用 tag set 路由到專屬 analytics secondary、跟 OLTP read 隔離。</p>
<p><strong>Freshness token 漏寫</strong>：write 沒帶 token 給 client / client 沒帶 token、token 機制 silently 失效、read 走 cache 拿舊資料。修法 token 必須 e2e 強制（middleware 自動帶 / 自動驗證）、不能靠 application 自覺。</p>
<p><strong>Cache bypass 比例失控</strong>：所有 read 都 bypass cache、cache 等於沒裝。修法是 token 失敗率要監控、過高表示 cache invalidation 設計有問題（cache 沒在 write 後 update / invalidate）。</p>
<p>Anti-recommendation：</p>
<ul>
<li>read-heavy 但有強一致需求的場景不要為了 scale 改 secondary read；該換 SQL + read replica 加 application-level cache、或加 sharding 把 primary 寫散開</li>
<li>大規模 OLTP（&gt;500K reads/sec）想單靠 MongoDB read preference 撐 = 拿不到那個量級。Coinbase 案明示「直接打 MongoDB 不可能撐 1.5M reads/sec」、必須 cache + freshness token</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<p>關鍵 metric：</p>
<ul>
<li><strong>Replica health</strong>：每個 member 的 <code>opcounters</code> 分布、<code>rs.status().members[].optimeDate</code> 推算 lag</li>
<li><strong>Read preference 命中</strong>：driver-side <code>readPreferenceTags</code> 命中率</li>
<li><strong>一致性 SLO</strong>：stale read 比例（causal consistency 拒絕重試次數）</li>
<li><strong>跨層 freshness</strong>：cache hit rate vs freshness bypass rate</li>
</ul>
<p>Mongo command：</p>
<ul>
<li><code>rs.status()</code>：replica set 整體</li>
<li><code>rs.printSecondaryReplicationInfo()</code>：lag 概況</li>
<li><code>db.serverStatus().repl</code>：詳細 replication metric</li>
<li><code>db.adminCommand({replSetGetStatus:1})</code>：完整 status</li>
</ul>
<p>Application observability：APM 看「同一 session 內 write + read 順序對 latency / error 的影響」、SLO 是 read-your-own-write 命中率；跨層還要看 freshness token 流動完整性（write 是否發 token、read 是否帶 token、cache 是否驗 token）。</p>
<p>Lag alarm：lag &gt; 30s 預警、&gt; 90s 觸發 driver <code>maxStalenessSeconds</code> 自動拒讀。</p>
<p>回到 <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</a>：把 read preference 命中分布、replication lag time series、failover drill recovery time、freshness token bypass rate 列為 evidence。</p>
<p>回到 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 bottleneck localization</a>：read latency 異常時要區分 (a) primary 飽和 (b) secondary lag 高 (c) tag routing 把流量集中到單一 member (d) cache hit rate 下降 / bypass 率上升。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="frame-5合規邊界--mongodb-用-cluster-per-region-吸收">Frame 5：合規邊界 — MongoDB 用 cluster-per-region 吸收</h3>
<p>MongoDB / Atlas 沒有 <em>row-level locality</em> 機制（不像 CockroachDB 可把單 row pin 在合規 region）— 跨境合規必須以 <em>cluster-per-region</em> 拓樸吸收：每個合規市場開獨立 cluster、application 層做 routing、不靠 replica set / sharded cluster 機制跨 region。</p>
<p>跨 vendor 對照：</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>合規吸收機制</th>
          <th>拓樸特性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>MongoDB / Cosmos DB</td>
          <td>cluster-per-region（無 row-level locality 等價物）</td>
          <td>各 region 獨立 cluster、application 層做市場 routing</td>
      </tr>
      <tr>
          <td>Aurora</td>
          <td>fleet 拓樸（每市場獨立 cluster、Global Database 在合規場景反指標）</td>
          <td>active-passive per market、跨市場不複製</td>
      </tr>
      <tr>
          <td>CockroachDB</td>
          <td>locality + placement（邏輯一個 cluster + region pinning + Outposts）</td>
          <td>單 logical cluster、physical row 鎖在合規 region</td>
      </tr>
      <tr>
          <td>DynamoDB</td>
          <td>region-pinned Global Tables（按 region 開關 replication、各市場可分離）</td>
          <td>仍 active-active、但 replication 範圍可控</td>
      </tr>
  </tbody>
</table>
<p><strong>MongoDB 在這 frame 的退化點</strong>：read preference 機制本身不解合規 — 即使 <code>readPreferenceTags={region:eu}</code> 把流量路由到歐洲 secondary、但 primary 在亞洲時跨境 replication 仍在跑、合規 audit 不會放行 <em>路由層</em> 控制當作 <em>資料邊界</em> 控制。合規市場必須整 cluster 分離、再用 application 層 routing 把 user 帶到對應 cluster。</p>
<p><strong>Atlas 在合規場景的 fit</strong>：Atlas global cluster（zone sharding 把 shard 鎖在 region）是「跨 region 但 <em>資料 pin 在 zone</em>」的中介選項、適合 GDPR 軟條款（資料在歐洲 EEA 內可流動）；strict 條款（資料不能離開單一國家）仍須走 cluster-per-region。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<p>Sibling deep articles：</p>
<ul>
<li><a href="../shard-key-selection/">shard key selection</a> — read preference 解決不了 write 飽和、要切 shard</li>
<li><a href="../change-streams-kafka/">change streams + Kafka</a> — change stream 預設打 primary、放 secondary 的 trade-off</li>
<li><a href="../aggregation-pipeline-optimization/">aggregation pipeline optimization</a> — 把 analytical aggregation 路由到專屬 secondary</li>
<li><a href="../connection-management-and-cache-layer/">connection management and cache layer</a> — freshness token 是該篇的核心議題之一、本文聚焦 DB 層 vs cache 層機制對照、不展開 cache 部署架構</li>
</ul>
<p>Migration playbook：</p>
<ul>
<li>跨 region 強 consistency 需求 → <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">→ Cosmos DB MongoDB API</a>（5 consistency level）</li>
<li>跨 region 想保留原生 MongoDB → <a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">→ Atlas global cluster</a></li>
</ul>
<p>跟 1.x 互引：<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a> 處理 read scaling pattern；<a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 處理跨 region 一致性升級路徑。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> — 本文是該頁尾「replica set + read preference」backlog 的深度展開</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a> — freshness token + 1.5M reads/sec（含 cache）</li>
<li>官方：<a href="https://www.mongodb.com/docs/manual/core/read-preference/">MongoDB Read Preference</a>、<a href="https://www.mongodb.com/docs/manual/reference/read-concern/">Read Concern</a>、<a href="https://www.mongodb.com/docs/manual/core/causal-consistency-read-write-concerns/">Causal Consistency</a></li>
</ul>
]]></content:encoded></item><item><title>MongoDB Connection Management and Cache Layer：driver × 部署模型 × cache × predictive scaling</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/connection-management-and-cache-layer/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/connection-management-and-cache-layer/</guid><description>&lt;p>MongoDB 大規模 OLTP 的真實架構不是「一個 driver pool 直連 cluster」、是 driver / proxy 層 + cache + freshness token 層 + scaling trigger 層三層協作。讀者最常的誤解是「Coinbase 用 MongoDB 撐 1.5M reads/sec」— 實際是這個合成架構撐出來的量級、單靠 MongoDB cluster 拿不到那個數字。本文把三層各自議題跟整合操作流程講清楚、並對 mongobetween 的部署模型適用範圍給出明確邊界。&lt;/p>
&lt;p>本文不重複 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview&lt;/a> 的 Atlas / 容量規劃簡介 — 而是 production 部署 + 跨層協作 + 失敗修復的實作層教學。&lt;/p>
&lt;h2 id="問題情境大規模-oltp-撞三道牆">問題情境：大規模 OLTP 撞三道牆&lt;/h2>
&lt;p>MongoDB 部署規模從中型撐到大規模時、會連環撞三道牆：&lt;/p>
&lt;p>&lt;strong>Connection ceiling&lt;/strong>：應用層 deploy 規模一上來、單一 MongoDB cluster 看到 connection storm。9.C36 Coinbase 揭露具體：Ruby + GVL + blue-green 部署把 instance 數 ×2、連線數隨之 ×2、單一 cluster 看到 60K connections / 分鐘（口徑：Coinbase 特定環境 CRuby + GVL 部署模型）。MongoDB cluster 的 connection limit 撞牆、新 deploy 連不上、線上服務 cascade 失敗。&lt;/p>
&lt;p>&lt;strong>Read scaling ceiling&lt;/strong>：讀者把所有 read 都打 secondary、replica 加到 5-7 仍撐不住 sustained 高 read（&amp;gt;500K reads/sec）。Replication lag 升 + secondary CPU 飽和；單靠 MongoDB cluster 內機制（replica scaling + read preference）拿不到大規模量級。&lt;/p>
&lt;p>&lt;strong>Scaling reaction lag&lt;/strong>：MongoDB cluster 擴容是天級議題、不是即時擴容。9.C36 Coinbase 揭露 reactive scaling 起點到完成 ~70 分鐘（口徑：Coinbase 特定環境、cluster tier / 資料量 / Atlas API 條件下、非 MongoDB 普遍承諾）。Surge 開始時才動來不及、預測性流量必須提前出手。&lt;/p>
&lt;p>Surge 形狀又不規則：加密貨幣 surge（隨外部市場波動）/ 媒體爆量（事件驅動）/ IoT 緊急通報（雙模式並存）— 都不適合單純 reactive auto-scaling 接住、必須 predictive + reactive 兩段式。&lt;/p>
&lt;p>讀者徵兆：&lt;/p>
&lt;ul>
&lt;li>MongoDB Atlas console 看到 connection count 在 deploy 後 spike 到上限&lt;/li>
&lt;li>p99 read latency 在事件時段集體爬&lt;/li>
&lt;li>Atlas auto-scaling event log 顯示 &lt;em>triggered too late&lt;/em>&lt;/li>
&lt;li>Cache hit rate 跟 read latency 反向相關&lt;/li>
&lt;/ul>
&lt;p>Case anchor：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &amp;#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase&lt;/a> 是 rich case，含具體數字（deploy 尖峰 &lt;em>connection event rate&lt;/em> ~60K connections / 分鐘 / mongobetween 後 &lt;em>steady-state concurrent connections&lt;/em> 由 ~30K 降到 ~2K — 兩者口徑不同、不是同一數字的連續變化；1.5M reads/sec 含 cache / 70 → 25 分鐘擴容）；&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected&lt;/a> 雙模式負載敘事（持續 sensor + 緊急事件）、&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes&lt;/a> 媒體爆量形狀。&lt;/p></description><content:encoded><![CDATA[<p>MongoDB 大規模 OLTP 的真實架構不是「一個 driver pool 直連 cluster」、是 driver / proxy 層 + cache + freshness token 層 + scaling trigger 層三層協作。讀者最常的誤解是「Coinbase 用 MongoDB 撐 1.5M reads/sec」— 實際是這個合成架構撐出來的量級、單靠 MongoDB cluster 拿不到那個數字。本文把三層各自議題跟整合操作流程講清楚、並對 mongobetween 的部署模型適用範圍給出明確邊界。</p>
<p>本文不重複 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> 的 Atlas / 容量規劃簡介 — 而是 production 部署 + 跨層協作 + 失敗修復的實作層教學。</p>
<h2 id="問題情境大規模-oltp-撞三道牆">問題情境：大規模 OLTP 撞三道牆</h2>
<p>MongoDB 部署規模從中型撐到大規模時、會連環撞三道牆：</p>
<p><strong>Connection ceiling</strong>：應用層 deploy 規模一上來、單一 MongoDB cluster 看到 connection storm。9.C36 Coinbase 揭露具體：Ruby + GVL + blue-green 部署把 instance 數 ×2、連線數隨之 ×2、單一 cluster 看到 60K connections / 分鐘（口徑：Coinbase 特定環境 CRuby + GVL 部署模型）。MongoDB cluster 的 connection limit 撞牆、新 deploy 連不上、線上服務 cascade 失敗。</p>
<p><strong>Read scaling ceiling</strong>：讀者把所有 read 都打 secondary、replica 加到 5-7 仍撐不住 sustained 高 read（&gt;500K reads/sec）。Replication lag 升 + secondary CPU 飽和；單靠 MongoDB cluster 內機制（replica scaling + read preference）拿不到大規模量級。</p>
<p><strong>Scaling reaction lag</strong>：MongoDB cluster 擴容是天級議題、不是即時擴容。9.C36 Coinbase 揭露 reactive scaling 起點到完成 ~70 分鐘（口徑：Coinbase 特定環境、cluster tier / 資料量 / Atlas API 條件下、非 MongoDB 普遍承諾）。Surge 開始時才動來不及、預測性流量必須提前出手。</p>
<p>Surge 形狀又不規則：加密貨幣 surge（隨外部市場波動）/ 媒體爆量（事件驅動）/ IoT 緊急通報（雙模式並存）— 都不適合單純 reactive auto-scaling 接住、必須 predictive + reactive 兩段式。</p>
<p>讀者徵兆：</p>
<ul>
<li>MongoDB Atlas console 看到 connection count 在 deploy 後 spike 到上限</li>
<li>p99 read latency 在事件時段集體爬</li>
<li>Atlas auto-scaling event log 顯示 <em>triggered too late</em></li>
<li>Cache hit rate 跟 read latency 反向相關</li>
</ul>
<p>Case anchor：<a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a> 是 rich case，含具體數字（deploy 尖峰 <em>connection event rate</em> ~60K connections / 分鐘 / mongobetween 後 <em>steady-state concurrent connections</em> 由 ~30K 降到 ~2K — 兩者口徑不同、不是同一數字的連續變化；1.5M reads/sec 含 cache / 70 → 25 分鐘擴容）；<a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a> 雙模式負載敘事（持續 sensor + 緊急事件）、<a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a> 媒體爆量形狀。</p>
<h2 id="核心機制三層合成-frame">核心機制：三層合成 frame</h2>
<p>跨案合成 frame（本章合成、case 原文沒這個 frame）：應用層連 MongoDB cluster 在大規模 production 是 <em>三層協作</em>、不是 driver 一個元件：</p>
<table>
  <thead>
      <tr>
          <th>層次</th>
          <th>角色</th>
          <th>9.C36 Coinbase 對應元件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Driver / Proxy</td>
          <td>連線多工、應用 process 跟 cluster 的橋接</td>
          <td>MongoDB driver + mongobetween proxy</td>
      </tr>
      <tr>
          <td>Cache + freshness token</td>
          <td>read scaling 主路、跨層一致性協議</td>
          <td>Memcached + freshness token + OCC version</td>
      </tr>
      <tr>
          <td>Scaling trigger</td>
          <td>cluster 擴容啟動時機</td>
          <td>ML predictive scaling + reactive fallback</td>
      </tr>
  </tbody>
</table>
<p>三層缺一都會在大規模時撞牆。本文聚焦這三層如何協作、單一層的深度議題（read preference 機制、schema 治理、aggregation pipeline）推到 sibling。</p>
<h3 id="driver--proxy-層">Driver / Proxy 層</h3>
<p>MongoDB driver 原生 connection 模式：driver 在 application process 內維護 connection pool、每個 process 跟 MongoDB cluster 開固定數量 socket。但 driver <strong>沒跨 process pool</strong> — 多個 process 共用同一台機器、每個 process 自己一份 pool、cluster 看到的是 N 倍 connection。跟 PostgreSQL 走 pgbouncer 是同樣需求。</p>
<p>Connection storm 的具體 trigger：</p>
<ul>
<li><strong>部署模型放大 process 數</strong>：CRuby + GVL 強制每 CPU core 一 process、blue-green 部署 instance 數 ×2、連線數隨之 ×2（9.C36 Coinbase 揭露：單 cluster 看到 60K connections/min）</li>
<li><strong>微服務數量多</strong>：50+ microservice 各自連 cluster、每服務 connection 加總後撞上限（9.C37 Forbes 50+ 微服務情境對照）</li>
</ul>
<p>mongobetween proxy（Coinbase 自建）：把多 application process 的連線合成少量到 MongoDB cluster 的連線。9.C36 揭露兩個獨立口徑、不是同一數字的連續變化：deploy 尖峰時 <em>connection event rate</em> 是 ~60K connections / 分鐘（unique connection 事件量、rate）；mongobetween 介入後 <em>steady-state concurrent connection 數</em> 由 ~30K 降到 ~2K（瞬時量、前後對比、一個量級）。引用時把 rate 跟瞬時 concurrent count 分開、不要壓成「60K 收斂到 2K」。</p>
<p><strong>Scope warning（必明示）</strong>：mongobetween 是 Coinbase 為 Ruby + GVL 需求自建、case 自承「Go / Java / Node.js 應用因原生支援連線多工、通常不需要這層 proxy」。寫進設計文件時不可寫成「MongoDB 在大規模都需要 mongobetween」、要寫成「特定部署模型才需要」。</p>
<h3 id="cache--freshness-token-層">Cache + freshness token 層</h3>
<p>直接打 MongoDB 不可能撐 1.5M reads/sec（口徑：users 服務應用層觀察、含 cache、非 MongoDB cluster 純讀取）。Coinbase 在 users 服務前面加 Memcached query cache、單 document query 先查 cache。</p>
<p>跨層一致性問題：write 進 MongoDB primary、cache 還是舊版、user 下次 read 拿到舊資料。</p>
<p><a href="/blog/backend/knowledge-cards/freshness-token/" data-link-title="Freshness Token" data-link-desc="DB write 後返回的版本 token、後續 read 帶 token、保證 read 看到的資料 ≥ token 版本、解 DB &#43; cache 跨層 read-after-write">Freshness Token</a> 機制：</p>
<ol>
<li>Write 成功後給 client token（含 OCC version / clusterTime）</li>
<li>Client read 帶 token</li>
<li>Server 保證返回的資料版本 ≥ token</li>
<li>必要時 bypass cache 直接打 DB</li>
</ol>
<p>跟 DB 層 causal consistency session 對照：causal session 解 MongoDB 內 read-your-own-write、freshness token 解 <em>DB + cache 跨層</em> read-your-own-write。機制細節見 <a href="../replica-set-read-preference/">replica set read preference</a>、本文不重複展開。</p>
<p><strong>Scope warning（必明示）</strong>：1.5M reads/sec 是 <em>users 服務 + cache</em> 合成數字、不是 MongoDB cluster 純讀取 benchmark。寫進設計文件必須明示口徑、避免讀者把 1.5M reads/sec 當成「MongoDB 單獨能撐」。</p>
<h3 id="scaling-trigger-層">Scaling trigger 層</h3>
<p>MongoDB cluster 擴容時間：傳統 reactive scaling 起點到完成 ~70 分鐘（9.C36 Coinbase 揭露口徑：含 instance provisioning + 資料 sync + balancer rebalance、特定 Atlas tier / 資料量條件）。</p>
<p>Reactive 為主撐不住快變流量：CPU / queue 觸發 reactive scaling 在 surge 開始時才動、來不及；surge 已經結束擴容才到位。</p>
<p>Predictive scaling 機制（Coinbase 揭露）：</p>
<ul>
<li>用外部訊號（加密貨幣價格、賽事行程、票務開賣時間）訓練 ML 模型</li>
<li>提前 60 分鐘預測流量</li>
<li>預先擴容</li>
<li>把擴容啟動時間從 70 分鐘壓到 25 分鐘（口徑：trigger 提前、不是擴容本身變快）</li>
</ul>
<p><strong>Scope warning（必明示）</strong>：case 警示「ML 預測有 false positive / false negative、Coinbase 沒揭露準確率、所以仍保留 reactive scaling 作為 safety net」。寫進設計文件要明示兩段式設計、不可寫成「Predictive scaling 取代 reactive scaling」。</p>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection-pool</a>、<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale-read</a>、<a href="/blog/backend/knowledge-cards/session-consistency/" data-link-title="Session Consistency" data-link-desc="同一使用者工作階段內維持讀寫一致、跨工作階段允許短暫不一致">session-consistency</a>、<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot-partition</a>（cache 失效時打穿 DB 的 hot key）。</p>
<h2 id="操作流程">操作流程</h2>
<p><strong>Step 1：connection ceiling audit</strong>。量測現有 deploy 在 peak 的 connection count、推算 deploy ×2 / 微服務新增時 connection 走勢；對照 MongoDB cluster 的 hard limit（Atlas tier 決定、典型 1500-32000）。</p>
<p><strong>Step 2：部署模型判讀</strong>。</p>
<table>
  <thead>
      <tr>
          <th>部署模型</th>
          <th>是否需 proxy 層</th>
          <th>原因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CRuby + GVL（process-per-core）</td>
          <td>需要</td>
          <td>每 core 一 process、連線隨 process 線性升</td>
      </tr>
      <tr>
          <td>大量微服務（50+）+ 各自 deploy</td>
          <td>需要</td>
          <td>微服務 connection 加總撞 cluster limit</td>
      </tr>
      <tr>
          <td>Blue-green 部署（雙環境並存）</td>
          <td>需要</td>
          <td>部署期間連線 ×2、容易撞 cluster ceiling</td>
      </tr>
      <tr>
          <td>Go / Java / Node.js 單一 binary + 多 thread</td>
          <td>通常不需要</td>
          <td>原生 driver pool 跨 thread 共用、收斂效率高</td>
      </tr>
  </tbody>
</table>
<p><strong>Step 3：proxy 選型</strong>。Coinbase mongobetween 是參考實作、社群還有 mongoproxy / DocumentDB 內建 connection multiplexer。自建 proxy 是 Coinbase 規模才合理、中型團隊先評估 Atlas tier 升級。</p>
<p><strong>Step 4：cache layer 設計</strong>（read scaling 主路）：</p>
<ul>
<li>前置 Memcached / Redis、cache key = collection + document id + version</li>
<li>Write API 返回 <code>{result, version_token}</code> — token 含 OCC version 或 MongoDB clusterTime</li>
<li>Read API 接受 optional version token、cache lookup 比對 entry version 跟 token、低於就 invalidate + bypass</li>
<li>DB 層 fallback <code>readConcern: &quot;majority&quot;</code> 保證返回 version ≥ token</li>
</ul>
<p><strong>Step 5：predictive scaling 設計</strong>（適用「外部訊號可預測流量」）：</p>
<ul>
<li><strong>識別 driver 訊號</strong>：加密貨幣價格 / 賽事行程 / 票務開賣 / 促銷活動 / IoT 緊急事件預警</li>
<li><strong>訓練 ML</strong>：用歷史流量 vs 訊號 correlation 訓練、輸出未來 30-60 分鐘流量預測</li>
<li><strong>觸發擴容</strong>：預測超 threshold 時主動 trigger Atlas scaling API、不等 reactive metric</li>
<li><strong>保留 reactive safety net</strong>：ML failure 時 reactive scaling 仍會接、不可拿掉</li>
</ul>
<p><strong>Step 6：全鏈路驗證</strong>。Staging 灌入 deploy ×2 模擬 connection storm、灌入 stale cache 驗證 freshness token bypass、放假流量驗證 predictive scaling trigger。</p>
<p>驗證點：</p>
<ul>
<li>Connection count 在 deploy 後不爆 cluster limit</li>
<li>Cache hit rate vs freshness bypass rate 比例正常（cache hit &gt; 90% + bypass &lt; 5% 屬通用工程估算、case 未揭露具體數字）</li>
<li>Predictive scaling 領先窗 ≥ 30 分鐘</li>
<li>Reactive scaling 仍保留作 safety</li>
</ul>
<p>Rollback boundary：</p>
<ul>
<li>Proxy 層可下線（流量改直連 cluster、但短時 connection storm 風險回來）</li>
<li>Cache 層可下線（read 全部打 DB、需 cluster 容量能撐）</li>
<li>Predictive scaling 可下線（退回純 reactive、但快變 surge 接不住）</li>
<li>三層都要設計 graceful degradation、不是全有全無</li>
</ul>
<h2 id="失敗模式">失敗模式</h2>
<p><strong>Connection storm during deploy</strong>：blue-green 部署 instance 數 ×2、connection 隨之爆、新 deploy 連不上 cluster、cascade 失敗。修法是 proxy 層 + cluster connection limit 預留 headroom（典型留 30% buffer、屬通用工程估算）。</p>
<p><strong>Proxy 變成單點瓶頸</strong>：mongobetween / pgbouncer 風格 proxy 自己變熱點、proxy 故障時下游全死。修法是 proxy 叢集 + health check + 客戶端 retry、跟 application 同 region 共部署降低 proxy ↔ application 的網路 RTT。</p>
<p><strong>Cache hit rate 崩塌</strong>：cache 失效 + 大量 read bypass、DB 突然吃 100% 流量、cluster 飽和。修法是 freshness token 設計時要監控 bypass rate、過高表示 cache invalidation 邏輯有問題、cache 沒在 write 後 update / invalidate。</p>
<p><strong>Freshness token 漏寫</strong>：write 沒帶 token / client 沒帶 token、token silently 失效、user 拿到舊資料。修法是 protocol 強制（middleware 攔截 write / read、自動帶 token）、不能靠 application 自覺。</p>
<p><strong>Predictive scaling false positive 浪費容量</strong>：ML 預測 surge 但實際沒來、cluster 預先擴容後閒置。接受成本、保留 ML model retraining、定期評估 precision / recall。</p>
<p><strong>Predictive scaling false negative 漏接 surge</strong>：ML 沒預測到、cluster 沒提前擴、surge 來時 reactive scaling 開始動但 70 分鐘來不及。修法是 reactive safety net + 服務降級（限流 / 部分 read 降級拿舊資料 + freshness token 告警）。</p>
<p><strong>三層協作脫節</strong>：proxy 擋住 connection storm 但 cluster 內部 read scaling 沒設計、application 仍打爆。三層必須一起設計、不是各自獨立。</p>
<p>Anti-recommendation：</p>
<ul>
<li>中小流量（&lt; 100K reads/sec、單 deploy &lt; 50 instance）不需要這三層；Atlas tier 升級 + cluster 內 replica + 簡單 cache 就夠</li>
<li>mongobetween 風格 proxy 只在 Ruby + GVL / 類似部署模型才必要、Go / Java / Node.js 通常不需要（case 自承）</li>
<li>Predictive scaling 只在外部訊號可預測時有效；無預測訊號的純隨機 surge 還是回 reactive + headroom</li>
<li>大規模 OLTP 不該為了省成本拿掉 cache 層；read scaling 主路就是 cache、單靠 MongoDB cluster 拿不到 1.5M reads/sec 量級</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<p>關鍵 metric：</p>
<ul>
<li><strong>Connection 層</strong>：cluster connection count / Atlas tier limit / proxy 到 cluster 的 connection multiplex 比、deploy 前後 connection 走勢</li>
<li><strong>Cache 層</strong>：cache hit rate、freshness token bypass rate、cache key collision rate</li>
<li><strong>Scaling 層</strong>：predictive scaling trigger event count / 領先窗、reactive scaling fallback 觸發頻率、實際擴容啟動到完成時間、ML 預測準確率（precision / recall）</li>
</ul>
<p>Mongo / Atlas command：</p>
<ul>
<li><code>db.serverStatus().connections</code>：cluster 當前 connection 統計</li>
<li><code>db.currentOp({})</code>：看 connection 使用</li>
<li>Atlas API：cluster scaling event log</li>
<li>Proxy admin metric：connection multiplex 比、上下游 latency</li>
</ul>
<p>Application observability：APM 看 connection acquire latency、cache hit rate time series、freshness token 流動完整性（write 是否發 token、read 是否帶 token、cache 是否驗 token）。</p>
<p>回到 <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</a>：把 connection storm event、cache hit rate / bypass rate、scaling trigger leadtime 列為跨層 evidence 三件套。</p>
<p>回到 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 bottleneck localization</a>：大規模 OLTP 撞牆時要區分 (a) connection ceiling (b) cache hit rate 下降 (c) cluster 內 replica 飽和 (d) scaling 跟不上。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<p>Sibling deep articles：</p>
<ul>
<li><a href="../replica-set-read-preference/">replica set read preference</a> — DB 層 causal session 機制、freshness token 跨層協議；本文聚焦三層協作、那篇聚焦 DB 層機制</li>
<li><a href="../shard-key-selection/">shard key selection</a> — cluster 擴容是天級議題、是 scaling layer 的 trigger；單 cluster vs 多 cluster 切分</li>
<li><a href="../schema-design-pattern/">schema design pattern</a> — app-layer abstraction 跟本文 cache + freshness token 同層協作、contract layer 三選一</li>
<li><a href="../aggregation-pipeline-optimization/">aggregation pipeline optimization</a> — report dashboard 跑爆 primary 的補位路徑是本文的 cache + read scaling、不是讓 aggregation 自己優化</li>
</ul>
<p>Migration playbook：</p>
<ul>
<li><strong>Federated DB 模式</strong>（9.C36 Coinbase 揭露：MongoDB + DynamoDB）— 不是「全用 MongoDB」、document-shaped 用 MongoDB、access pattern 固定的 KV 用 DynamoDB；對應 <a href="/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB vendor page</a> 跨 vendor 對照</li>
<li><strong>跨雲 hedging</strong>（9.C37 Forbes 跨雲彈性）— Atlas 跨 AWS / GCP / Azure 是規避未來雲商鎖定的 selection 訊號</li>
</ul>
<p>跟 1.x 互引：</p>
<ul>
<li><a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a> — connection storm 通用模式（pgbouncer / mongobetween 對應）</li>
<li><a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> — 三層架構列為大規模 OLTP 容量規劃必看點</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> — predictive scaling 的 ML 訓練紀律</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> — 本文是該頁尾「connection management + Atlas scaling」backlog 的深度展開</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a> — 三層合成 rich case</li>
<li><a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a> — 媒體爆量形狀</li>
<li><a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a> — IoT 雙模式負載</li>
<li>官方：<a href="https://www.mongodb.com/docs/manual/reference/connection-string-options/">MongoDB Connection Pool Options</a>、<a href="https://www.mongodb.com/docs/atlas/cluster-autoscaling/">Atlas Auto-Scaling</a>、<a href="https://github.com/coinbase/mongobetween">mongobetween GitHub</a></li>
</ul>
]]></content:encoded></item><item><title>MongoDB Aggregation Pipeline Optimization：stage 順序、index 配合與 memory 邊界</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/aggregation-pipeline-optimization/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/aggregation-pipeline-optimization/</guid><description>&lt;p>MongoDB aggregation pipeline 是 document model 做 analytical query 的主要介面、stage stream 設計直觀但 production 容易踩雷 — 上線時 200ms、半年後資料量翻倍變 8s、加 index 沒用；profiler 顯示 stage 之間在 memory 累積上百 MB temp data。Aggregation pipeline 的最佳化跟 RDBMS 的 SQL planner 完全不同邏輯 — RDBMS 靠 planner 自動重排 join / filter、MongoDB 靠寫 query 的人手動排 stage 順序。本文把 stage 機制、index 配合、memory 邊界、cross-shard 限制講清楚、並對「report dashboard 跑爆 primary」這個常見 anti-pattern 給治理路徑。&lt;/p>
&lt;p>本文不重複 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview&lt;/a> 已寫過的 aggregation 簡介 — 而是 production tuning + 失敗修復的實作層教學。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>前置閱讀&lt;/strong>：MongoDB workload 適配判讀（document shape 主導 / contract layer 該放哪 / 跨雲 hedging 是否需要）見 &lt;a href="../schema-design-pattern/#%e5%95%8f%e9%a1%8c%e6%83%85%e5%a2%83document-%e8%87%aa%e7%94%b1%e7%9a%84%e5%be%8c%e5%ba%a7%e5%8a%9b">schema-design-pattern 開頭 3 軸前置判讀&lt;/a>。本文聚焦 aggregation pipeline 操作層、是 &lt;em>已選 MongoDB 後&lt;/em> 的 query 層工程議題、不重複前置判讀。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境aggregation-是-hot-path-的反模式">問題情境：aggregation 是 hot path 的反模式&lt;/h2>
&lt;p>典型觸發場景：報表 pipeline 上線時 200ms、半年後資料量翻倍變 8s、加 index 沒用；profiler 顯示 stage 之間在 memory 累積上百 MB temp data。&lt;/p>
&lt;p>進一步徵兆：&lt;/p>
&lt;ul>
&lt;li>「OLTP collection 上跑 analytical query」的混合 workload：把 &lt;code>$group + $lookup + $sort&lt;/code> 接成長 pipeline、aggregation 把整個 working set 從 cache 擠走&lt;/li>
&lt;li>Sharded cluster 上跑 cross-shard aggregation：&lt;code>$group&lt;/code> / &lt;code>$sort&lt;/code> 必須在 mongos 合併、mongos 變單點瓶頸&lt;/li>
&lt;li>&lt;code>$lookup&lt;/code> 出現在 hot path：每筆 input doc 都要去另一個 collection 查、嚴格意義上是 N+1&lt;/li>
&lt;li>&lt;code>db.serverStatus().metrics.aggStageCounters&lt;/code> 飆、&lt;code>executionStats.executionTimeMillis&lt;/code> 跟 doc 數線性增長&lt;/li>
&lt;li>Profiler 報 &lt;code>usedDisk: true&lt;/code>、aggregation OOM kill &lt;code>QueryExceededMemoryLimitNoDiskUseAllowed&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Case anchor：report dashboard 跑爆 primary 的具體 incident 細節需未來 case 補完、本文以「常見 anti-pattern」處理、不憑空編造 incident 數字。側面引用 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365&lt;/a> — 從 MongoDB 把 analytics 分離出來的 driver。&lt;/p></description><content:encoded><![CDATA[<p>MongoDB aggregation pipeline 是 document model 做 analytical query 的主要介面、stage stream 設計直觀但 production 容易踩雷 — 上線時 200ms、半年後資料量翻倍變 8s、加 index 沒用；profiler 顯示 stage 之間在 memory 累積上百 MB temp data。Aggregation pipeline 的最佳化跟 RDBMS 的 SQL planner 完全不同邏輯 — RDBMS 靠 planner 自動重排 join / filter、MongoDB 靠寫 query 的人手動排 stage 順序。本文把 stage 機制、index 配合、memory 邊界、cross-shard 限制講清楚、並對「report dashboard 跑爆 primary」這個常見 anti-pattern 給治理路徑。</p>
<p>本文不重複 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> 已寫過的 aggregation 簡介 — 而是 production tuning + 失敗修復的實作層教學。</p>
<blockquote>
<p><strong>前置閱讀</strong>：MongoDB workload 適配判讀（document shape 主導 / contract layer 該放哪 / 跨雲 hedging 是否需要）見 <a href="../schema-design-pattern/#%e5%95%8f%e9%a1%8c%e6%83%85%e5%a2%83document-%e8%87%aa%e7%94%b1%e7%9a%84%e5%be%8c%e5%ba%a7%e5%8a%9b">schema-design-pattern 開頭 3 軸前置判讀</a>。本文聚焦 aggregation pipeline 操作層、是 <em>已選 MongoDB 後</em> 的 query 層工程議題、不重複前置判讀。</p></blockquote>
<h2 id="問題情境aggregation-是-hot-path-的反模式">問題情境：aggregation 是 hot path 的反模式</h2>
<p>典型觸發場景：報表 pipeline 上線時 200ms、半年後資料量翻倍變 8s、加 index 沒用；profiler 顯示 stage 之間在 memory 累積上百 MB temp data。</p>
<p>進一步徵兆：</p>
<ul>
<li>「OLTP collection 上跑 analytical query」的混合 workload：把 <code>$group + $lookup + $sort</code> 接成長 pipeline、aggregation 把整個 working set 從 cache 擠走</li>
<li>Sharded cluster 上跑 cross-shard aggregation：<code>$group</code> / <code>$sort</code> 必須在 mongos 合併、mongos 變單點瓶頸</li>
<li><code>$lookup</code> 出現在 hot path：每筆 input doc 都要去另一個 collection 查、嚴格意義上是 N+1</li>
<li><code>db.serverStatus().metrics.aggStageCounters</code> 飆、<code>executionStats.executionTimeMillis</code> 跟 doc 數線性增長</li>
<li>Profiler 報 <code>usedDisk: true</code>、aggregation OOM kill <code>QueryExceededMemoryLimitNoDiskUseAllowed</code></li>
</ul>
<p>Case anchor：report dashboard 跑爆 primary 的具體 incident 細節需未來 case 補完、本文以「常見 anti-pattern」處理、不憑空編造 incident 數字。側面引用 <a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — 從 MongoDB 把 analytics 分離出來的 driver。</p>
<h2 id="核心機制">核心機制</h2>
<p>Aggregation pipeline 是 stage 序列：每個 stage 接 stream of document、產出 stream of document。Stage 順序直接決定後續 stage 處理量 — 第一個 stage 是 IXSCAN 還是 COLLSCAN、<code>$match</code> 推到前面還是後面、<code>$project</code> 早 drop 還是晚 drop、都會放大或縮小後續 cost。</p>
<p><strong>Optimizer rewrite</strong>：MongoDB 會自動把 <code>$match</code> / <code>$project</code> 往前推、把 <code>$sort + $limit</code> 合併成 top-K、但不保證所有 case。用 <code>explain(&quot;executionStats&quot;)</code> 看 rewrite 後的 effective pipeline、不要靠原始 pipeline 推斷實際執行順序。</p>
<p><strong>Index 配合</strong>：pipeline 的 <em>第一個 stage</em> 若是 <code>$match</code> 或 <code>$sort</code>、且能對到 index、就走 IXSCAN。中間 stage 都是 in-memory stream、沒 index 概念。所以 <code>$match</code> 永遠該排第一、配合對應 index。</p>
<p><strong>Memory 邊界</strong>：每個 aggregation stage 預設 100MB memory 上限、超過要 <code>allowDiskUse: true</code>（4.2+ 是預設）。Disk spill 啟動後 IO 嚴重拖慢、aggregation 變慢 50-100x。</p>
<p><strong><code>$lookup</code> 在 sharded cluster</strong>：foreign collection 不能 sharded（5.0 前完全不行、5.0+ 有限放寬）；<code>$lookup</code> 本質是 nested loop join、沒 hash join / merge join — 對大 collection 不可用。</p>
<p><strong><code>$facet</code> 平行多 pipeline</strong>：但所有 facet 共享同一個 100MB 限制、複雜 facet 容易撞 memory ceiling。</p>
<p><strong><code>$merge</code> / <code>$out</code></strong>：把結果寫回 collection（pre-computed view / materialized view）— 把 hot analytical query 移出 read path、是治理 anti-pattern 的主要工具。</p>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot-partition</a>（aggregation 集中讀單 shard 的副作用）、<a href="/blog/backend/knowledge-cards/document-store/" data-link-title="Document Store" data-link-desc="說明以 JSON 文件與彈性 schema 提供資料存取的模式，以及它仍需的治理邊界">document-store</a>、<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale-read</a>（從 secondary 跑 aggregation 的 trade-off）。</p>
<h2 id="操作流程">操作流程</h2>
<p><strong>Step 0：把壞 pipeline 跟好 pipeline 並排</strong>。看一個簡化但典型的優化：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 壞：lookup 在 match 前、sort 沒 limit、project 在最後
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">db</span><span class="p">.</span><span class="nx">orders</span><span class="p">.</span><span class="nx">aggregate</span><span class="p">([</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="p">{</span> <span class="nx">$lookup</span><span class="o">:</span> <span class="p">{</span> <span class="nx">from</span><span class="o">:</span> <span class="s2">&#34;users&#34;</span><span class="p">,</span> <span class="nx">localField</span><span class="o">:</span> <span class="s2">&#34;userId&#34;</span><span class="p">,</span> <span class="nx">foreignField</span><span class="o">:</span> <span class="s2">&#34;_id&#34;</span><span class="p">,</span> <span class="nx">as</span><span class="o">:</span> <span class="s2">&#34;user&#34;</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="p">{</span> <span class="nx">$match</span><span class="o">:</span> <span class="p">{</span> <span class="nx">status</span><span class="o">:</span> <span class="s2">&#34;completed&#34;</span><span class="p">,</span> <span class="s2">&#34;user.region&#34;</span><span class="o">:</span> <span class="s2">&#34;ap-tokyo&#34;</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="p">{</span> <span class="nx">$sort</span><span class="o">:</span> <span class="p">{</span> <span class="nx">createdAt</span><span class="o">:</span> <span class="o">-</span><span class="mi">1</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="p">{</span> <span class="nx">$project</span><span class="o">:</span> <span class="p">{</span> <span class="nx">_id</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">total</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">createdAt</span><span class="o">:</span> <span class="mi">1</span> <span class="p">}</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">])</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">// 好：可推前的 match 寫前面、sort + limit 配對、project 早寫
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="nx">db</span><span class="p">.</span><span class="nx">orders</span><span class="p">.</span><span class="nx">aggregate</span><span class="p">([</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="p">{</span> <span class="nx">$match</span><span class="o">:</span> <span class="p">{</span> <span class="nx">status</span><span class="o">:</span> <span class="s2">&#34;completed&#34;</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="p">{</span> <span class="nx">$sort</span><span class="o">:</span> <span class="p">{</span> <span class="nx">createdAt</span><span class="o">:</span> <span class="o">-</span><span class="mi">1</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="p">{</span> <span class="nx">$limit</span><span class="o">:</span> <span class="mi">100</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="p">{</span> <span class="nx">$lookup</span><span class="o">:</span> <span class="p">{</span> <span class="nx">from</span><span class="o">:</span> <span class="s2">&#34;users&#34;</span><span class="p">,</span> <span class="nx">localField</span><span class="o">:</span> <span class="s2">&#34;userId&#34;</span><span class="p">,</span> <span class="nx">foreignField</span><span class="o">:</span> <span class="s2">&#34;_id&#34;</span><span class="p">,</span> <span class="nx">as</span><span class="o">:</span> <span class="s2">&#34;user&#34;</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="p">{</span> <span class="nx">$match</span><span class="o">:</span> <span class="p">{</span> <span class="s2">&#34;user.region&#34;</span><span class="o">:</span> <span class="s2">&#34;ap-tokyo&#34;</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="p">{</span> <span class="nx">$project</span><span class="o">:</span> <span class="p">{</span> <span class="nx">_id</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">total</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">createdAt</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">&#34;user.name&#34;</span><span class="o">:</span> <span class="mi">1</span> <span class="p">}</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">])</span></span></span></code></pre></div><p>差別：壞 pipeline 對整個 orders 做 lookup、然後才過濾；好 pipeline 先過濾 + top-100、只對 100 筆做 lookup、再過濾 lookup 結果。實際 collection 大時兩者差 50-100x。</p>
<p><strong>Step 1：拿 explain plan</strong>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">coll</span><span class="p">.</span><span class="nx">explain</span><span class="p">(</span><span class="s2">&#34;executionStats&#34;</span><span class="p">).</span><span class="nx">aggregate</span><span class="p">([...])</span></span></span></code></pre></div><p>看 <code>stages[]</code> 顯示 rewrite 後的 effective pipeline、<code>executionTimeMillis</code>、<code>totalDocsExamined / totalDocsReturned</code> 比值、是否 <code>usedDisk</code>。</p>
<p><strong>Step 2：把 <code>$match</code> 推到最前</strong>。越早過濾、後續 stage 處理量越小。Optimizer 通常自己會推、但 <code>$lookup</code> 之後的 <code>$match</code> 不會自動推到 <code>$lookup</code> 之前 — 因為 lookup 出的欄位才能被那個 match 用、邏輯依賴。寫 query 時就把能推前的 <code>$match</code> 寫前面。</p>
<p><strong>Step 3：對 <code>$match</code> 欄位建 compound index</strong>。確保 <code>executionStages</code> 顯示 <code>IXSCAN</code> 而不是 <code>COLLSCAN</code>。Compound index 順序敏感 — <code>{ status: 1, createdAt: -1 }</code> 對 <code>{ status: ..., createdAt: $gte: ... }</code> 高效、對 <code>{ createdAt: $gte: ... }</code> 走不到 index。</p>
<p><strong>Step 4：<code>$sort + $limit</code> 寫在一起</strong>。Optimizer 才會推 top-K（不需要 full sort、只需要 heap）。單 <code>$sort</code> 不限 limit 會做 full sort、容易撞 memory。</p>
<p><strong>Step 5：<code>$project</code> 早寫</strong>。把不需要的欄位早期 drop、減少後續 stage 處理 doc size。對大 document 特別有效。</p>
<p><strong>Step 6：把 hot analytical pipeline 寫成 materialized view</strong>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nx">orders</span><span class="p">.</span><span class="nx">aggregate</span><span class="p">([</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="p">{</span> <span class="nx">$match</span><span class="o">:</span> <span class="p">{</span> <span class="nx">createdAt</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$gte</span><span class="o">:</span> <span class="nx">ISODate</span><span class="p">(</span><span class="s2">&#34;2026-05-01&#34;</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="p">{</span> <span class="nx">$group</span><span class="o">:</span> <span class="p">{</span> <span class="nx">_id</span><span class="o">:</span> <span class="s2">&#34;$customerId&#34;</span><span class="p">,</span> <span class="nx">total</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$sum</span><span class="o">:</span> <span class="s2">&#34;$amount&#34;</span> <span class="p">}</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="p">{</span> <span class="nx">$merge</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">      <span class="nx">into</span><span class="o">:</span> <span class="s2">&#34;monthly_customer_summary&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      <span class="nx">on</span><span class="o">:</span> <span class="s2">&#34;_id&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      <span class="nx">whenMatched</span><span class="o">:</span> <span class="s2">&#34;merge&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      <span class="nx">whenNotMatched</span><span class="o">:</span> <span class="s2">&#34;insert&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="p">}}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">])</span></span></span></code></pre></div><p>定時更新（cron / 5 分鐘一次）、application 讀 materialized view 而不是即時跑 aggregation。</p>
<p><strong>Step 7：sharded cluster 處理</strong>。避免在 hot path 用 cross-shard <code>$lookup</code> / <code>$group</code>、或把這類 query 路由到 analytical replica（用 tag set + read preference）、見 <a href="../replica-set-read-preference/">replica set read preference</a>。</p>
<p>驗證點：</p>
<ul>
<li><code>executionTimeMillis</code> 在預期 budget 內</li>
<li><code>totalDocsExamined / totalDocsReturned</code> 比值接近 1（過濾效率高）</li>
<li>無 <code>usedDisk: true</code></li>
<li>無 stage 看到 <code>inMemory &gt; 50MB</code></li>
</ul>
<p>Rollback boundary：pipeline 改寫是 application code 變更、可以灰度；materialized view（<code>$merge</code>）需備份 target collection 才能還原。</p>
<h3 id="典型-tuning-過程200ms--8s--250ms">典型 tuning 過程（200ms → 8s → 250ms）</h3>
<p>一個常見的 production pipeline 演化路徑：</p>
<ol>
<li><strong>上線時 200ms</strong>：collection 100K doc、<code>$match</code> 過濾 95%、<code>$lookup</code> 只跑 5K 次、in-memory <code>$sort</code> 處理 5K row 在 100MB 內</li>
<li><strong>半年後 8s</strong>：collection 長到 2M doc、<code>$match</code> 仍過濾 95% 但變 100K row、<code>$lookup</code> 跑 100K 次（5K → 100K 是 20x）、<code>$sort</code> 在 in-memory 撞 100MB 開始 disk spill、IO 100x 退化</li>
<li><strong>加 compound index 沒用</strong>：index 是給 <code>$match</code> 用的、但 <code>$match</code> 之後的 stage（<code>$lookup</code> / <code>$sort</code>）走的是 in-memory pipeline、index 救不了</li>
<li><strong>修法到 250ms</strong>：(a) <code>$sort + $limit</code> 配對讓 optimizer 走 top-K、避免 full sort (b) 改 schema embed 把 <code>$lookup</code> 拿掉（見 <a href="../schema-design-pattern/">schema design pattern</a>）(c) hot pipeline 寫成 <code>$merge</code> materialized view、application 讀 view 不跑 aggregation</li>
</ol>
<p>關鍵教訓：aggregation 慢的原因不在 query 本身、在 <em>資料形狀演進</em>。Index 是 hot path 的第一個槓桿、但只對 <code>$match</code> / <code>$sort</code> 第一 stage 有效；後續 stage 要靠 stage 順序、materialized view、schema denormalize 來救。</p>
<h2 id="失敗模式">失敗模式</h2>
<p><strong><code>$lookup</code> 在 hot path</strong>：list page 每行去另一 collection 查、p99 隨 page size 線性增。應在 schema design 階段 denormalize、把 read-together 資料 embed 回 aggregate root（見 <a href="../schema-design-pattern/">schema design pattern</a>）。</p>
<p><strong><code>$sort</code> 不帶 limit + 沒 index</strong>：全表 in-memory sort、撞 100MB 限制 → OOM 或 disk spill。<code>allowDiskUse: true</code> 解 OOM 但 IO 100x 退化。修法是建對應 index 走 IXSCAN sort、或限 limit 走 top-K。</p>
<p><strong>Sharded cluster cross-shard aggregation</strong>：<code>$group</code> 階段所有 partial result 跑到 mongos 合併、mongos memory + CPU 爆。修法是 group key 包含 shard key prefix（讓 group 在 shard 內完成）、或路由到 analytical replica 跑。</p>
<p><strong>Stage 順序錯</strong>：<code>$lookup</code> 放在 <code>$match</code> 前、等於對全表都做 lookup 再過濾、每個 input doc 都觸發 lookup。<code>$match</code> 永遠該排第一。</p>
<p><strong>Aggregation 把 working set 擠走</strong>：OLTP 的 hot page 被 aggregation 的 cold scan 擠出 cache、整體 query latency 一起退化。修法是 analytical workload 跟 OLTP read 隔離（read preference tag）、或搬走 analytical（見下面 anti-recommendation）。</p>
<p><strong><code>$facet</code> 滿載</strong>：四個 facet 各跑大 pipeline、共享 100MB 限制立刻爆。修法是拆成獨立 query、不要硬塞 facet。</p>
<p>Anti-recommendation：</p>
<ul>
<li><strong>報表 / BI / analytics workload 跑 MongoDB primary 是反模式</strong>：應該 (a) 設定 analytical secondary + read preference tag (b) 用 <code>$merge</code> 寫到 reporting collection (c) 進階用 BI Connector / data lake / 把 analytical workload 整批搬到 <a href="https://clickhouse.com">ClickHouse</a> / BigQuery</li>
<li><strong>「report dashboard 跑爆 primary」典型 anti-pattern</strong>：BI 工具直連 MongoDB primary 跑長 pipeline、cache eviction 把 OLTP working set 擠走、p99 latency 在報表時段集體升。沒拿到具體 incident 數字、不在本文編造、改寫成「常見 anti-pattern」並推到治理路徑</li>
<li><strong>Aggregation 不能解 read scaling</strong>：aggregation 是 OLTP 的補位、不是 read scaling 的主路。Read scaling 在大規模 OLTP 走 cache + freshness token（見 <a href="../connection-management-and-cache-layer/">connection management and cache layer</a>）、不是把 aggregation 跑爆 secondary</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<p>關鍵 metric：</p>
<ul>
<li>Aggregation operation time 分布</li>
<li>Disk spill 次數</li>
<li><code>opcounters.command</code> 中 aggregate 比例</li>
<li>Cache eviction rate 在 aggregation 高峰時的變化</li>
</ul>
<p>Mongo command：</p>
<ul>
<li><code>db.currentOp({ &quot;command.aggregate&quot;: { $exists: true } })</code>：當前 aggregation 在跑</li>
<li><code>db.serverStatus().metrics.aggStageCounters</code>：stage 級別 counter</li>
<li><code>explain(&quot;executionStats&quot;)</code>：單 query 詳細分析</li>
</ul>
<p>Profiler：<code>db.setProfilingLevel(1, {slowms: 200})</code>、看 <code>usedDisk</code> flag 跟 <code>numYield</code>。</p>
<p>回到 <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</a>：aggregation slow log + cache hit ratio + disk spill rate 是「analytical 壓力」的 evidence 三件套。</p>
<p>回到 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 bottleneck localization</a>：用 explain executionStats 把 pipeline stage 對到瓶頸（IXSCAN 還是 COLLSCAN、in-memory 還是 disk spill、shard-local 還是 mongos merge）。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<p>Sibling deep articles：</p>
<ul>
<li><a href="../schema-design-pattern/">schema design pattern</a> — embedded 設計可消除大部分 <code>$lookup</code></li>
<li><a href="../shard-key-selection/">shard key selection</a> — 決定 aggregation 是 shard-local 還是 cross-shard</li>
<li><a href="../replica-set-read-preference/">replica set read preference</a> — aggregation 跑 secondary 的 stale read trade-off</li>
<li><a href="../connection-management-and-cache-layer/">connection management and cache layer</a> — report dashboard 跑爆 primary 時的 cache + read scaling 主路</li>
</ul>
<p>Migration playbook：analytical workload 大到不能繼續混在 MongoDB → split 出 <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">→ Cosmos DB MongoDB API + Synapse</a> 或 <a href="/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">→ DynamoDB + Athena/Glue</a>（access pattern 重設計）。</p>
<p>跟 1.x 互引：<a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> 把 aggregation 列為 read-shape 的成本維度；<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a> 處理「OLTP + analytical 同 cluster」的反模式。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> — 本文是該頁尾「aggregation pipeline optimization」backlog 的深度展開</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a></li>
<li>官方：<a href="https://www.mongodb.com/docs/manual/aggregation/">Aggregation Pipeline</a>、<a href="https://www.mongodb.com/docs/manual/core/aggregation-pipeline-optimization/">Optimize Pipelines</a>、<a href="https://www.mongodb.com/docs/manual/reference/operator/aggregation/merge/">$merge</a></li>
</ul>
]]></content:encoded></item><item><title>MongoDB Change Streams + Kafka 整合：resume token、scope 選擇與 connector 治理</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/change-streams-kafka/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/change-streams-kafka/</guid><description>&lt;p>MongoDB change streams 是 3.6+ 原生 CDC 介面、本質上是 oplog tail 包裝成 cursor API。Application 從 dual-write 模式（自己寫 MongoDB 又寫 Elasticsearch / Redis / data warehouse）換成 change stream → Kafka → downstream sink 後、有了第一版 CDC pipeline、但連續工作幾週後出現「downstream 漏 event」或「duplicate event」；最痛的是 connector restart 後 resume token 過期（oplog 已滾掉）、整個 collection 必須重灌。本文把 change stream 機制、Kafka Connector 配置、resume token 治理、sharded cluster scope 選擇講清楚。&lt;/p>
&lt;p>本文不重複 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview&lt;/a> 已寫過的 change streams 簡介 — 而是 production CDC pipeline 部署 + 失敗修復的實作層教學。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>MongoDB 適用度前置判讀&lt;/strong>：進到 CDC pipeline 設計前先確認 workload 在 MongoDB 適用區（document shape 主導 / contract layer 該放哪 / 跨雲 hedging 是否需要）— 詳見 &lt;a href="../schema-design-pattern/#%e5%95%8f%e9%a1%8c%e6%83%85%e5%a2%83document-%e8%87%aa%e7%94%b1%e7%9a%84%e5%be%8c%e5%ba%a7%e5%8a%9b">schema-design-pattern 開頭 3 軸前置判讀&lt;/a>、本篇不重複展開。Change streams 是 &lt;em>已選 MongoDB 後&lt;/em> 的 event-driven 整合議題。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境第一版-cdc-pipeline-跑幾週的踩雷">問題情境：第一版 CDC pipeline 跑幾週的踩雷&lt;/h2>
&lt;p>典型觸發場景：application 寫 MongoDB 後還要 dual-write Elasticsearch / Redis / data warehouse、application code 越塞越多 hook、寫入失敗的補償邏輯散落各處。改用 change stream → Kafka → downstream sink 後、有了第一版 CDC pipeline、但連續工作幾週後出現：&lt;/p>
&lt;ul>
&lt;li>Downstream 漏 event 或 duplicate event&lt;/li>
&lt;li>Connector restart 後 resume token 過期（oplog 已滾掉）、整個 collection 必須重灌&lt;/li>
&lt;li>Sharded cluster 上 collection-level change stream 跟 cluster-wide change stream 行為不同、application 連 mongos 跟連 single shard 拿到不同 event&lt;/li>
&lt;/ul>
&lt;p>讀者徵兆：&lt;/p>
&lt;ul>
&lt;li>MongoDB Kafka Connector log &lt;code>ChangeStreamHistoryLost&lt;/code> 或 &lt;code>ResumeTokenChanged&lt;/code>&lt;/li>
&lt;li>Downstream Kafka topic event count vs source collection write count 不平&lt;/li>
&lt;li>Replication oplog 跟 change stream consumer 的 lag 同時升&lt;/li>
&lt;/ul>
&lt;p>Case anchor：CDC pipeline resume token 過期導致全量重灌的具體 incident 細節需未來 case 補完、本文以「常見 failure pattern」+ 容量公式處理、不憑空編造 incident 數字。側面引用 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spotify-kafka-to-pubsub-migration-gcp/" data-link-title="9.C9 Spotify：從自管 Kafka 遷移到 GCP Pub/Sub 的事件交付系統" data-link-desc="Spotify 把自管 Kafka 事件系統遷移到 Google Cloud Pub/Sub、避免自管 broker 的容量規劃成本">Spotify Kafka → PubSub migration&lt;/a>（pipeline-level migration 經驗對照）。&lt;/p></description><content:encoded><![CDATA[<p>MongoDB change streams 是 3.6+ 原生 CDC 介面、本質上是 oplog tail 包裝成 cursor API。Application 從 dual-write 模式（自己寫 MongoDB 又寫 Elasticsearch / Redis / data warehouse）換成 change stream → Kafka → downstream sink 後、有了第一版 CDC pipeline、但連續工作幾週後出現「downstream 漏 event」或「duplicate event」；最痛的是 connector restart 後 resume token 過期（oplog 已滾掉）、整個 collection 必須重灌。本文把 change stream 機制、Kafka Connector 配置、resume token 治理、sharded cluster scope 選擇講清楚。</p>
<p>本文不重複 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> 已寫過的 change streams 簡介 — 而是 production CDC pipeline 部署 + 失敗修復的實作層教學。</p>
<blockquote>
<p><strong>MongoDB 適用度前置判讀</strong>：進到 CDC pipeline 設計前先確認 workload 在 MongoDB 適用區（document shape 主導 / contract layer 該放哪 / 跨雲 hedging 是否需要）— 詳見 <a href="../schema-design-pattern/#%e5%95%8f%e9%a1%8c%e6%83%85%e5%a2%83document-%e8%87%aa%e7%94%b1%e7%9a%84%e5%be%8c%e5%ba%a7%e5%8a%9b">schema-design-pattern 開頭 3 軸前置判讀</a>、本篇不重複展開。Change streams 是 <em>已選 MongoDB 後</em> 的 event-driven 整合議題。</p></blockquote>
<h2 id="問題情境第一版-cdc-pipeline-跑幾週的踩雷">問題情境：第一版 CDC pipeline 跑幾週的踩雷</h2>
<p>典型觸發場景：application 寫 MongoDB 後還要 dual-write Elasticsearch / Redis / data warehouse、application code 越塞越多 hook、寫入失敗的補償邏輯散落各處。改用 change stream → Kafka → downstream sink 後、有了第一版 CDC pipeline、但連續工作幾週後出現：</p>
<ul>
<li>Downstream 漏 event 或 duplicate event</li>
<li>Connector restart 後 resume token 過期（oplog 已滾掉）、整個 collection 必須重灌</li>
<li>Sharded cluster 上 collection-level change stream 跟 cluster-wide change stream 行為不同、application 連 mongos 跟連 single shard 拿到不同 event</li>
</ul>
<p>讀者徵兆：</p>
<ul>
<li>MongoDB Kafka Connector log <code>ChangeStreamHistoryLost</code> 或 <code>ResumeTokenChanged</code></li>
<li>Downstream Kafka topic event count vs source collection write count 不平</li>
<li>Replication oplog 跟 change stream consumer 的 lag 同時升</li>
</ul>
<p>Case anchor：CDC pipeline resume token 過期導致全量重灌的具體 incident 細節需未來 case 補完、本文以「常見 failure pattern」+ 容量公式處理、不憑空編造 incident 數字。側面引用 <a href="/blog/backend/09-performance-capacity/cases/spotify-kafka-to-pubsub-migration-gcp/" data-link-title="9.C9 Spotify：從自管 Kafka 遷移到 GCP Pub/Sub 的事件交付系統" data-link-desc="Spotify 把自管 Kafka 事件系統遷移到 Google Cloud Pub/Sub、避免自管 broker 的容量規劃成本">Spotify Kafka → PubSub migration</a>（pipeline-level migration 經驗對照）。</p>
<h2 id="核心機制">核心機制</h2>
<p>Change stream 是 MongoDB 3.6+ 原生 CDC、本質上是 oplog tail 包裝成 cursor API。可以從 collection / database / cluster 三個 scope 開：</p>
<ul>
<li><strong>Collection-level</strong>：監看單一 collection 的變更</li>
<li><strong>Database-level</strong>：監看整個 database 的所有 collection</li>
<li><strong>Cluster-wide</strong>：監看整個 cluster 的所有 database</li>
</ul>
<p>Oplog 是 capped collection、預設 size = disk 5% 或 50GB（取較小）。Resume token 對應 oplog entry 的 timestamp + UUID + documentKey。Token 必須對應仍在 oplog 內的 entry — oplog 滾掉就拿不到 token 對應的位置、<code>ChangeStreamHistoryLost</code>。</p>
<p><strong>Resume token 兩種用法</strong>：</p>
<ul>
<li><code>_id</code>：每個 event 都帶、application 自己存</li>
<li><code>startAfter</code> / <code>resumeAfter</code> parameter：重啟 cursor 時帶上</li>
</ul>
<p><strong><code>fullDocument: &quot;updateLookup&quot;</code></strong>：update event 預設只給 delta、加這個 option 會額外 query 一次 primary 拿完整 doc；高頻 update 下成本顯著（primary 負擔翻倍）。</p>
<p><strong>Pre-image / post-image（6.0+）</strong>：可以拿到 update 前的 doc 狀態、需 collection-level option <code>changeStreamPreAndPostImages: true</code>。</p>
<p><strong>Cluster-wide vs collection-level change stream</strong>：</p>
<ul>
<li>Cluster-wide 必須打 mongos、event ordering 是 global</li>
<li>Collection-level 可直接打單 shard、ordering 只在該 shard 內</li>
<li>Sharded cluster 上 cluster-wide stream 容易把 mongos 變單點瓶頸（所有 shard 的 event 都收斂到 mongos）</li>
</ul>
<p><strong>MongoDB Kafka Connector</strong>（Confluent / MongoDB 官方）：</p>
<ul>
<li>Source connector：把 change stream → Kafka topic</li>
<li>Sink connector：把 Kafka topic → MongoDB</li>
<li>At-least-once 語義、需 application 處理 idempotency</li>
</ul>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/change-data-capture/" data-link-title="Change Data Capture" data-link-desc="說明資料變更如何被捕捉並傳送到其他系統">change-data-capture</a>、<a href="/blog/backend/knowledge-cards/replication-channel/" data-link-title="Replication Channel" data-link-desc="說明多來源複製中，每個來源對應的獨立複製通道如何成為隔離單位">replication-channel</a>、<a href="/blog/backend/knowledge-cards/replication-slot/" data-link-title="Replication Slot" data-link-desc="說明邏輯複製如何用 slot 追蹤消費進度，並對來源端造成保留壓力">replication-slot</a>（MongoDB 沒 slot、概念對照）。</p>
<h2 id="操作流程">操作流程</h2>
<p><strong>Step 1：scope 決策樹</strong>。</p>
<table>
  <thead>
      <tr>
          <th>Scope</th>
          <th>適用條件</th>
          <th>代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Collection-level</td>
          <td>單一 collection 的下游 sink、ordering 需求單一</td>
          <td>多 collection 要多 connector</td>
      </tr>
      <tr>
          <td>Database-level</td>
          <td>多 collection 共享 sink、ordering 跨 collection</td>
          <td>filter cost 在 connector 端</td>
      </tr>
      <tr>
          <td>Cluster-wide</td>
          <td>整個 cluster 統一 audit / replay</td>
          <td>mongos 單點瓶頸風險、event 量大</td>
      </tr>
  </tbody>
</table>
<p><strong>Step 2：oplog sizing</strong>。容量公式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">oplog size &gt;= peak write rate × max acceptable consumer downtime</span></span></code></pre></div><p>典型設 24-72 小時可恢復窗口。例：peak 5K WPS、想容忍 48 小時 connector down、oplog 至少 5K × 86400 × 2 ÷ docs_per_GB ≈ 看實際 doc size 決定。在 Atlas 上 oplog size 可直接調、自管 cluster 改 <code>replSetResizeOplog</code>。</p>
<p><strong>Step 3：Kafka Connector 配置</strong>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nt">&#34;connector.class&#34;</span><span class="p">:</span> <span class="s2">&#34;com.mongodb.kafka.connect.MongoSourceConnector&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nt">&#34;connection.uri&#34;</span><span class="p">:</span> <span class="s2">&#34;mongodb://...&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nt">&#34;database&#34;</span><span class="p">:</span> <span class="s2">&#34;shop&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nt">&#34;collection&#34;</span><span class="p">:</span> <span class="s2">&#34;orders&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="nt">&#34;publish.full.document.only&#34;</span><span class="p">:</span> <span class="s2">&#34;true&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="nt">&#34;change.stream.full.document&#34;</span><span class="p">:</span> <span class="s2">&#34;updateLookup&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="nt">&#34;copy.existing&#34;</span><span class="p">:</span> <span class="s2">&#34;true&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="nt">&#34;copy.existing.namespace.regex&#34;</span><span class="p">:</span> <span class="s2">&#34;shop\\.orders&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="nt">&#34;errors.tolerance&#34;</span><span class="p">:</span> <span class="s2">&#34;none&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nt">&#34;offset.flush.interval.ms&#34;</span><span class="p">:</span> <span class="s2">&#34;10000&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>關鍵欄位：</p>
<ul>
<li><code>change.stream.full.document: &quot;updateLookup&quot;</code>：每 update 額外 query primary 拿完整 doc（成本意識）</li>
<li><code>copy.existing: &quot;true&quot;</code>：connector 啟動時先把現有 collection 全量複製、再切到 change stream — 適合初次部署</li>
<li><code>errors.tolerance: &quot;none&quot;</code>：sink 失敗時 batch 停在 dead-letter queue、不 silently drop</li>
</ul>
<p><strong>Step 4：resume token persistence</strong>。Connector 把 token 寫 Kafka <code>__consumer_offsets</code> 或外部 store；application 自管 change stream 時要寫到 durable store（不是 in-memory）。</p>
<p><strong>Step 5：filter pipeline</strong>。Change stream 支援 aggregation pipeline 把過濾下推到 MongoDB：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">pipeline</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="p">{</span> <span class="nx">$match</span><span class="o">:</span> <span class="p">{</span> <span class="s2">&#34;operationType&#34;</span><span class="o">:</span> <span class="p">{</span> <span class="nx">$in</span><span class="o">:</span> <span class="p">[</span><span class="s2">&#34;insert&#34;</span><span class="p">,</span> <span class="s2">&#34;update&#34;</span><span class="p">,</span> <span class="s2">&#34;delete&#34;</span><span class="p">]</span> <span class="p">}</span> <span class="p">}</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="p">{</span> <span class="nx">$match</span><span class="o">:</span> <span class="p">{</span> <span class="s2">&#34;fullDocument.region&#34;</span><span class="o">:</span> <span class="s2">&#34;ap-tokyo&#34;</span> <span class="p">}</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="kr">const</span> <span class="nx">changeStream</span> <span class="o">=</span> <span class="nx">db</span><span class="p">.</span><span class="nx">orders</span><span class="p">.</span><span class="nx">watch</span><span class="p">(</span><span class="nx">pipeline</span><span class="p">)</span></span></span></code></pre></div><p>把過濾下推減少 connector 處理量、特別是高頻 collection 上。</p>
<p><strong>Step 6：downstream idempotency</strong>。Sink 收 Kafka event 時用 <code>documentKey._id + clusterTime</code> 做 dedup key — at-least-once 語義意味著 connector restart 後幾分鐘 event 會重發。</p>
<p>驗證點：</p>
<ul>
<li>Source collection write count vs Kafka topic event count 差異 &lt; 0.1%</li>
<li>Resume token age &lt; oplog retention 的 50%（健康狀態）</li>
<li>Connector restart drill 能 5 分鐘內接回</li>
</ul>
<p>Rollback boundary：source connector 是 read-only 對 MongoDB 無傷；sink connector 要備份 target 才能還原；resume token 寫錯 → 從 <code>startAtOperationTime</code> 回退到時間點重跑。</p>
<h2 id="失敗模式">失敗模式</h2>
<p><strong>Resume token 過期（oplog 滾掉）</strong>：connector down 太久、oplog 已超出 retention、<code>ChangeStreamHistoryLost</code> → 必須 <code>copy.existing</code> 全量重灌、期間 downstream 看不到新資料。預防是 oplog sizing 留 buffer + connector lag alarm + token age 監控（age &gt; oplog retention 的 50% 預警）。</p>
<p><strong>updateLookup 在高頻 update 下打爆 primary</strong>：每筆 update event 都觸發一次 primary query、primary 負擔翻倍。修法是改 collection-level pre/post image（6.0+）、由 MongoDB 自己在寫入時記錄、或在 application 補完整 doc 後再寫 Kafka、不用 updateLookup。</p>
<p><strong>Sharded cluster cluster-wide stream 打爆 mongos</strong>：所有 shard 的 event 都收斂到 mongos、mongos 變單點瓶頸。修法是改 collection-level stream 多 connector 並行、每 connector 連 mongos 但只訂單一 collection。</p>
<p><strong>At-least-once 變 duplicate flood</strong>：connector restart 點之後幾分鐘 event 重發、downstream 沒做 idempotency → 重複 side effect（重複發 email、重複扣款）。修法是 sink 端強制 idempotency（dedup key 寫 Redis / DB）、不能假設「我用 at-least-once 但實際不會 duplicate」。</p>
<p><strong>Schema drift 突然 break sink</strong>：MongoDB 寫了新欄位 / 改型別、sink connector 的 JSON schema 不認、batch 停在 dead-letter queue。修法是 schema 變動有 validation gate（見 <a href="../schema-design-pattern/">schema design pattern</a>）、sink schema 設 <code>lenient</code> 模式吃 unknown field、或加 schema registry 統一版本。</p>
<p><strong>Backup / DDL 期間 change stream 異常</strong>：<code>reIndex</code> / <code>compact</code> / <code>dropCollection</code> 觸發特殊 event、connector 沒處理 → consumer 停。修法是 connector 處理特殊 event 邏輯要明確、不認得的 operation type 至少 log warning 而不是 silently stuck。</p>
<p>Anti-recommendation：</p>
<ul>
<li>簡單的 <a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox pattern</a> + application transactional write 對於低吞吐 / 單 sink 的場景比 change stream + Kafka 簡單；不是所有「需要 event 通知」的場景都要 CDC pipeline</li>
<li>若 downstream 只是同一 region 同團隊的 Elasticsearch index、<code>$merge</code> 寫進中介 collection 或 application 雙寫 + 對賬可能成本更低</li>
<li>Resume token 過期是這條路徑最痛的事故、oplog sizing 是 <em>投資而不是成本</em> — 不要為了省 storage 把 oplog 設太小</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<p>關鍵 metric：</p>
<ul>
<li><strong>Oplog 健康</strong>：oplog 寫入速率與保留時間</li>
<li><strong>Change stream 健康</strong>：cursor age、resume token 距 oplog 頭尾的距離</li>
<li><strong>Connector 健康</strong>：connector lag（Kafka offset 對比 source write）</li>
<li><strong>下游健康</strong>：event count diff（source write count vs sink apply count）、event time → arrival time lag 分布</li>
</ul>
<p>Mongo command：</p>
<ul>
<li><code>db.getReplicationInfo()</code>：oplog 大小 / 時間範圍</li>
<li><code>db.printReplicationInfo()</code>：oplog 摘要</li>
<li><code>db.currentOp({ &quot;op&quot;: &quot;getmore&quot;, &quot;ns&quot;: &quot;local.oplog.rs&quot; })</code>：看 change stream consumer 連線</li>
</ul>
<p>Connector metric（Kafka Connect JMX）：<code>source-record-poll-rate</code>、<code>source-record-write-rate</code>、<code>offset-commit-success-rate</code>。</p>
<p>回到 <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</a>：oplog retention + connector lag + dedup rate 是 CDC pipeline 健康狀態 evidence 三件套。</p>
<p>回到 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 bottleneck localization</a>：CDC lag 升高時區分 (a) source oplog 寫太快 (b) connector 處理慢 (c) downstream sink 慢。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<p>Sibling deep articles：</p>
<ul>
<li><a href="../shard-key-selection/">shard key selection</a> — cluster-wide vs collection-level change stream 在 sharded cluster 的選擇</li>
<li><a href="../replica-set-read-preference/">replica set read preference</a> — change stream 對 primary load 的影響、能否走 secondary</li>
<li><a href="../schema-design-pattern/">schema design pattern</a> — schema validator 對下游 sink 的契約意義</li>
<li><a href="../connection-management-and-cache-layer/">connection management and cache layer</a> — CDC sink 在 production 跨層架構裡的角色（cache invalidation / federated DB 同步）</li>
</ul>
<p>Migration playbook：</p>
<ul>
<li>MongoDB → 其他 sink 的 bulk migration 走 <a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">→ Atlas Migration Service</a></li>
<li>遷出 MongoDB 時 change stream 是 catch-up 機制（先 bulk export、再 change stream 補增量）</li>
</ul>
<p>跟 1.x 互引：<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 evidence</a> 處理 schema drift 時 CDC pipeline 的對賬；<a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 reconciliation data repair</a> 處理 CDC 失準後的對賬流程。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> — 本文是該頁尾「change streams + Kafka」backlog 的深度展開</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a></li>
<li>官方：<a href="https://www.mongodb.com/docs/manual/changeStreams/">Change Streams</a>、<a href="https://www.mongodb.com/docs/kafka-connector/current/">MongoDB Kafka Connector</a>、<a href="https://www.mongodb.com/docs/manual/core/replica-set-oplog/">Oplog</a></li>
</ul>
]]></content:encoded></item><item><title>從 MongoDB / Cassandra 遷入 Cosmos DB：protocol-compat API drop-in vs native API paradigm shift、相容性邊界與 dual-write cutover</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/migrate-from-mongodb-cassandra/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/migrate-from-mongodb-cassandra/</guid><description>&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB&lt;/a> overview 的 migration playbook、寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 寫作方法論&lt;/a>。從 MongoDB 或 Cassandra 遷入 Cosmos DB 的核心決策是 &lt;em>選哪條路徑&lt;/em> — 用 Cosmos 的 protocol-compat API（MongoDB API / Cassandra API）做 wire-protocol drop-in、driver 與 query 大致不動；還是換 native SQL API、把 application 重寫成 Cosmos native paradigm。這兩條路的 diff 維度、風險、不可逆性都不同、是一個 multi-element 的 migration 規劃。本文先把 driver 與 no-go 講清楚、再做 6 維 diff audit 分出兩條路徑、再進各自的 phase plan、evidence 與 cutover。&lt;/p>
&lt;p>API &lt;em>選擇判斷&lt;/em> 本身（MongoDB API vs SQL API 的四層 framing、dogfood signal、multi-model、跨雲 hedging）由 &lt;a href="../mongodb-api-vs-sql-api/">mongodb-api-vs-sql-api&lt;/a> 主寫、本文不重複展開那層對比；本文主寫 &lt;em>遷移流程&lt;/em> — 選定路徑後怎麼安全把資料與流量搬過去。&lt;/p>
&lt;p>Case anchor：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365&lt;/a>（MongoDB → Cosmos DB MongoDB API、planet-scale、dogfood）、&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes&lt;/a>（自管 → Atlas、6 個月、同 DB 換託管的時程對照）、&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &amp;#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase&lt;/a>（保留 MongoDB 補周邊、對照「不一定要遷」）。Microsoft 365 case 自承沒揭露 throughput / latency / cost 數字、本文不拿它當 benchmark、只取遷移路徑 frame。&lt;/p>
&lt;h2 id="driver為什麼遷什麼條件不遷">Driver：為什麼遷、什麼條件不遷&lt;/h2>
&lt;p>有效的遷移 driver 不是「Cosmos DB 比較好」、而是具體壓力：team 已綁 Azure 生態、需要 turnkey global distribution、自管 MongoDB / Cassandra cluster 的 ops 負擔要轉移、或需要 multi-model 把多個 NoSQL 集中治理。Microsoft 365 的 driver 是 planet-scale 全球分散 + Azure dogfood、不是 query 性能。&lt;/p></description><content:encoded><![CDATA[<p>本文是 <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB</a> overview 的 migration playbook、寫作參照 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration Playbook 寫作方法論</a>。從 MongoDB 或 Cassandra 遷入 Cosmos DB 的核心決策是 <em>選哪條路徑</em> — 用 Cosmos 的 protocol-compat API（MongoDB API / Cassandra API）做 wire-protocol drop-in、driver 與 query 大致不動；還是換 native SQL API、把 application 重寫成 Cosmos native paradigm。這兩條路的 diff 維度、風險、不可逆性都不同、是一個 multi-element 的 migration 規劃。本文先把 driver 與 no-go 講清楚、再做 6 維 diff audit 分出兩條路徑、再進各自的 phase plan、evidence 與 cutover。</p>
<p>API <em>選擇判斷</em> 本身（MongoDB API vs SQL API 的四層 framing、dogfood signal、multi-model、跨雲 hedging）由 <a href="../mongodb-api-vs-sql-api/">mongodb-api-vs-sql-api</a> 主寫、本文不重複展開那層對比；本文主寫 <em>遷移流程</em> — 選定路徑後怎麼安全把資料與流量搬過去。</p>
<p>Case anchor：<a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a>（MongoDB → Cosmos DB MongoDB API、planet-scale、dogfood）、<a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a>（自管 → Atlas、6 個月、同 DB 換託管的時程對照）、<a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a>（保留 MongoDB 補周邊、對照「不一定要遷」）。Microsoft 365 case 自承沒揭露 throughput / latency / cost 數字、本文不拿它當 benchmark、只取遷移路徑 frame。</p>
<h2 id="driver為什麼遷什麼條件不遷">Driver：為什麼遷、什麼條件不遷</h2>
<p>有效的遷移 driver 不是「Cosmos DB 比較好」、而是具體壓力：team 已綁 Azure 生態、需要 turnkey global distribution、自管 MongoDB / Cassandra cluster 的 ops 負擔要轉移、或需要 multi-model 把多個 NoSQL 集中治理。Microsoft 365 的 driver 是 planet-scale 全球分散 + Azure dogfood、不是 query 性能。</p>
<p>No-go condition（這些情況不該遷入 Cosmos DB）：</p>
<ul>
<li>跨雲是核心需求 — Cosmos DB 只在 Azure；跨雲彈性高於 Azure 整合時、MongoDB 留 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">Atlas</a>（Forbes 路徑、跨 AWS / GCP / Azure）、Cassandra 留自管或 ScyllaDB。</li>
<li>需要 native MongoDB / Cassandra 最新 feature — Cosmos DB 的 protocol-compat API server version 落後原生、且部分 feature 行為不同。</li>
<li>未來雲商策略未定 — hedging 價值高於當下整合、見 <a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">vendor lock-in</a> 的退出成本。</li>
<li>現有 cluster 補周邊就夠用 — Coinbase 保留 MongoDB 加 proxy / cache / predictive scaling、沒遷出。遷移成本高、先確認「補周邊」解不了問題再遷。</li>
</ul>
<h2 id="diff-audit6-維度分出兩條路徑">Diff audit：6 維度分出兩條路徑</h2>
<p>source（MongoDB / Cassandra）與 target（Cosmos DB）的差異按 6 維度盤點、兩條路徑的維度高低不同、這也是 type 判定的依據。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>protocol-compat API（MongoDB / Cassandra API）</th>
          <th>native SQL API</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema</td>
          <td>Low — document / table shape 大致保留</td>
          <td>Medium — 重新建模成 Cosmos native document</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td>High — 自管 cluster → managed RU/s + region</td>
          <td>High — 同左</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Low — 仍 document / wide-column 語意</td>
          <td>High — 換 query 模型、index policy、RU 思維</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Medium — driver 保留、aggregation / CQL 部分要改</td>
          <td>High — driver、query layer、ORM 全換</td>
      </tr>
      <tr>
          <td>Application</td>
          <td>Medium — connection string、auth、consistency 對應</td>
          <td>High — 整個 data access layer 重寫</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>High — replica set / ring → partition + multi-region</td>
          <td>High — 同左</td>
      </tr>
  </tbody>
</table>
<p>主導差異決定 type：</p>
<ul>
<li>protocol-compat 路徑 — 最大差異是 operational 與 data topology、paradigm 維持 Low、是 wire-compat 的 drop-in 但有相容 gap。對應 <strong>Type B drop-in（partial）</strong>：driver 不換、但每個 query pattern 要驗證相容性、不是無腦切換。</li>
<li>native API 路徑 — paradigm High + application High、是 <strong>Type E paradigm shift</strong>：不只搬資料、要重寫 application 的整個 data access layer。</li>
</ul>
<p>判讀句：protocol-compat 是「換底層儲存與運維、保留 query 介面」、native API 是「連 query 範式一起換」。多數遷移先走 protocol-compat 把資料與 ops 搬過去、native API 是後續若要拿完整 Cosmos feature（Change Feed、stored procedure 原生支援、SQL API query）才考慮的二次遷移 — 一次到位 native API 的工程複雜度與風險顯著更高。</p>
<h3 id="cassandra-路徑的專屬差異">Cassandra 路徑的專屬差異</h3>
<p>Cassandra → Cosmos DB Cassandra API 跟 MongoDB 路徑有一個關鍵不同：Cassandra 的資料建模是 <em>query-driven</em>（partition key + clustering key 對應 access pattern）、這套建模思維跟 Cosmos DB 的 logical partition 概念部分對齊、但 Cosmos DB 的 per-partition RU 上限（目前約 10,000 RU/s、vendor 規格、實作時 cross-verify Azure doc 當前值）與 RU 計費會讓原本 Cassandra 上「寬 partition + 大量 clustering row」的設計變成 hot partition 風險。CQL 的 consistency level（QUORUM / LOCAL_ONE 等）要對應到 Cosmos DB 的 5 個 consistency level、語義不是一對一、見 <a href="../consistency-levels-engineering/">consistency-levels-engineering</a>。Cassandra 的 secondary index / materialized view 在 Cassandra API 的支援度要逐項驗證（時間敏感、查文件）。</p>
<h2 id="phase-plan">Phase plan</h2>
<p>兩條路徑共用大架構、protocol-compat 的相容 audit 較輕、native API 多一段 application 重寫。</p>
<h3 id="protocol-compat-路徑type-b-drop-in">protocol-compat 路徑（Type B drop-in）</h3>
<ul>
<li>Phase 0：相容性 audit — 把 production query / aggregation pipeline（MongoDB）或 CQL statement（Cassandra）拉出來、逐條對照 Cosmos DB 對應 API 的 <a href="https://learn.microsoft.com/azure/cosmos-db/mongodb/feature-support-60">feature support</a> 清單、列出 unsupported 與行為不同的部分。</li>
<li>Phase 1：partition key 設計 — MongoDB shard key / Cassandra partition key 翻譯成 Cosmos logical partition key、檢查 10,000 RU/s 上限與 hot partition 風險、見 <a href="../partition-key-design/">partition-key-design</a>。</li>
<li>Phase 2：bulk export-import — 初始資料用 Data Migration Tool / mongodump / sstable export 灌入。</li>
<li>Phase 3：CDC sync — source 的持續變更（MongoDB oplog / Cassandra CDC）同步到 Cosmos DB、收斂初始 load 後的增量。</li>
<li>Phase 4：shadow read — production query 在兩邊各跑一遍、對 result checksum、量 Cosmos 端 RU baseline、見 <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a>。</li>
<li>Phase 5：read cutover — 讀切 Cosmos、寫仍 source（可回退）。</li>
<li>Phase 6：write cutover — 寫切 Cosmos。</li>
<li>Phase 7：cleanup — 退役 source cluster、保留 export 與最終 checksum。</li>
</ul>
<h3 id="native-api-路徑type-e-paradigm-shift多出的工作">native API 路徑（Type E paradigm shift）多出的工作</h3>
<p>native API 路徑在 Phase 0 與 Phase 1 之間插入 <em>application 重寫 stream</em>、與資料遷移 stream 並行：</p>
<ul>
<li>重新建模 document（從 MongoDB document / Cassandra table 設計 Cosmos native shape、決定 embed vs reference）</li>
<li>重寫 data access layer（換掉 MongoDB driver / CQL、改用 Cosmos SQL API SDK、重寫所有 query）</li>
<li>重寫 aggregation（Cosmos SQL API 沒有 JOIN、aggregation 模型不同、部分邏輯移到 application 或用 stored procedure / Change Feed 物化）</li>
</ul>
<p>這條 application stream 是 native API 路徑的主要風險與工期來源、必須跟資料遷移 stream 用獨立 owner 並行、shadow read 階段要對 <em>重寫後的 query</em> 與 <em>原 query</em> 的結果一致性、不只是資料一致性。</p>
<h3 id="時程現實">時程現實</h3>
<p>Forbes 同 DB 換託管（自管 → Atlas、paradigm 不變）用 6 個月、中型團隊多 squad 並行。protocol-compat 遷入 Cosmos DB 的工程複雜度高於 Forbes 型（多了 RU / partition / region 範式與相容 gap）、native API 路徑再高一個量級（加 application 重寫）。拿 Forbes 6 個月當 native API 路徑 baseline 會從第一天 over-commit。</p>
<h2 id="evidence">Evidence</h2>
<p>每個 phase 用資料證明可前進、不靠感覺：</p>
<ul>
<li>Phase 0：unsupported feature 清單已窮舉、每條有對應策略（改寫 / 移 application 層 / 接受降級）</li>
<li>Phase 2-3：row / document count 對齊、CDC replication lag 收斂到穩定</li>
<li>Phase 4：query result checksum 一致（protocol-compat 比原 query 結果；native API 比重寫 query 與原 query 結果）、RU baseline 量到、aggregation result 逐條對齊</li>
<li>Phase 5-6：error rate、p99 latency、RU consumption 在 cutover 後在預期範圍</li>
<li>對應 <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。">schema-migration-rollout-evidence</a> 的 dual-write 驗證</li>
</ul>
<h2 id="cutover">Cutover</h2>
<ul>
<li>read cutover window：先切讀、寫留 source、Cosmos 端 read error rate 與 latency 達標再進 write cutover</li>
<li>write cutover window：read-only freeze &lt; 10 分鐘、切寫、最終 checksum 對齊</li>
<li>Rollback condition：query error rate 超過閾值（如 &gt; 1%）、RU consumption 顯著高於估算（protocol-compat 翻譯層 overhead 比預期高）、或 result mismatch — 任一成立回退到 source、對應 <a href="/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback condition</a></li>
<li>decision owner：cutover 期間誰有權回退要事前定、資料庫切流失敗代價高、不靠臨場判斷</li>
<li>不可逆點：API kind 是 account 層、建 account 時選定、無法事後切換 — protocol-compat 與 native API 是 <em>兩個不同 account</em>；選 protocol-compat 後想升 native API 是 export → 新 account → import + 重寫 application 的二次全量遷移、不是 in-place 升級。這個不可逆性要在 Phase 0 就決定方向、不能 cutover 後反悔</li>
</ul>
<h2 id="cleanup">Cleanup</h2>
<ul>
<li>退役 source cluster 前確認最終 checksum、保留 export dump 90 天作為 rollback 後路</li>
<li>移除 dual-write writer、CDC connector、shadow read harness</li>
<li>保留 RU baseline 與 partition 分布觀測進 production dashboard、見 <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a></li>
<li>incident write-back：把相容 gap 與翻譯層成本意外寫回 runbook、給未來同類遷移</li>
</ul>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="假設-wire-compat--100-行為相同">假設 wire-compat = 100% 行為相同</h3>
<p>protocol-compat API 是「在某些 query pattern 下相容」、不是普遍相容。MongoDB 的部分 aggregation stage（<code>$graphLookup</code> / <code>$facet</code> 等）、Cassandra 的部分 CQL feature 在對應 API 行為不同或不支援、dev 環境 sample data 看不出、production 才爆。修法是 Phase 0 把 <em>所有</em> production query 拉出來逐條驗證、Phase 4 shadow read 對 checksum、不能假設相容。</p>
<h3 id="shard-key--partition-key-直接照搬">shard key / partition key 直接照搬</h3>
<p>MongoDB shard key 或 Cassandra partition key 直接當 Cosmos logical partition key、忽略 10,000 RU/s per partition 上限。原本 Cassandra 寬 partition 在 Cosmos 變 hot partition、throttle。修法是 Phase 1 按 Cosmos 的 partition 上限重新評估、必要時用 synthetic / composite key 強制分散、見 <a href="../partition-key-design/">partition-key-design</a> 與 <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a>。</p>
<h3 id="把-native-api-二次遷移當升級低估">把 native API 二次遷移當「升級」低估</h3>
<p>選 protocol-compat 上線後、想拿 Change Feed / SQL query 等 native 能力、以為「升級到 SQL API」是改設定。實際是新 account + 全量資料遷 + application 重寫的第二次完整遷移。修法是 Phase 0 就決定終態方向 — 若終態確定要 native feature 且團隊能承擔重寫、直接走 native API 路徑、不要兩段遷。</p>
<h3 id="consistency-level-對應錯">consistency level 對應錯</h3>
<p>CQL 的 QUORUM / MongoDB 的 read concern majority 直接假設等價於 Cosmos 某個 level、語義不是一對一。修法是按 <a href="../consistency-levels-engineering/">consistency-levels-engineering</a> 把 read-after-write 與順序需求逐場景對應、不照字面翻譯 consistency 名稱。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<ul>
<li>主對比 SSoT：<a href="../mongodb-api-vs-sql-api/">mongodb-api-vs-sql-api</a> — API <em>選擇判斷</em> 與三型遷移路徑分類在它主寫、本文主寫選定後的 <em>遷移流程</em></li>
<li>Sibling deep articles：<a href="../partition-key-design/">partition-key-design</a>（shard / partition key 翻譯）、<a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a>（翻譯層 RU overhead 與 baseline）、<a href="../consistency-levels-engineering/">consistency-levels-engineering</a>（read concern / CQL consistency 對應）、<a href="../change-feed-cdc/">change-feed-cdc</a>（native API 才有原生 Change Feed、是 native 路徑的 feature driver 之一）</li>
<li>不遷的對照：<a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">Coinbase</a> 保留 MongoDB 補周邊 — 確認「補周邊」解不了再遷</li>
<li>跨雲對照：<a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">Forbes</a> 留 Atlas 跨雲 — 跨雲需求是 Cosmos DB 的 no-go</li>
<li>共通遷移模型：<a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a></li>
<li>Knowledge card：<a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">vendor lock-in</a> / <a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a></li>
<li>回 overview：<a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> 的「從 MongoDB / Cassandra 遷入」backlog</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> — 本文是該頁尾遷入 backlog 的深度展開</li>
<li><a href="../mongodb-api-vs-sql-api/">mongodb-api-vs-sql-api</a> — API 選擇判斷與三型遷移路徑 SSoT</li>
<li><a href="/blog/backend/09-performance-capacity/cases/microsoft-365-cosmos-db-analytics/" data-link-title="9.C30 Microsoft 365：從 MongoDB 遷移到 Cosmos DB 的分析平台" data-link-desc="Microsoft 365 把使用分析平台從 MongoDB 遷移到 Cosmos DB、planet-scale 全球分散式分析">9.C30 Microsoft 365</a> — MongoDB → Cosmos DB MongoDB API dogfood</li>
<li><a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a> — 同 DB 換託管時程對照</li>
<li><a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a> — 保留 MongoDB 不遷的對照</li>
<li><a href="../partition-key-design/">partition-key-design</a> / <a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a> / <a href="../consistency-levels-engineering/">consistency-levels-engineering</a> — 遷移各 phase 的 sibling</li>
<li><a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a> — 跨 vendor 共通模型</li>
<li><a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">Vendor Lock-in 卡片</a> — 跨雲 no-go 判讀</li>
<li>官方：<a href="https://learn.microsoft.com/azure/cosmos-db/mongodb/">Migrate to Cosmos DB for MongoDB</a> / <a href="https://learn.microsoft.com/azure/cosmos-db/cassandra/">Cosmos DB for Apache Cassandra</a></li>
</ul>
]]></content:encoded></item></channel></rss>