iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0

Day 17 用 CAP Theorem 收尾時提到一句話:這條線會一路貫穿到後面 Sharding、Counter System 這些主題。今天就從 Sharding 開始。

回顧一下 Day 7 做的事:把一個 Database 拆成 Primary 跟多個 Read Replica。

Day 7 的 Primary / Replica Database 架構圖

當時特別澄清過一件事:這樣做還稱不上 Database 的 Horizontal Scaling,因為每個 Replica 裡面裝的都是同一份完整資料

這代表兩件事還是沒解決:

  1. 寫入量還是被單一 Primary 卡死——不管加幾個 Replica,所有 Write 永遠只能走那一台 Primary,寫入吞吐量的天花板從頭到尾沒變過。
  2. 儲存空間還是受限於單一台機器——如果 PokeThreads 的 Post 表膨脹到幾十億筆,就算 Primary 本身的硬碟塞得下,索引大小、查詢效能也會開始出問題。

Replication 解決的是「太多人同時讀」,但沒解決「資料量太大、寫入太多」,這是 Sharding 要處理的問題。

比喻:大圖書館按書名字母分層存放

想像一間圖書館一開始藏書不多,全部書都放在同一層樓,管理員一個人就能顧好進出借還。

但藏書量成長到幾百萬本之後,同一層樓已經放不下,而且不管你要借哪本書,都得跑進同一層樓翻找,人一多動線就開始打結,這時候合理的做法是把書按書名字母分層存放:

  • A~F 放一樓
  • G~M 放二樓
  • N~S 放三樓
  • 剩下以此類推...

每一層樓只需要負責自己那一段書籍,找書的人也可以直接跳去對應樓層,不用擠在同一個地方。

這就是 Sharding(也叫 Partitioning):把資料依照某種規則切成好幾份,分別放到不同的 Database 節點上,讓每個節點只需要負責一部分資料。

Shard Key:決定書要放在哪一層

要把資料切開,第一件事是決定依照什麼欄位切,這個欄位叫 Shard Key

假設我們決定用 user_id 當作 Shard Key,把 User 跟這個使用者相關的 Post 都分配到同一個 Shard:

以 user_id 為 Shard Key 的 Database Sharding 示意圖

Application Server 不會直接連某一個特定的 DB,而是先經過一層 Shard Router(可以是應用層邏輯,也可以是專門的 Proxy),根據 Request 裡的 user_id 算出這筆資料該去哪個 Shard,再把 Query 轉發過去。

常見的切法

Range-based Sharding

依照 Shard Key 的數值範圍切分,例如:

Shard 1:user_id 0 ~ 1,000,000
Shard 2:user_id 1,000,001 ~ 2,000,000
Shard 3:user_id 2,000,001 ~ 3,000,000

優點是實作簡單、直覺,而且範圍查詢(例如「找出 user_id 100 萬到 150 萬之間的使用者」)很好處理,因為這些資料本來就集中在同一個 Shard。

缺點是容易產生熱點(Hot Shard):如果 user_id 是遞增的(新註冊的使用者 ID 越來越大),那所有新使用者、新資料都會不斷擠進最後一個 Shard,其他 Shard 早期滿載、後期閒置。

Hash-based Sharding

把 Shard Key 丟進一個 Hash Function,用計算出來的結果決定要落在哪個 Shard,例如:

shard_id = hash(user_id) % 3

優點是資料分佈通常很平均,不容易出現「大家都擠在同一個 Shard」的狀況。

缺點是範圍查詢變得很麻煩:因為 Hash 過後的資料是打散的,原本相鄰的 user_id 現在可能分散在完全不同的 Shard 上,像「找出某個時間區間內所有貼文」這種查詢,就沒辦法只查一個 Shard 解決。

Directory-based Sharding

前面兩種切法都是靠一個公式直接算出資料該去哪個 Shard,Shard Router 自己算一算就知道答案,不需要問任何人。

Directory-based Sharding 換了一種思路:另外建立一個查詢目錄(Lookup Table / Directory Service),專門記錄「這個 Shard Key 對應到哪一個 Shard」。

Directory Service
user_id 12345 -> Shard 2
user_id 67890 -> Shard 5

每次讀寫前,先問一次 Directory,拿到答案後才知道該去哪個 Shard。

