01

為什麼要分區

一台機器裝不下、也算不動全部資料時,就把資料切開,分給很多台機器一起扛

先聽一句 60 年前的提醒

這一章開頭引用了電腦科學先驅 Grace Murray Hopper 在 1962 年說的話。她講的不是資料庫,但那個精神,剛好就是這一整章要做的事——別再把資料硬綁在「一步接一步」的單機思維裡,而是老實描述資料之間的關係,讓系統自己決定怎麼分工。

原文 · DDIA Ch.6 "Clearly, we must break away from the sequential and not limit the computers. We must state definitions and provide for priorities and descriptions of data. We must state relationships, not procedures." — Grace Murray Hopper, Management and the Computer of the Future (1962) In Chapter 5 we discussed replication—that is, having multiple copies of the same data on different nodes. For very large datasets, or very high query throughput, that is not sufficient: we need to break the data up into partitions, also known as sharding. Normally, partitions are defined in such a way that each piece of data belongs to exactly one partition.
白話翻譯

「我們必須擺脫『一步接一步』的思維,不要把電腦綁死。我們該做的,是講清楚資料的定義、優先順序與描述方式。我們要陳述的是『關係』,而不是『步驟』。」

——葛麗絲・霍普,《管理與未來的電腦》(1962)

上一章談的是複製:把同一份資料,多存幾份放到不同節點上。

但資料量大到一個程度、或查詢多到一個程度,光複製不夠用——我們得把資料切開成好幾塊,這個動作也叫做 sharding(分片)。

通常分區的規則會定成:每一筆資料,剛好屬於其中一塊,不多也不少。

💡
霍普的話,用在這裡剛剛好

「不要限制電腦」放到這一章,就是別把所有資料硬塞進一台機器裡等它慢慢處理——而是把資料的歸屬關係講清楚,讓很多台機器各自認領一塊,同時動工。

複製解決的是「壞掉」,不是「裝不下」

第 5 章教的 複製 很好用,但有個天花板:不管複製幾份,每一台機器都還是得存下「整份」資料。資料一旦大到一個地步,兩個問題會同時冒出來:

1
存不下

單一台機器的硬碟空間有限,一份超大的資料集,塞不進去。

2
跑不動

就算塞得下,太多讀寫請求一起湧進來,一顆 CPU、一條硬碟頻寬也處理不完。

解法就是這一章的主角—— 分區(Partitioning), 也常被叫做 分片(Sharding)。 書裡有一句話講得很傳神:每一個 partition,其實就像是「一個自己的小型資料庫」——原本一個塞爆的大資料庫,被拆成很多個麻雀雖小、五臟俱全的小資料庫,各自管好自己那一塊。

⚠️
複製 ≠ 分區,別搞混

複製是「同一份資料抄很多份」;分區是「把一份資料切成很多塊」。一個解決「機器會壞」,一個解決「一台裝不下、算不動」——兩件事、兩個目的。

跑一遍:一台機器怎麼變成好幾台

下面用一條資料流,演給你看「裝不下、算不動的單一機器」怎麼被拆成好幾個分區、分散到不同節點上。按「下一步」開始。

🖥️
單一機器(裝不下)
✂️
切成分區 Partition
🗄️
節點一
🗄️
節點二
🗄️
節點三
按「下一步」開始
🧱
分區通常疊在複製之上

實務上幾乎不會「只分區、不複製」。真正的做法是:先把資料切成好幾個 partition,每個 partition 自己也會有好幾個副本,分散到不同節點。分區負責「裝得下、忙得過來」,複製負責「壞了也不怕」——兩者疊在一起用,缺一不可。

生活妙喻:爆滿的社區圖書館

想像你家社區只有一間圖書館,藏書全塞在這一棟樓。社區愈長愈大,兩個症頭準時出現:書架塞不下、櫃台大排長龍。社區的解法,不是硬把書架疊更高,而是拆成好幾間分館:

📚
一本書只放一間分館

不會同一本書同時出現在兩間分館——對應「每筆資料剛好屬於一個 partition」。

🏛️
每間分館自己顧自己

各有自己的櫃台、館員、書架,互不共用——對應 shared-nothing 叢集。

🏗️
蓋更多分館服務更多人

社區愈大就蓋愈多分館——對應加更多節點,換來更大的 可擴展性

🔤
同一道菜,各家餐廳菜名不同

「分區」這個概念業界共通,但各家系統取的名字不一樣:MongoDB、Elasticsearch、SolrCloud 叫 shard;HBase 叫 region;Bigtable 叫 tablet;Cassandra、Riak 叫 vnode;Couchbase 叫 vBucket。看到這些詞,先在心裡翻譯回「partition」,就不會被術語嚇到。

分區能帶來多少「倍數」?

分區最主要的理由,就是可擴展性。它同時把兩種負擔攤開:資料量被攤到很多顆硬碟上,查詢負載被攤到很多顆 CPU 上。書裡給了一個很乾脆的理想直覺:如果每個節點都公平地承擔一份,10 個節點,理論上就能扛下單一節點 10 倍的資料量,以及 10 倍的 吞吐量 (先不管複製)。

但這個「10 倍」不是憑空掉下來的,有兩個前提:

公平
資料與負載要分得平均

