總覽與定位
從「知道 Range / Hash / Directory」到「Production 怎麼落地」——實務分片決策地圖。
上一篇「資料分片基礎概念」建立了分片策略與系統挑戰的語彙。本篇聚焦實務落地:真實產品如何選 sharding key、如何設計 co-location、如何在不停機前提下做 rebalance,以及不同業務型態的典型分片藍圖。
面試與架構 review 的高分答案,往往不是背出公式,而是能說出:「這個存取模式 → 因此選這個 key → 用這種策略 → 擴容時走這條遷移路徑」 的完整因果鏈。
• 基礎篇:Range / Hash / Directory 的定義、優缺點、hot spot 與 scatter-gather 概念。
• 本篇:多租戶 SaaS、電商、社交、時序等場景的具體分片藍圖、線上遷移 SOP、Directory 路由表治理,以及 co-location 設計模式。
• 延伸:Hash 擴容細節見「一致性雜湊綜合手冊」;Cassandra 的 partition key 與 clustering key 設計見寬列儲存篇。
| 實務決策層次 | 核心問題 | 常見產出 |
|---|---|---|
| 1. 存取模式 | 80% 請求是 point lookup 還是 range scan? | 決定 Range vs Hash 的大方向 |
| 2. Sharding Key | 哪個欄位能讓多數查詢只打一個 shard? | user_id、tenant_id、order_id 等 |
| 3. Co-location | 哪些表 / 列必須在同一 shard? | 避免跨 shard JOIN 與 2PC |
| 4. 擴容與遷移 | 新增 shard 或搬大 tenant 時怎麼零 downtime? | 雙寫、backfill、map version、節流 |
實務分片是「存取模式驅動」的設計,不是先選技術再硬套業務。
四大典型應用場景
多租戶 SaaS、電商、社交訊息、時序日誌——各自的分片藍圖長什麼樣?
1. 多租戶 SaaS(B2B)
每個 tenant 有獨立資料邊界,但 tenant 大小差異極大(免費版 vs 企業 VIP)。典型做法:
- Directory Sharding:維護
tenant_id → shard_id路由表,control plane 可手動把大 tenant 遷到專用 shard。 - Shared vs Dedicated:小 tenant 共用 shard pool;大 tenant 獨佔節點,避免 noisy neighbor。
- 查詢模式:幾乎所有 API 帶
tenant_id,天然單 shard point lookup。
2. 電商訂單系統
核心矛盾:下單寫入高吞吐 vs 用戶查「我的訂單歷史」。
- Sharding Key:通常用
user_id(或buyer_id),讓同一用戶的 orders + order_items 共 shard。 - Co-location:
order與order_item都用order_id或父級user_id做 key,本地 JOIN 成立。 - 反例:用
order_id做 key 卻常查「某用戶全部訂單」→ 每次 scatter-gather 全 shard。
3. 社交訊息 / Feed
寫入分散、讀取局部性強——適合 hash 均勻打散。
- 訊息 inbox:以
conversation_id或user_idhash,同一對話訊息共 shard。 - Hot user:celebrity 帳號可能打爆單 shard → 讀 replica fanout、edge cache、或 fan-out on write 改 fan-out on read。
- Timeline:常另建 per-user timeline 表(denormalize),寫入時 fan-out,讀取變單 shard point lookup。
• 寫入時 fan-out(fan-out on write):發文的當下,系統就主動把這篇貼文複製一份,塞進每個粉絲自己的動態列表;粉絲刷新時只需查自己那份,很快,但粉絲越多,發文時要複製的份數就越多。
• fan-out on read:反過來,發文時只存一份在作者自己那邊,不主動派送;等粉絲要看動態時,才臨時去逐一查詢所有追蹤對象、現場拼出畫面——寫入變輕,讀取變重。
• replica fanout:跟前兩者是不同層次的事,指把「同一個熱門 shard」收到的讀取請求,分散發給它底下多台唯讀複本一起扛,用來稀釋單台機器的讀取壓力,跟 on-write / on-read 的取捨無關。
4. 日誌 / 指標 / IoT 時序
寫入連續、查詢多為時間區間——Range by time 幾乎是預設答案。
- Partition:按天 / 週 / 月切 partition,舊 partition 可整批 archive 或 TTL drop。
- Hot partition:「今天」的 partition 承接全部新寫入 → 需 monitor 並預先 split 或 buffer 層削峰。
- 跨時間聚合:報表走 OLAP / 預聚合表,避免線上 scatter-gather 全歷史 shard。
| 場景 | 建議 Key | 策略 | 擴容痛點 |
|---|---|---|---|
| 多租戶 SaaS | tenant_id |
Directory | 大 tenant 遷移、map 一致性 |
| 電商訂單 | user_id |
Hash / Consistent Hash | 跨 shard 報表需二級索引 |
| 社交訊息 | conversation_id |
Hash | Hot user / celebrity |
| 時序日誌 | timestamp(+ device_id) |
Range by time | 最新 partition hot spot |
沒有通用 key:同一公司不同服務(訂單 vs 搜尋 vs 報表)可能用不同 sharding 策略。
Co-location 與資料模型
讓相關列落在同一 shard——分片後避免跨 shard JOIN 的第一道防線。
Co-location(共置)指刻意讓邏輯上相關的多筆資料,經 sharding key 計算後落在同一物理 shard。這樣本地 ACID、本地 JOIN、本地 transaction 仍然成立,不必引入 2PC 或 Saga。
Parent-Child 共 Shard
父表與子表使用相同的 sharding key(通常是父 ID 或共同的 user_id):
orders+order_items:key =user_id或order_id(若 order_id 含 user 前綴更佳)。documents+document_versions:key =document_id。accounts+account_settings:key =account_id。
Denormalize 換取單 Shard 讀
當正規化模型必然跨 shard 時,實務常冗餘一份到查詢側:
- Feed:寫入時 fan-out 到各 follower 的 timeline shard(以
follower_user_id為 key)。 - 搜尋:主庫 shard 存 authoritative data;Elasticsearch 建全域 inverted index 承擔跨 shard 搜尋。
- 報表:CDC 同步到 OLAP(BigQuery / ClickHouse),線上 OLTP shard 不做全表聚合。
在 shard 內建 local secondary index 只能加速該 shard 內的查詢。若查詢條件不含 sharding key(例如「依 email 查用戶」而 key 是 user_id),仍須 scatter-gather 或維護全域 lookup 表(email → user_id → shard)。
若一個 business transaction 需要原子更新多個實體,設計時應讓這些列共用 sharding key。跨 shard 原子性代價是 2PC 延遲、協調者 SPOF 與 partition 下的阻塞——能避免就避免。
線上遷移與 Rebalance SOP
新增 shard、搬大 tenant、split hot range——Production 零 downtime 遷移的標準步驟。
分片不是「上線時切好就一勞永逸」。流量成長、tenant 膨脹、hot partition 出現時,都需要背景搬資料。以下 SOP 適用 Directory 與 Hash 擴容,細節因策略而異,但階段邏輯相通。
標準六階段遷移流程
- Plan:定義遷移範圍(哪些 key / tenant)、目標 shard、節流上限、回滾條件。
- Dual-write:Router 同時寫舊 shard 與新 shard(或寫舊 + async replicate),確保新寫入兩邊都有。
- Backfill:背景掃描舊 shard 歷史資料,批量複製到新 shard,追 incremental changelog 直到 lag 接近零。
- Verify:抽樣 checksum、row count、業務 invariant 比對;shadow read 新 shard 結果與舊 shard diff。
- Cutover:更新 shard map / ring version,讀寫流量切到新 shard;保留舊副本一段時間做 rollback window。
- Cleanup:確認無回滾需求後,停止 dual-write、刪除舊副本、釋放容量。
| 階段 | 風險 | 緩解 |
|---|---|---|
| Dual-write | 兩邊寫入不一致 | Idempotent write、version column、compensation job |
| Backfill | 與線上流量搶 I/O | Rate limit、離峰執行、優先搬冷資料 |
| Cutover | Client cache 舊 map | Map version + TTL + sticky routing |
| Cleanup | 過早刪除無法回滾 | 保留舊副本 N 天 + 監控告警 |
遷移期間 observability 比速度更重要:lag、error rate、P99 與 checksum diff 必須可視。
Directory vs Hash 擴容差異
- Directory:改 map 即可指向新 shard;遷移粒度是 tenant / key range,最靈活。map service 需高可用 + cache。
- Hash (
% N):N 改變幾乎全量 remap,Production 很少直接改 N;通常換 Consistent Hash 或做 logical shard → physical shard 兩層映射。 - Consistent Hash:新增節點只搬相鄰 vnode range;需 ring version 讓 client 漸進切換(詳見一致性雜湊手冊)。
面試若只答「dump → restore → 改 DNS」會失分。Production 預期是漸進式、可回滾、可節流的線上遷移;並說明 dual-write 窗口與 map version 如何保證 read-your-writes。
互動 Demo:擴容策略並排對照
同一組租戶與同一次擴容,餵給 Hash % N 與 Directory 兩種策略,比搬遷量、窗口與傾斜。
下方左右兩欄吃的是完全相同的輸入:同一組租戶(大小依傾斜參數與巨鯨倍數決定)、同一組擴容前落點(hash % N)、同一條搬遷頻寬與寫入速率。差別只在擴容時怎麼決定新落點——左欄把 N 改成 N+k 讓算式重算,右欄只改路由表。第 4 節的六階段 SOP(dual-write → backfill → verify → cutover)在兩欄用的是同一套成本公式,兩邊秒數不同純粹是因為要搬的量不同。
% N 重分配N 一改,全體落點跟著算式重算| 指標 | Hash % N | Directory | 差異 |
|---|
看兩欄的「搬遷資料量」與巨鯨那一格:Directory 省下的不是演算法優雅度,而是實際要過網路的位元組——而且它隔離巨鯨的方式是把鄰居清走,不是搬那隻 240 GB 的鯨魚。
% N 要搬的是「算式重算後落點改變的全部資料」,Directory 只搬「為了隔離與填平所必需的那幾筆」;代價換成一張要進關鍵路徑的路由表,而回填能不能收斂,取決於雙寫吃掉多少頻寬。
面試考點與架構折衷
實務追問:大 tenant 怎麼搬?跨 shard 報表怎麼做?什麼時候不該分片?
回答框架:
1. 確認 sharding 策略是否 Directory(可單獨遷移)。
2. 把大 tenant 搬到 dedicated shard / 獨立 cluster。
3. 遷移走 dual-write + backfill + cutover,全程節流。
4. 若仍不足:tenant 內再分 sub-shard(例如依 workspace_id hash)。
回答框架:
1. OLTP shard 不做全 cluster 聚合——延遲與成本不可控。
2. CDC → OLAP(ClickHouse / BigQuery)或搜尋引擎(Elasticsearch)建全域索引。
3. 即時性要求低時用 batch ETL;要求高時用 stream + materialized view。
回答框架:
1. 資料量與 QPS 仍可在單機 + read replica + cache 內解決。
2. 業務強依賴跨表 JOIN / 複雜 transaction,且無法 co-locate 或 denormalize。
3. 團隊缺乏 rebalance、map 治理與跨 shard debug 的運維能力。
4. 過早分片會永久增加系統複雜度——應有明確的容量 trigger(例如單表 > 500GB 或寫入 P99 觸頂)。
| 實務信號 | 建議動作 |
|---|---|
| 單 shard CPU / 磁碟 > 70% 且 key 分布均勻 | 加 shard + rebalance |
| 單 tenant 佔一 shard 過半 | Directory 遷到 dedicated shard |
| 跨 shard 查詢 P99 飆升 | 檢視 sharding key 是否與查詢對齊;考慮 denormalize 或二級索引 |
| 最新 time partition 持續 hot | Buffer 層(Kafka)+ 預 split + 寫入 batch |
• Payload / Latency:scatter-gather 與 dual-write 期間寫入放大;Directory 每次請求多一次 map lookup(通常 cache)。
• Security:多租戶環境需確保 router 強制注入 tenant_id,防止跨 tenant 資料洩漏。
• Dependency:shard map / control plane 成為關鍵路徑;需 HA、版本化與 cache invalidation 策略。