優點是彈性極高,想把某個使用者的資料單獨搬到別的 Shard,只要更新 Directory 裡的這一筆對應就好,不需要照著公式重新計算整批資料,這對之後要單獨處理某些熱門帳號特別有用。

缺點是每一次讀寫都要多一次查 Directory 的網路往返,多了一層延遲;而且這個 Directory Service 一旦掛掉,所有 Shard 都連帶找不到路,它自己就成了系統裡新的 Single Point of Failure(跟 Day 5 的 Load Balancer、Day 14 的 API Gateway 是同一種老問題),所以 Directory Service 通常也得做成高可用的叢集,而不是單一節點。

Geographic Partitioning

前面三種切法都是依照資料本身的某個欄位來分,Geographic Partitioning 換了一個維度:依照資料產生的地理位置切分,例如歐洲使用者的資料放在法蘭克福的節點,亞洲使用者放在東京的節點。

優點是分類方式很直接,使用者連到離自己最近的節點,延遲大幅降低;如果剛好有些地區的法規要求「使用者資料要留在境內」(例如 GDPR),這種切法也順便解決了合規問題。

缺點是負載很容易不均,如果某個地區的使用者成長速度遠高於其他地區,那個地區的 Shard 會率先撐不住,其他地區的 Shard 卻還很閒置,本質上跟 Range-based Sharding 的 Hot Shard 問題是同一件事,只是換成用地理位置當切分依據。

這也是之後要完整討論「PokeThreads 如何全球化」時,會再深入的主題。

Sharding 帶來的新問題

跟前面每一次「加元件解決舊問題」一樣,Sharding 也帶來了自己的一組新麻煩,哪次不是這樣了...

Cross-shard Query 變麻煩

原本一個 JOIN 就能搞定的查詢,現在如果牽涉到的資料分散在不同 Shard,就得各自查詢再由 Application 端合併,甚至有些查詢邏輯上就直接做不到了。

Resharding 令人頭痛

假設原本 3 個 Shard 已經不夠用,要擴增到 4 個,代表 Shard 的分配規則變了(不管是 Range 的邊界,還是 Hash 的除數),大量既有資料需要重新搬移到正確的新位置,這個搬遷過程本身就是一個不小的工程。

以 Hash-based Sharding 為例,shard_id = hash(user_id) % N 這個公式只要 N 一變,幾乎所有資料算出來的 shard_id 都會跟著改變,等於把整份對照表打掉重練。

這個「牽一髮動全身」的問題,可利用 Day 21 介紹的 Consistent Hashing 來處理:只要換一種讓節點與資料上環的方式,加減節點時就只需要搬動一小段資料,而不是全部重新洗牌。

跨 Shard 交易失去簡單性

單一 Database 裡,一個 Transaction 可以輕鬆保證多筆資料要嘛全部成功、要嘛全部失敗。但如果這幾筆資料分散在不同 Shard 上,就沒辦法再用資料庫原生的 Transaction 一次搞定,得靠額外的機制(例如 Two-phase Commit,或乾脆在應用層設計成允許最終一致)來處理。

而這些問題,追根究底又是昨天討論過的 CAP 取捨,節點越多,網路分區、資料不一致的機會就越大

小結

今天透過 Sharding,把資料真正切開分散到多個 Database 節點:

  • Range-based Sharding:簡單、支援範圍查詢,但容易出現 Hot Shard
  • Hash-based Sharding:分佈平均,但範圍查詢變困難
  • Directory-based Sharding:靠查詢目錄決定位置,彈性最高,但多一次查詢開銷,且目錄本身是新的 Single Point of Failure
  • Geographic Partitioning:依地理位置切分,延遲低又符合資料落地法規,但地區間負載容易不均

同時也換來了新的代價:

  • Cross-shard Query
  • Resharding
  • 跨 Shard 交易的複雜度

不過切完資料後還有一種關聯特別麻煩:Follow 這種「使用者與使用者之間」的關係,該怎麼切才合理?這是明天要處理的問題。


上一篇
Day 17 CAP Theorem - 你無法全都要
下一篇
Day 19 深入探討 Follow 功能
系列文
系統設計就像九頭蛇:打造社群網站的 30 天22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言