如果 9 個節點閒閒沒事,所有流量都擠在 1 個節點上,那 1 個節點就是瓶頸,10 倍紅利拿不到。

單區
查詢最好只落在單一分區

像「查某個使用者的資料」這種查詢,只打到它所屬的那個節點,加節點就能線性放大吞吐量。

跨區
跨分區的大查詢會變難

需要橫跨很多分區、彙整全站資料的複雜查詢,雖然也能平行處理,但協調的難度明顯升高。

🍲
火鍋店比喻:一位廚師 vs. 十位廚師

一位廚師包辦切肉、洗菜、煮湯、擺盤,客人一多就忙到爆。改成 10 位廚師、每人只顧一種食材,點「一盤牛肉」只需要 1 號廚師動手,其他 9 位完全不受影響——出餐速度自然跟著人數往上衝。但如果客人點的是「牛肉、海鮮、蔬菜都要」的綜合大拼盤,就得協調好幾位廚師一起做、再合盤,明顯慢下來。

小試身手

分區這個概念,你抓到了嗎?來兩題檢查一下。

某團隊的單一資料庫機器硬碟快滿了,查詢量也暴增到一顆 CPU 處理不完。下列哪一個最能說明「為什麼要分區」?
書中說「每一個 partition 就像是一個自己的小型資料庫」,這句話最想傳達的重點是什麼?
⚖️
下一站:怎麼切才公平

知道「為什麼要分區」只是第一步。下一站:資料要怎麼切才公平,不會某一塊特別熱、某一塊沒人理?

02

兩大分區法:範圍 vs Hash

先學會用「公平」與「找得到」這把尺,再看兩種切法怎麼各自贏一半、輸一半

分區的野心:讓 10 台機器扛出 10 倍效能

當資料量大到一台機器裝不下、查詢量大到一台機器扛不住,我們就把資料切成好幾塊,分散到不同節點上,這個動作叫 分區(partitioning), 有些系統叫它 sharding。

但「切開」只是手段,真正的目標只有一句話:把資料和查詢負載平均地分散到所有節點上。理想狀況下,10 個節點就該扛起 10 倍的資料量、10 倍的 吞吐量(throughput)

傾斜 Skew

負載沒有被公平分配,某些分區拿到的資料或查詢比其他分區多很多。

熱點 Hot Spot

傾斜走到極端後,那個被擠爆、忙到冒煙的倒楣分區,變成整個系統的瓶頸。

浪費的另一端

熱點的另一面,是其他節點在旁邊納涼——你花了 10 台機器的錢,卻只換到約 1 台的效能。

🛒
四個結帳櫃台的超市

把超市想成分散式資料庫:每個結帳櫃台是一個分區,排隊客人是查詢負載。四個櫃台各站 3 個人,流動順暢,效率是單一櫃台的 4 倍——這是公平。但如果客人全擠到 1 號櫃台,它大排長龍、結帳員忙到冒汗,另外 3 個櫃台空空如也,整間店的速度被這一個熱點拖垮。問題不是「櫃台不夠多」,而是「人沒有被公平地引導開」。

🎲
那隨機分配呢?可惜卡關

把每筆資料隨機丟到某個節點,分布確實很平均、幾乎不傾斜。但要讀取某筆特定資料時,你完全不知道它在哪一台——只能「同時問遍所有節點」再拼結果,節點越多、查詢越貴,完全抵消了分區想換來的效率。好的分區法必須同時通過兩關:公平嗎?找得到嗎?下面兩種主流做法,各自用不同方式回答這兩題。

直接讀原文:什麼是「依 Key 範圍分區」

先看書怎麼定義最直覺的第一種分區法——把一段連續的 key 範圍指派給一個分區。

原文 · DDIA Ch.6 One way of partitioning is to assign a continuous range of keys to each partition, like the volumes of a paper encyclopedia. Consider an application that stores data from a network of sensors, where the key is the timestamp of the measurement. Because we write data from the sensors to the database as the measurements happen, all the writes end up going to the same partition, so that partition can be overloaded with writes while others sit idle. To avoid this problem, you need to use something other than the timestamp as the first element of the key. You could prefix each timestamp with the sensor name, so that the partitioning is first by sensor name and then by time.
白話翻譯

分區的一種做法,是把一段連續的 key 範圍指派給每個分區——就像紙本百科全書的每一冊。

想像一個應用在儲存感測器網路的資料,key 是每筆量測的時間戳。

因為感測器資料是隨著量測發生即時寫入的,所有寫入最後都會落到同一個分區(今天那個),於是那個分區被寫爆,其他分區卻閒著沒事做。

要避免這個問題,key 的第一個元素就不能是時間戳。

你可以在每個時間戳前面加上感測器名稱,讓分區先依感測器名稱、再依時間排。

💡
邊界不用切得平均,切得「公平」就好

紙本百科全書某一冊可能只收 A、B(這兩個字母的字特別多),最後一冊卻塞了 T、U、V、X、Y、Z。重點是讓每塊的資料量差不多,而不是字母數量一樣多。每個分區內部的 範圍查詢(range scan) 之所以飛快,正是因為分區內部的 key 本來就保持 排序。 採用這種策略的資料庫包括 Bigtable、HBase、RethinkDB,以及 2.4 版以前的 MongoDB。

點點看:兩種分區法,各贏一半、各輸一半

依範圍分區保留了排序、換來快速的範圍查詢;依 Hash 分區打散了排序、換來均勻分布。點下面兩張卡片,看它們的優缺點各是什麼。

📚 依 Key 範圍分區
🎲 依 Key 的 Hash 分區
✅ 優點:範圍查詢超快。分區內部的 key 保持排序,想找「某段連續範圍」(例如整個第二季),只要走到對應分區從頭翻到尾即可,不用把全部資料掃一遍。 ⚠️ 缺點:容易因單調遞增的 key(例如時間戳記)造成熱點——所有「此刻」的寫入都湧向同一個分區,其他分區閒置。

Hash 分區的機制其實只有三步:把 key 丟進 hash function → 得到一個看似隨機、均勻散布的數字 → 把整個數字範圍切成好幾段,每段對應一個分區。這裡的 hash 函數不需要加密等級的強度:Cassandra、MongoDB 用 MD5,Voldemort 用 Fowler–Noll–Vo。

⚠️
別偷懶用程式語言內建的 hash

Java 的 Object.hashCode()、Ruby 的 Object#hash,同一個 key 在不同 process 可能算出不同結果——這在分散式系統裡是災難,資料寫進去後在別處就找不回來。分區用的 hash 必須是穩定、跨節點一致的。

熱點的兩種解法:複合 Key 各顯神通

有趣的是,範圍分區與 Hash 分區都能靠「複合 key」來緩解自己的弱點——只是解法的方向剛好相反。

📡
範圍分區的解法:sensor 名稱前綴

壞 key:2026-06-17T14:30:05(時間在前,寫入全擠今天)。好 key:A12#2026-06-17T14:30:05(sensor 名稱在前,先依名稱分區)。只要同時有很多顆 sensor 活躍,寫入就被攤平;代價是查「多顆 sensor 同一時段」得逐一查詢再合併。

🏢
Hash 分區的解法:Cassandra 複合主鍵

複合主鍵 (user_id, update_timestamp) ——只有 user_id 被 hash 決定去哪個分區(負載均勻),update_timestamp 則在分區內部排序( 串接索引 )。固定 user_id 之後,就能對 timestamp 做高效範圍掃描。

📱
實戰場景:社群網站的貼文

一個使用者會發很多則貼文,這是典型的一對多關係。把主鍵設計成 (user_id, update_timestamp):不同使用者的貼文被 hash 打散到不同分區,負載均勻;但同一個使用者的貼文,在他所在的那個分區內,是依時間排好的——固定 user_id、查某段時間的貼文,一次就能高效掃出來,魚與熊掌各拿一半。

動手配配看:這幾種 Key,該用哪招?

把下面三種鍵,拖到它最適合的分區策略或熱點緩解做法。配完按「對答案」。

時間序列感測器資料(key 是時間戳記)
隨機產生的使用者 ID
突然爆紅的名人帳號(單一 key 被瘋狂讀寫)
想保留範圍查詢(例如撈某段時間的讀數),又要避開「今天」這個分區被寫爆——該在 key 前面加上什麼來分散?
拖到這裡
本身已經夠隨機、彼此獨立,用一般 Hash 分區就能自然均勻分散,不需要額外處理
拖到這裡
同一個 key 的 hash 永遠相同,Hash 分區完全幫不上忙——只能在這個 key 前後加上隨機數字,拆成上百個 key 硬性分散
拖到這裡
🌟
名人效應:Hash 也救不了的極端案例

如果所有讀寫都集中在同一個 key(例如某位百萬粉絲名人的帳號),Hash 分區完全無用武之地——因為 同一個 key 的 hash 永遠相同, 一百萬次請求全部導向同一個分區。書中提到,曾經 Twitter 有 3% 的伺服器是專門為了 Justin Bieber 而存在的。緩解方式是在 熱 key 前後加上隨機數字(例如兩位數,拆成 100 份),把寫入打散;但讀取時要掃齊全部 100 份再合併,還得靠一本 簿記(bookkeeping) 記住哪些 key 被拆過——這是用讀取成本換寫入不再有熱點的 權衡取捨,目前大多數系統都無法自動偵測補償,得靠工程師自己手動決定。

小試身手

兩種分區法的取捨、還有熱 key 的緩解招式,來檢查一下有沒有記牢。

關於依範圍分區與依 Hash 分區的取捨,下列敘述何者最準確?
某位百萬粉絲名人一發文就讓同一個 key 湧入海量寫入,Hash 分區對此束手無策。書中建議的緩解方式是什麼?
🔑
下一站:主鍵以外的查詢

分區解決了「用主鍵查詢」的擴展問題——不管是範圍還是 Hash,前提都是你手上已經有那把 key。但如果使用者想用「主鍵以外」的欄位查詢呢?往下捲,看看次級索引怎麼接手這個難題。

03

次要索引的分區難題

主鍵查一筆很乾脆,但「找所有紅色的車」為什麼會逼你在讀跟寫之間選邊站?

先複習:只靠主鍵,分區很單純

前面幾節都假設資料是用簡單的 主鍵(primary key) 存取的:你知道 key,就能直接算出它落在哪個 分區(partition),把讀寫請求精準送過去。像查百科全書,知道條目標題,翻到那一頁就找到了——一個 key 對一筆,乾淨俐落。

但真實應用很少只靠主鍵查資料。你常常想問:「user 123 做過哪些事」「所有紅色的車有哪些」——這種「給一個值、找一群」的需求,就是這一模組要拆開講的麻煩。

1
Primary Key:一個 key 對一筆

知道 key 就能直接定位到某一個分區,結果通常恰好一筆。

2
Secondary Index:一個值對一群

「顏色是紅色」不是唯一值,符合條件的可能有 0 筆、1 筆,甚至上萬筆。

3
麻煩的根源

分區是依主鍵切的,但同色的車,主鍵可能散落在每一個分區裡。

📚
圖書館的兩種找書方式

圖書館按索書號把書分到各棟樓(分區),拿著索書號直接找到一本書,精準又快。但「找所有跟貓有關的書」是照主題找,這些書可能散落在 A 棟、B 棟、C 棟每一棟樓裡——因為樓是按索書號分的,不是按主題分的。

直接讀原文,旁邊就是白話

這一段是全書講「次要索引為什麼麻煩」的關鍵幾句,我們把原文和白話一句對一句放在一起。

原文 · DDIA Ch.6 A secondary index usually doesn't identify a record uniquely but rather is a way of searching for occurrences of a particular value: find all actions by user 123, find all articles containing the word hogwash, find all cars whose color is red, and so on. Secondary indexes are the bread and butter of relational databases, and they are common in document databases too. And finally, secondary indexes are the raison d'être of search servers such as Solr and Elasticsearch. The problem with secondary indexes is that they don't map neatly to partitions.
白話翻譯

次要索引通常不會唯一標識一筆資料,而是用來搜尋「擁有某個值」的所有紀錄:找出 user 123 的所有動作、找出所有含「hogwash」這個字的文章、找出所有紅色的車,諸如此類。

次要索引是關聯式資料庫的家常必備,文件資料庫裡也很常見。

而它更是 Solr、Elasticsearch 這類搜尋伺服器存在的全部理由。

次要索引真正麻煩的地方在於:它沒辦法乾淨地對應到分區上。

💡
一句話記住這模組

次要索引(secondary index)不能乾淨對應到分區——因為符合條件的資料,主鍵天生散落在每一個 partition 裡。接下來兩種做法,就是在這個麻煩上給出的兩個不同答案。

做法一:每個分區只管自己的索引(local index)

以二手車網站為例:每筆車輛刊登有唯一的 document ID,資料庫依 ID 分區(0~499 在分區 0,500~999 在分區 1……)。使用者想用顏色廠牌篩選,於是你在這兩個欄位上宣告 次要索引

關鍵特性:每個分區完全獨立,只替「自己內部」的文件維護索引,完全不管別的分區裡有什麼。一台紅色的車被加進某分區,就是那個分區自己把它加進 color:red 這個索引項——這也是為什麼它又叫「區域索引(local index)」。

✍️
寫入很單純

新增一筆資料,只需要更新它所在的那「一個」分區的索引,代價低。

📡
讀取要撒網

紅色的車沒有理由全落在同一分區,查詢必須送到「所有」分區再彙總,這叫 scatter/gather

🐌
被最慢那條腿拖住

整體要等所有分區回完才算數,只要一個分區偶爾慢, 尾端延遲就被放大。

🏬
沒有總目錄的連鎖書店

每間分店只替自己店裡的書做卡片目錄。上架一本紅色封面的書,只要更新那一間店的目錄,完全不用通知別間店——寫入很輕鬆。但客人問「整個連鎖有哪些紅色封面的書」,店員只能打電話問遍每一間分店,各自查完再彙整回報。只要有一間店店員慢半拍,整筆查詢就被它拖住。MongoDB、Cassandra、Elasticsearch、Riak 等都採用這種做法。

做法二:一座橫跨所有分區的中央索引(global index)

第二種做法反過來:建立一個涵蓋「全部分區」資料的 全域索引(global index)。紅色的車不管原本住在哪個分區,在這裡通通被收進同一個 color:red 條目底下。

但全域索引不能只塞在一台機器上——那樣所有查詢都會打它,變成瓶頸,正好違背當初分區的用意。所以全域索引本身也必須被分區,這種分法叫 依詞彙分區(term-partitioned),因為決定一筆索引放哪個分區的,是我們要查的那個「term」本身,可以依 term 本身的值分(利於範圍查詢)或依 term 的 hash 分(負載更均勻)。

讀取很快

要找紅色的車,只要打中「包含 color:red 這個 term」的那一個分區就好,不用撒網。

🧩
寫入變複雜

一筆文件有顏色、廠牌等多個 term,可能分屬索引的不同分區,一次寫入要同時更新好幾處。

⏱️
常靠非同步補上

要即時一致得跨分區做 分散式交易,代價太高,實務上常改採 非同步更新

🚇
捷運全線的失物招領總台

不再是每站各自登記,而是全線共用一個總台,按「物品名稱」把所有站撿到的東西集中分類——a~r 開頭一櫃、s~z 開頭一櫃。找紅雨傘只要走到對應那一櫃,一次問到全線清單,超快;但新登記一件同時有多個特徵的物品,得同時跑好幾個櫃台,才叫麻煩。DynamoDB 的 global secondary index 正常情況下不到一秒就補上,但故障時傳播延遲會拉長。

動手跑一遍:兩條查詢路徑,正面對決

同一個查詢「找出所有紅色的車」,走 local index 和走 global index,過程完全不一樣。按「下一步」,看封包怎麼跑。

🙋
使用者查詢
🧭
協調節點
🗂️
分區 0(本地索引)
🗂️
分區 1(本地索引)
🌐
全域索引分區
按「下一步」開始比較兩條路徑
⚖️
同一顆球,丟給誰接都要付代價

路徑一(依文件分區的本地索引)把「找齊」的工作丟給讀取——每次查詢都要問過所有分區。路徑二(依詞彙分區的全域索引)把工作挪到寫入——每次新增資料都要同步更新好幾個索引分區。天下沒有白吃的午餐,只是「誰先付」的差別。

怎麼選?看你的讀寫比例

兩種做法沒有絕對的好壞,關鍵是你的系統「寫得多」還是「讀得多」,以及你能不能接受「剛寫完、馬上查可能還查不到」。

寫多讀少 → local index

寫入只碰一個分區,代價低;讀取的 scatter/gather 成本你比較少遇到。

讀多寫少 → global index

查詢只打中一個索引分區,很快;願意用寫入變複雜、非同步延遲來換。

更聰明的招:讓查詢常帶的欄位當分區依據

例如社群網站把貼文主鍵設計成 (user_id, timestamp),同一使用者的貼文自然落在同一分區,查詢就不必跨分區。

🧭
別忘了盯尾端延遲

如果你的系統真的躲不掉 scatter/gather,記得把所有分區「平行」查詢、並監控 p99/p999 延遲,而不是只看平均值——平均低不代表使用者不會等到抓狂。

小試身手

兩條路徑、兩種代價,來兩題確認你抓到重點。

採用「依文件分區的本地索引(local index)」時,為什麼查詢「所有紅色的車」通常很貴?
「依詞彙分區的全域索引(global index)」讀取快,但為什麼寫入常常是非同步更新,而不是寫完立刻反映?
🔀
下一站:重新分區

分區指派不會永遠不變——機器會加入、會退場,資料要怎麼跟著重新分配?

04

再平衡:叢集會變動的藝術

機器會增、會減、會掛掉——資料要怎麼搬,才不會把自己搬垮?

叢集不是裝好就一勞永逸

前面幾個模組,我們把資料切成一塊塊 分區(partition), 分散到多台 節點(node) 上。但故事沒有結束——資料庫的世界一直在變,三種情境會逼你動手調整:

1
吞吐量上升

查詢請求變多,現有 CPU 忙不過來,你得加機器分攤運算。

2
資料變大

資料集越長越大,硬碟與記憶體快裝不下,你得加機器擴容。

3
機器故障

某台節點掛了,它身上的責任總得有人接手。

這三種情境有一個共同點:都需要把資料和請求,從某個節點搬到另一個節點。這整件事,就叫 再平衡(rebalancing)

🍲
想像一家火鍋店連鎖

每家分店是一個節點,每桌客人是一份分區,服務生忙不忙就是負載。客人暴增要多請店員(加 CPU)、桌子不夠要加桌加冰箱(加硬碟與 RAM)、分店淹水關門要鄰店接手(機器故障)。再平衡,就是店長重新調度人力與客人的那個過程。

好店長調度時要守的三條鐵則

不管你用哪種分區策略,只要有節點加入或離開,再平衡就一定要滿足三個最低要求。我們直接把原文和白話放在一起讀:

原文 · DDIA Ch.6 The process of moving load from one node in the cluster to another is called rebalancing. After rebalancing, the load (data storage, read and write requests) should be shared fairly between the nodes in the cluster. While rebalancing is happening, the database should continue accepting reads and writes. No more data than necessary should be moved between nodes, to make rebalancing fast and to minimize the network and disk I/O load.
白話翻譯

把負載從叢集裡的一個節點搬到另一個節點,這個過程就叫「再平衡」。

再平衡結束後,負載(資料儲存、讀取請求、寫入請求)應該在叢集的各節點之間公平分攤。

再平衡進行的當下,資料庫仍然要能繼續接受讀取和寫入。

搬移的資料不該超過必要範圍,這樣才能讓再平衡跑得快,也把網路和磁碟的 I/O 負擔降到最低。

📋
三條鐵則,記一句話

「跑完公平嗎?跑的時候還能用嗎?是不是只搬了該搬的?」——下次有人提再平衡方案,問這三句就能快速看穿好壞。

反面教材:hash mod N 錯在哪

切分區時,一個看起來很自然的點子是:拿 hash(key) mod N (N 是節點數)來決定 key 該放哪台機器。10 個節點就對 10 取餘數,答案剛好落在 0~9,看起來完美——直到你想加機器。

用同一把 key(hash(key) = 123456)示範,只是把節點數從 10 一路加到 12,它被分到的節點就整個亂跳:

🔟
10 個節點

123456 mod 10 = 6 → key 落在節點 6。一切正常運作中。

加到 11 個節點

123456 mod 11 = 3 → 同一把 key 被迫搬到節點 3。key 本身完全沒變,只因為 N 變了。

再加到 12 個節點

123456 mod 12 = 0 → 又得搬到節點 0。而且不是它一個,幾乎全叢集的 key 都在同時大搬家。

🚨
錯不在 hash,錯在把 N 綁進公式

hash mod N 的問題根源,是把「節點數 N」直接寫進了 key 到節點的對應公式裡。只要 N 一動,對應關係就全盤崩解——明明只是多加一台機器,理應只搬一小部分資料,結果卻逼著幾乎所有 key 跨節點搬家,網路與磁碟 I/O 瞬間暴衝,嚴重違反「別搬超過必要範圍」這條鐵則。

正解:分區數量先訂死,不再跟著節點數變

既然問題出在「N 一變、對應全崩」,那就讓 key 到分區的對應 跟節點數脫鉤:一開始就切出遠多於節點數的 固定數量分區(fixed number of partitions),之後不管加減節點,分區「總數」都不再變——變的只是「哪個節點管哪幾塊分區」。按下一步,看新節點加入時到底發生了什麼事:

🧩
1000 個固定分區
🖥️
節點 A
🖥️
節點 B
🆕
新節點 C
按「下一步」開始
📚
圖書館比喻:釘死書架數,換人不換架

圖書館一開館就釘死 1,000 個書架,只請 10 位館員,每人先顧約 100 個。新館員報到時,不必重排所有書,只要從老館員手上接走幾座整書架就好——書放在哪個書架完全不動,變的只是「哪位館員顧哪些書架」。這正是 Riak、Elasticsearch、Couchbase、Voldemort 採用的做法。

搬的時候還能不能正常用?搬的決定要不要人管?

光是「搬得少」還不夠,鐵則二說:搬移期間資料庫仍要能正常讀寫。實作上通常會 保留舊的分區對應關係繼續服務中的讀寫,等資料完整複製到新節點後,再一次性、原子地切換過去——使用者幾乎感覺不到搬家正在發生。

另一個常被忽略的問題是:這個搬家的決定,要不要讓系統自己做?答案落在一條光譜上:從 全自動(系統自己判斷、自己動手)到全手動(管理員手動指定),中間還有一種折衷——系統自動算出建議方案,但要等管理員按下確認才真正執行。Couchbase、Riak、Voldemort 就是這樣做的。

⚠️
全自動最怕遇到誰:自動故障偵測

某節點只是暫時變慢(負載太高),其他節點卻誤判它已經死了;系統立刻自動再平衡,把負載硬搬走——搬資料本身就很重,反而讓這台喘不過氣的節點、其他節點和網路壓力更大,觸發更多節點被拖慢、被誤判,滾雪球式地演變成 連鎖故障(cascading failure)。 這正是為什麼讓 人類介入(human in the loop) 往往是明智之舉——慢一點,但不會一起垮。

小試身手

再平衡的三條鐵則、hash mod N 為什麼是地雷、以及固定分區怎麼解——來兩題檢查一下:

當叢集從 10 台節點擴充到 11 台,若採用 hash(key) mod N 決定 key 該放哪台節點,會發生什麼、違反了哪條再平衡鐵則?
要避免 hash mod N 的搬家災難,下列哪種設計方向最符合本節精神?
🧭
下一站:分區指派搬來搬去,客戶端的請求要怎麼知道該往哪裡送?

分區數量固定了,歸屬也會因為再平衡不斷改變——但使用者送出一個請求時,它要連到哪一台節點才問得到正確的資料?下一站,我們就去看請求路由怎麼解決這個問題。

05

請求路由:找到對的分區

資料被拆成一塊塊之後,客戶端到底該連去哪一台機器?

我想讀 key「foo」,該連哪一台機器?

資料被切成很多個 分區 、散在很多節點上之後,一個很現實的問題冒出來:客戶端想讀或寫 key「foo」,到底要連到哪一個 IP 位址與 port?

麻煩在於,分區指派給哪個節點不是固定的。叢集做 再平衡 ——加機器、退機器、或某台機器掛了——分區跟節點的對應關係就會跟著改變。一定要有「某個角色」隨時盯著這份對照表,才答得出上面那個問題。

🗺️
這其實是個更大的問題

書裡把這件事歸類成更通用的 服務發現 ——不只資料庫,任何想做高可用、跑在多台機器上互為備援的網路服務,都會遇到「客戶端該連誰」這個問題。

原文怎麼寫這個問題

這一段是全書定義「請求路由」問題的關鍵幾句。原文在左,白話在右,一句對一句。

原文 · DDIA Ch.6 When a client wants to make a request, how does it know which node to connect to? As partitions are rebalanced, the assignment of partitions to nodes changes. This is an instance of a more general problem called service discovery. In all cases, the key problem is: how does the component making the routing decision learn about changes in the assignment of partitions to nodes? These [IP addresses] are not as fast-changing as the assignment of partitions to nodes, so it is often sufficient to use DNS for this purpose.
白話翻譯

當客戶端想發出請求,它怎麼知道要連到哪個節點?隨著分區被再平衡,分區指派給哪個節點的關係會一直變。

這其實是一個更通用的問題的其中一個例子,叫做服務發現。

不管哪種情況,關鍵問題都一樣:做路由決策的那個角色,要怎麼得知分區指派的變化?

機器的 IP 位址,變動速度遠不如分區指派來得快,所以用 DNS 來處理 IP 通常就夠了。

💡
一句話記住這節的核心

路由問題最終都收斂成同一句話:「對照表變了,怎麼讓每個人都知道,而且知道的是同一份?」

三種帶路方式:點點看,誰負責做決定?

想像一間有上百間診間的大型醫院。每位醫師(節點)負責特定科別的病人,而「哪位醫師看哪一群病人」會隨醫師輪調、請假、新人報到而不斷變動——這正對應著再平衡。你(客戶端)拿著健保卡(key)走進來,問題是:我到底該去哪一間診間?點下面三個元件,看醫院怎麼帶你找到人。

🏥 解法一:任意節點轉發
🧭 解法二:路由層
📱 解法三:分區感知客戶端
點上面任一個解法,看醫院怎麼帶你找到對的診間。
⚠️
三種解法背後,其實是同一道難題

不管決策交給節點、路由層還是客戶端,真正困難的問題都一樣:做決策的那個角色,要如何得知分區指派的變化,而且所有參與者要對同一份對照表達成一致?對照表沒對齊,導引台就會把你送到錯的診間。

那張對照表,怎麼保持最新?

醫師輪調這麼頻繁,櫃台、導引台、你的 App 怎麼知道最新情況?書裡點出兩種主流做法。按「下一步」看它們如何運作。

🖥️
節點 A(變動發生處)
🗂️
ZooKeeper(協調服務)
🧭
路由層/分區感知客戶端
🖥️
節點 B(gossip 鄰居)
📨
其他節點/客戶端
按「下一步」看兩種方案怎麼讓對照表保持最新
📢
方案 A:中央廣播室(ZooKeeper)

每位醫師報到都要去協調服務登記,它握有唯一權威的對照表;一有輪調,就主動廣播通知大家改表。HBase、SolrCloud、Kafka、MongoDB 都走這條路。

🗣️
方案 B:鄰居口耳相傳(gossip)

沒有中央廣播室,節點之間靠 八卦協定 互相通報「3 號診間換人了」,消息逐步擴散全場。好處是不必養一個外部單位,代價是複雜度被推回節點自己身上。Cassandra、Riak 走這條路。

真實系統怎麼選?還有一個容易被忽略的細節

這三種解法、兩種同步方案不是紙上談兵,主流資料庫各有偏好:

1
Cassandra/Riak

解法一+gossip:請求可送到任意節點,該節點用 gossip 學到的叢集狀態轉發。不依賴外部協調服務,部署較自足,代價是節點本身更複雜。

2
MongoDB/LinkedIn Espresso

解法二:MongoDB 用 mongos 當路由層、自家 config server 保存對照表;Espresso 的路由層由 Helix 管理,Helix 底層仍靠 ZooKeeper。

3
HBase/SolrCloud/Kafka

都直接用 ZooKeeper 追蹤 partition 指派,是最典型的「協調服務」派。

📇
一個常被忽略的細節:IP 還是要靠 DNS

不管用路由層還是把請求丟給隨機節點,客戶端終究要先找到能連的 IP 位址。好消息是:IP 的變動遠比分區指派來得慢,所以通常用 DNS 就足夠了——「機器在哪」變動慢交給 DNS,「分區歸誰」變動快才需要 ZooKeeper/gossip/路由層。

小試身手

路由這節看懂了嗎?來兩題檢查一下。

書中把「請求路由」歸為一個更通用問題的特例,這個更通用的問題是什麼?
某團隊想部署分區資料庫,但極力避免額外維運一套像 ZooKeeper 的外部協調服務,最符合需求的做法是?
🧭
下一站

分區、索引、再平衡、路由全部攤開,串成一個查詢真實走過的完整旅程。往下捲,我們把整章地圖收攏成一趟旅程。

06

大局:一個查詢的完整旅程

把切分區、找路由、查索引串成一條線——看一個查詢怎麼從你手上,走到正確的那一小塊資料

先倒帶:我們為什麼要把資料切開

這一章從頭到尾都在回答一個很現實的問題:當資料多到一台機器 存不下、也跑不動 的時候,該怎麼辦?答案是 分區(partitioning) ——把一份巨大的資料集切成一塊塊,攤到很多台各自獨立的機器上。每一塊都乾淨俐落地只歸屬一個 shared-nothing 節點管,理論上機器加得越多,能扛的資料量與查詢量就跟著往上加。

但光是「切開」還不夠。切完之後還有兩件事必須說清楚:怎麼切(哪一筆資料歸哪一塊),以及切完之後,查詢怎麼找得到路。這正是這一章一路鋪陳下來的骨架:

1
怎麼切(分區策略)

依 key 範圍切、或依雜湊切——決定「這筆資料該落在哪一塊」。

2
次級索引怎麼辦

如果查詢不是靠主鍵,而是「找所有紅色的車」,事情就沒那麼單純了。

3
再平衡

節點增減時,怎麼把資料搬來搬去,又不把整個叢集搬到停擺。

4
請求路由

客戶端到底該連哪一台機器?誰負責隨時更新這份「地圖」。

🧭
這個模組要做的事

前面幾個模組是分開教這四塊拼圖。這裡我們把它們拼回同一張圖——跟著一個真實查詢,看它怎麼從你的應用程式出發,一路走到正確的那一小塊資料,再彙總結果回到你手上。

原文一句話,先定調這一章在忙什麼

這一章開頭就把問題講得很白:複製解決不了「一台機器扛不住」的問題,於是我們得把資料切開。往下捲之前,先看看書裡怎麼講這件事。

原文 · DDIA Ch.6 For very large datasets, or very high query throughput, that is not sufficient: we need to break the data up into partitions, also known as sharding. Normally, partitions are defined in such a way that each piece of data (each record, row, or document) belongs to exactly one partition. In effect, each partition is a small database of its own, although the database may support operations that touch multiple partitions at the same time. The main reason for wanting to partition data is scalability.
白話翻譯

面對超大資料集或超高查詢量,光靠複製還不夠:我們得把資料切開分區,這也叫做 sharding(分片)。

通常分區的定義方式是:每一筆資料(每一筆 record、row 或 document)剛好屬於一個分區,不多也不少。

實際上,每一個分區就像自己的一個小型資料庫,儘管資料庫仍可能支援同時碰到多個分區的操作。

想要分區資料,最主要的理由就是可擴展性。

💡
記住這句就夠了

「每一個 partition 就像自己的一個小型資料庫」——這句話是整章的地基。後面不管講 key 範圍、雜湊、次級索引還是路由,本質都在回答同一個問題:這個「小型資料庫」該怎麼切、切完怎麼找。

跑一次完整旅程:帶著次要條件的查詢

假設你在經營前面提過的二手車網站,使用者在畫面上打了「找出所有顏色是紅色的車」——這不是用 主鍵 查一筆,而是靠次級索引找一群。按「下一步」,看這個查詢怎麼一路走到底。

🖥️
客戶端
🚦
路由層
🗂️
協調服務
🔧
分區一
🔧
分區二
🔧
分區三
按「下一步」開始這趟查詢旅程
⚠️
如果查的是主鍵,這條路徑會短很多

如果使用者查的是「這台車的 document ID」,路由層只要算出它落在哪一個分區,直接把請求送過去那一台就好——不必驚動其他分區。今天走的這條長路,正是因為「顏色」是次級索引,天生橫跨所有分區,才逼出了 scatter/gather 這種「全部問一遍再彙總」的查法。

切法會決定查詢好不好走

剛剛那趟旅程之所以要「問過所有分區」,根源在於次級索引和分區規則本來就對不齊。但「怎麼切」這件事本身,也大有講究——選錯了,連最基本的查詢都會卡住。

📚
依 Key 範圍分區

把一段連續的 key(例如某個月的時間戳)交給一個分區。範圍查詢超快,因為分區內本來就排好序——但代價是熱門的那一段(例如「今天」)容易被擠爆,形成 熱點

🔀
複合 Key 分散熱點

把「sensor 名稱+時間戳」這種 複合 key 前綴放最前面,寫入就能分散到多個分區——但換來的代價是:跨前綴的範圍查詢,得逐一分開查再合併。

🔍
次級索引天生跨區

分區是照主鍵切的,但「顏色是紅色」這種條件的資料,其主鍵散落在每一個分區裡——這正是它「不能乾淨對應到分區」,非得 scatter/gather 不可的根本原因。

🎯
選型時真正在問的問題

不是「哪種分區法最好」,而是「我的查詢模式,最常是哪一種?」如果常常要按 user 找他的所有貼文,就把 user_id 放進複合主鍵,讓資料落在同一分區;如果常要按顏色、按標籤篩選,就得接受次級索引跨分區的代價。

誰在幫查詢帶路:三種路由架構回顧

資料切好了、索引也建好了,但客戶端要連到「哪一台機器」,還得靠某個角色隨時盯著這份會變動的 分區對照表。點點看下面三種帶路方式:

🔀 任意節點轉發
🚦 路由層轉發
🎯 分區感知客戶端
解法一:任意節點轉發。客戶端隨便連一個節點,如果剛好是對的就直接處理;不是的話,這個節點會幫你轉發給正確的節點,再把結果帶回來。Cassandra、Riak 用這招,靠 gossip 協定在節點間口耳相傳分區異動,好處是不必依賴外部協調服務。
🔑
三種解法背後同一個難題

不管誰做決策,真正困難的都是同一件事:這個角色要怎麼隨時得知「分區指派變了」,而且所有參與者都得對同一份對照表達成一致——這正是前面那趟查詢旅程裡,路由層要先問過協調服務的原因。

整章總複習

從「為什麼要切」到「查詢怎麼找到路」,你已經走完這一章的完整地圖。來兩題,檢查一下這幾塊拼圖有沒有接對。

在二手車網站的例子中,為什麼「找出所有紅色的車」這個查詢,需要同時問過所有分區、再把結果彙總(scatter/gather),而不能像查主鍵一樣直接連到一台機器?
不管採用任意節點轉發、路由層、還是分區感知客戶端,這三種請求路由解法背後,真正困難的共通問題是什麼?
🔐
下一站(其他章)

資料切好分區、找得到了,下一個現實問題是——當系統出錯、機器之間意見不合時,怎麼還能保證資料正確?