為什麼要分區
一台機器裝不下、也算不動全部資料時,就把資料切開,分給很多台機器一起扛
先聽一句 60 年前的提醒
這一章開頭引用了電腦科學先驅 Grace Murray Hopper 在 1962 年說的話。她講的不是資料庫,但那個精神,剛好就是這一整章要做的事——別再把資料硬綁在「一步接一步」的單機思維裡,而是老實描述資料之間的關係,讓系統自己決定怎麼分工。
「我們必須擺脫『一步接一步』的思維,不要把電腦綁死。我們該做的,是講清楚資料的定義、優先順序與描述方式。我們要陳述的是『關係』,而不是『步驟』。」
——葛麗絲・霍普,《管理與未來的電腦》(1962)
上一章談的是複製:把同一份資料,多存幾份放到不同節點上。
但資料量大到一個程度、或查詢多到一個程度,光複製不夠用——我們得把資料切開成好幾塊,這個動作也叫做 sharding(分片)。
通常分區的規則會定成:每一筆資料,剛好屬於其中一塊,不多也不少。
「不要限制電腦」放到這一章,就是別把所有資料硬塞進一台機器裡等它慢慢處理——而是把資料的歸屬關係講清楚,讓很多台機器各自認領一塊,同時動工。
複製解決的是「壞掉」,不是「裝不下」
第 5 章教的 複製 很好用,但有個天花板:不管複製幾份,每一台機器都還是得存下「整份」資料。資料一旦大到一個地步,兩個問題會同時冒出來:
單一台機器的硬碟空間有限,一份超大的資料集,塞不進去。
就算塞得下,太多讀寫請求一起湧進來,一顆 CPU、一條硬碟頻寬也處理不完。
解法就是這一章的主角—— 分區(Partitioning), 也常被叫做 分片(Sharding)。 書裡有一句話講得很傳神:每一個 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 倍紅利拿不到。
像「查某個使用者的資料」這種查詢,只打到它所屬的那個節點,加節點就能線性放大吞吐量。
需要橫跨很多分區、彙整全站資料的複雜查詢,雖然也能平行處理,但協調的難度明顯升高。
一位廚師包辦切肉、洗菜、煮湯、擺盤,客人一多就忙到爆。改成 10 位廚師、每人只顧一種食材,點「一盤牛肉」只需要 1 號廚師動手,其他 9 位完全不受影響——出餐速度自然跟著人數往上衝。但如果客人點的是「牛肉、海鮮、蔬菜都要」的綜合大拼盤,就得協調好幾位廚師一起做、再合盤,明顯慢下來。
小試身手
分區這個概念,你抓到了嗎?來兩題檢查一下。
知道「為什麼要分區」只是第一步。下一站:資料要怎麼切才公平,不會某一塊特別熱、某一塊沒人理?
兩大分區法:範圍 vs Hash
先學會用「公平」與「找得到」這把尺,再看兩種切法怎麼各自贏一半、輸一半
分區的野心:讓 10 台機器扛出 10 倍效能
當資料量大到一台機器裝不下、查詢量大到一台機器扛不住,我們就把資料切成好幾塊,分散到不同節點上,這個動作叫 分區(partitioning), 有些系統叫它 sharding。
但「切開」只是手段,真正的目標只有一句話:把資料和查詢負載平均地分散到所有節點上。理想狀況下,10 個節點就該扛起 10 倍的資料量、10 倍的 吞吐量(throughput)。
負載沒有被公平分配,某些分區拿到的資料或查詢比其他分區多很多。
傾斜走到極端後,那個被擠爆、忙到冒煙的倒楣分區,變成整個系統的瓶頸。
熱點的另一面,是其他節點在旁邊納涼——你花了 10 台機器的錢,卻只換到約 1 台的效能。
把超市想成分散式資料庫:每個結帳櫃台是一個分區,排隊客人是查詢負載。四個櫃台各站 3 個人,流動順暢,效率是單一櫃台的 4 倍——這是公平。但如果客人全擠到 1 號櫃台,它大排長龍、結帳員忙到冒汗,另外 3 個櫃台空空如也,整間店的速度被這一個熱點拖垮。問題不是「櫃台不夠多」,而是「人沒有被公平地引導開」。
把每筆資料隨機丟到某個節點,分布確實很平均、幾乎不傾斜。但要讀取某筆特定資料時,你完全不知道它在哪一台——只能「同時問遍所有節點」再拼結果,節點越多、查詢越貴,完全抵消了分區想換來的效率。好的分區法必須同時通過兩關:公平嗎?找得到嗎?下面兩種主流做法,各自用不同方式回答這兩題。
直接讀原文:什麼是「依 Key 範圍分區」
先看書怎麼定義最直覺的第一種分區法——把一段連續的 key 範圍指派給一個分區。
分區的一種做法,是把一段連續的 key 範圍指派給每個分區——就像紙本百科全書的每一冊。
想像一個應用在儲存感測器網路的資料,key 是每筆量測的時間戳。
因為感測器資料是隨著量測發生即時寫入的,所有寫入最後都會落到同一個分區(今天那個),於是那個分區被寫爆,其他分區卻閒著沒事做。
要避免這個問題,key 的第一個元素就不能是時間戳。
你可以在每個時間戳前面加上感測器名稱,讓分區先依感測器名稱、再依時間排。
紙本百科全書某一冊可能只收 A、B(這兩個字母的字特別多),最後一冊卻塞了 T、U、V、X、Y、Z。重點是讓每塊的資料量差不多,而不是字母數量一樣多。每個分區內部的 範圍查詢(range scan) 之所以飛快,正是因為分區內部的 key 本來就保持 排序。 採用這種策略的資料庫包括 Bigtable、HBase、RethinkDB,以及 2.4 版以前的 MongoDB。
點點看:兩種分區法,各贏一半、各輸一半
依範圍分區保留了排序、換來快速的範圍查詢;依 Hash 分區打散了排序、換來均勻分布。點下面兩張卡片,看它們的優缺點各是什麼。
Hash 分區的機制其實只有三步:把 key 丟進 hash function → 得到一個看似隨機、均勻散布的數字 → 把整個數字範圍切成好幾段,每段對應一個分區。這裡的 hash 函數不需要加密等級的強度:Cassandra、MongoDB 用 MD5,Voldemort 用 Fowler–Noll–Vo。
Java 的 Object.hashCode()、Ruby 的 Object#hash,同一個 key 在不同 process 可能算出不同結果——這在分散式系統裡是災難,資料寫進去後在別處就找不回來。分區用的 hash 必須是穩定、跨節點一致的。
熱點的兩種解法:複合 Key 各顯神通
有趣的是,範圍分區與 Hash 分區都能靠「複合 key」來緩解自己的弱點——只是解法的方向剛好相反。
壞 key:2026-06-17T14:30:05(時間在前,寫入全擠今天)。好 key:A12#2026-06-17T14:30:05(sensor 名稱在前,先依名稱分區)。只要同時有很多顆 sensor 活躍,寫入就被攤平;代價是查「多顆 sensor 同一時段」得逐一查詢再合併。
複合主鍵 (user_id, update_timestamp) ——只有 user_id 被 hash 決定去哪個分區(負載均勻),update_timestamp 則在分區內部排序( 串接索引 )。固定 user_id 之後,就能對 timestamp 做高效範圍掃描。
一個使用者會發很多則貼文,這是典型的一對多關係。把主鍵設計成 (user_id, update_timestamp):不同使用者的貼文被 hash 打散到不同分區,負載均勻;但同一個使用者的貼文,在他所在的那個分區內,是依時間排好的——固定 user_id、查某段時間的貼文,一次就能高效掃出來,魚與熊掌各拿一半。
動手配配看:這幾種 Key,該用哪招?
把下面三種鍵,拖到它最適合的分區策略或熱點緩解做法。配完按「對答案」。
如果所有讀寫都集中在同一個 key(例如某位百萬粉絲名人的帳號),Hash 分區完全無用武之地——因為 同一個 key 的 hash 永遠相同, 一百萬次請求全部導向同一個分區。書中提到,曾經 Twitter 有 3% 的伺服器是專門為了 Justin Bieber 而存在的。緩解方式是在 熱 key 前後加上隨機數字(例如兩位數,拆成 100 份),把寫入打散;但讀取時要掃齊全部 100 份再合併,還得靠一本 簿記(bookkeeping) 記住哪些 key 被拆過——這是用讀取成本換寫入不再有熱點的 權衡取捨,目前大多數系統都無法自動偵測補償,得靠工程師自己手動決定。
小試身手
兩種分區法的取捨、還有熱 key 的緩解招式,來檢查一下有沒有記牢。
分區解決了「用主鍵查詢」的擴展問題——不管是範圍還是 Hash,前提都是你手上已經有那把 key。但如果使用者想用「主鍵以外」的欄位查詢呢?往下捲,看看次級索引怎麼接手這個難題。
次要索引的分區難題
主鍵查一筆很乾脆,但「找所有紅色的車」為什麼會逼你在讀跟寫之間選邊站?
先複習:只靠主鍵,分區很單純
前面幾節都假設資料是用簡單的 主鍵(primary key) 存取的:你知道 key,就能直接算出它落在哪個 分區(partition),把讀寫請求精準送過去。像查百科全書,知道條目標題,翻到那一頁就找到了——一個 key 對一筆,乾淨俐落。
但真實應用很少只靠主鍵查資料。你常常想問:「user 123 做過哪些事」「所有紅色的車有哪些」——這種「給一個值、找一群」的需求,就是這一模組要拆開講的麻煩。
知道 key 就能直接定位到某一個分區,結果通常恰好一筆。
「顏色是紅色」不是唯一值,符合條件的可能有 0 筆、1 筆,甚至上萬筆。
分區是依主鍵切的,但同色的車,主鍵可能散落在每一個分區裡。
圖書館按索書號把書分到各棟樓(分區),拿著索書號直接找到一本書,精準又快。但「找所有跟貓有關的書」是照主題找,這些書可能散落在 A 棟、B 棟、C 棟每一棟樓裡——因為樓是按索書號分的,不是按主題分的。
直接讀原文,旁邊就是白話
這一段是全書講「次要索引為什麼麻煩」的關鍵幾句,我們把原文和白話一句對一句放在一起。
次要索引通常不會唯一標識一筆資料,而是用來搜尋「擁有某個值」的所有紀錄:找出 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,過程完全不一樣。按「下一步」,看封包怎麼跑。
路徑一(依文件分區的本地索引)把「找齊」的工作丟給讀取——每次查詢都要問過所有分區。路徑二(依詞彙分區的全域索引)把工作挪到寫入——每次新增資料都要同步更新好幾個索引分區。天下沒有白吃的午餐,只是「誰先付」的差別。
怎麼選?看你的讀寫比例
兩種做法沒有絕對的好壞,關鍵是你的系統「寫得多」還是「讀得多」,以及你能不能接受「剛寫完、馬上查可能還查不到」。
寫入只碰一個分區,代價低;讀取的 scatter/gather 成本你比較少遇到。
查詢只打中一個索引分區,很快;願意用寫入變複雜、非同步延遲來換。
例如社群網站把貼文主鍵設計成 (user_id, timestamp),同一使用者的貼文自然落在同一分區,查詢就不必跨分區。
如果你的系統真的躲不掉 scatter/gather,記得把所有分區「平行」查詢、並監控 p99/p999 延遲,而不是只看平均值——平均低不代表使用者不會等到抓狂。
小試身手
兩條路徑、兩種代價,來兩題確認你抓到重點。
分區指派不會永遠不變——機器會加入、會退場,資料要怎麼跟著重新分配?
再平衡:叢集會變動的藝術
機器會增、會減、會掛掉——資料要怎麼搬,才不會把自己搬垮?
叢集不是裝好就一勞永逸
前面幾個模組,我們把資料切成一塊塊 分區(partition), 分散到多台 節點(node) 上。但故事沒有結束——資料庫的世界一直在變,三種情境會逼你動手調整:
查詢請求變多,現有 CPU 忙不過來,你得加機器分攤運算。
資料集越長越大,硬碟與記憶體快裝不下,你得加機器擴容。
某台節點掛了,它身上的責任總得有人接手。
這三種情境有一個共同點:都需要把資料和請求,從某個節點搬到另一個節點。這整件事,就叫 再平衡(rebalancing)。
每家分店是一個節點,每桌客人是一份分區,服務生忙不忙就是負載。客人暴增要多請店員(加 CPU)、桌子不夠要加桌加冰箱(加硬碟與 RAM)、分店淹水關門要鄰店接手(機器故障)。再平衡,就是店長重新調度人力與客人的那個過程。
好店長調度時要守的三條鐵則
不管你用哪種分區策略,只要有節點加入或離開,再平衡就一定要滿足三個最低要求。我們直接把原文和白話放在一起讀:
把負載從叢集裡的一個節點搬到另一個節點,這個過程就叫「再平衡」。
再平衡結束後,負載(資料儲存、讀取請求、寫入請求)應該在叢集的各節點之間公平分攤。
再平衡進行的當下,資料庫仍然要能繼續接受讀取和寫入。
搬移的資料不該超過必要範圍,這樣才能讓再平衡跑得快,也把網路和磁碟的 I/O 負擔降到最低。
「跑完公平嗎?跑的時候還能用嗎?是不是只搬了該搬的?」——下次有人提再平衡方案,問這三句就能快速看穿好壞。
反面教材:hash mod N 錯在哪
切分區時,一個看起來很自然的點子是:拿 hash(key) mod N (N 是節點數)來決定 key 該放哪台機器。10 個節點就對 10 取餘數,答案剛好落在 0~9,看起來完美——直到你想加機器。
用同一把 key(hash(key) = 123456)示範,只是把節點數從 10 一路加到 12,它被分到的節點就整個亂跳:
123456 mod 10 = 6 → key 落在節點 6。一切正常運作中。
123456 mod 11 = 3 → 同一把 key 被迫搬到節點 3。key 本身完全沒變,只因為 N 變了。
123456 mod 12 = 0 → 又得搬到節點 0。而且不是它一個,幾乎全叢集的 key 都在同時大搬家。
hash mod N 的問題根源,是把「節點數 N」直接寫進了 key 到節點的對應公式裡。只要 N 一動,對應關係就全盤崩解——明明只是多加一台機器,理應只搬一小部分資料,結果卻逼著幾乎所有 key 跨節點搬家,網路與磁碟 I/O 瞬間暴衝,嚴重違反「別搬超過必要範圍」這條鐵則。
正解:分區數量先訂死,不再跟著節點數變
既然問題出在「N 一變、對應全崩」,那就讓 key 到分區的對應 跟節點數脫鉤:一開始就切出遠多於節點數的 固定數量分區(fixed number of partitions),之後不管加減節點,分區「總數」都不再變——變的只是「哪個節點管哪幾塊分區」。按下一步,看新節點加入時到底發生了什麼事:
圖書館一開館就釘死 1,000 個書架,只請 10 位館員,每人先顧約 100 個。新館員報到時,不必重排所有書,只要從老館員手上接走幾座整書架就好——書放在哪個書架完全不動,變的只是「哪位館員顧哪些書架」。這正是 Riak、Elasticsearch、Couchbase、Voldemort 採用的做法。
搬的時候還能不能正常用?搬的決定要不要人管?
光是「搬得少」還不夠,鐵則二說:搬移期間資料庫仍要能正常讀寫。實作上通常會 保留舊的分區對應關係繼續服務中的讀寫,等資料完整複製到新節點後,再一次性、原子地切換過去——使用者幾乎感覺不到搬家正在發生。
另一個常被忽略的問題是:這個搬家的決定,要不要讓系統自己做?答案落在一條光譜上:從 全自動(系統自己判斷、自己動手)到全手動(管理員手動指定),中間還有一種折衷——系統自動算出建議方案,但要等管理員按下確認才真正執行。Couchbase、Riak、Voldemort 就是這樣做的。
某節點只是暫時變慢(負載太高),其他節點卻誤判它已經死了;系統立刻自動再平衡,把負載硬搬走——搬資料本身就很重,反而讓這台喘不過氣的節點、其他節點和網路壓力更大,觸發更多節點被拖慢、被誤判,滾雪球式地演變成 連鎖故障(cascading failure)。 這正是為什麼讓 人類介入(human in the loop) 往往是明智之舉——慢一點,但不會一起垮。
小試身手
再平衡的三條鐵則、hash mod N 為什麼是地雷、以及固定分區怎麼解——來兩題檢查一下:
分區數量固定了,歸屬也會因為再平衡不斷改變——但使用者送出一個請求時,它要連到哪一台節點才問得到正確的資料?下一站,我們就去看請求路由怎麼解決這個問題。
請求路由:找到對的分區
資料被拆成一塊塊之後,客戶端到底該連去哪一台機器?
我想讀 key「foo」,該連哪一台機器?
資料被切成很多個 分區 、散在很多節點上之後,一個很現實的問題冒出來:客戶端想讀或寫 key「foo」,到底要連到哪一個 IP 位址與 port?
麻煩在於,分區指派給哪個節點不是固定的。叢集做 再平衡 ——加機器、退機器、或某台機器掛了——分區跟節點的對應關係就會跟著改變。一定要有「某個角色」隨時盯著這份對照表,才答得出上面那個問題。
書裡把這件事歸類成更通用的 服務發現 ——不只資料庫,任何想做高可用、跑在多台機器上互為備援的網路服務,都會遇到「客戶端該連誰」這個問題。
原文怎麼寫這個問題
這一段是全書定義「請求路由」問題的關鍵幾句。原文在左,白話在右,一句對一句。
當客戶端想發出請求,它怎麼知道要連到哪個節點?隨著分區被再平衡,分區指派給哪個節點的關係會一直變。
這其實是一個更通用的問題的其中一個例子,叫做服務發現。
不管哪種情況,關鍵問題都一樣:做路由決策的那個角色,要怎麼得知分區指派的變化?
機器的 IP 位址,變動速度遠不如分區指派來得快,所以用 DNS 來處理 IP 通常就夠了。
路由問題最終都收斂成同一句話:「對照表變了,怎麼讓每個人都知道,而且知道的是同一份?」
三種帶路方式:點點看,誰負責做決定?
想像一間有上百間診間的大型醫院。每位醫師(節點)負責特定科別的病人,而「哪位醫師看哪一群病人」會隨醫師輪調、請假、新人報到而不斷變動——這正對應著再平衡。你(客戶端)拿著健保卡(key)走進來,問題是:我到底該去哪一間診間?點下面三個元件,看醫院怎麼帶你找到人。
不管決策交給節點、路由層還是客戶端,真正困難的問題都一樣:做決策的那個角色,要如何得知分區指派的變化,而且所有參與者要對同一份對照表達成一致?對照表沒對齊,導引台就會把你送到錯的診間。
那張對照表,怎麼保持最新?
醫師輪調這麼頻繁,櫃台、導引台、你的 App 怎麼知道最新情況?書裡點出兩種主流做法。按「下一步」看它們如何運作。
每位醫師報到都要去協調服務登記,它握有唯一權威的對照表;一有輪調,就主動廣播通知大家改表。HBase、SolrCloud、Kafka、MongoDB 都走這條路。
沒有中央廣播室,節點之間靠 八卦協定 互相通報「3 號診間換人了」,消息逐步擴散全場。好處是不必養一個外部單位,代價是複雜度被推回節點自己身上。Cassandra、Riak 走這條路。
真實系統怎麼選?還有一個容易被忽略的細節
這三種解法、兩種同步方案不是紙上談兵,主流資料庫各有偏好:
解法一+gossip:請求可送到任意節點,該節點用 gossip 學到的叢集狀態轉發。不依賴外部協調服務,部署較自足,代價是節點本身更複雜。
解法二:MongoDB 用 mongos 當路由層、自家 config server 保存對照表;Espresso 的路由層由 Helix 管理,Helix 底層仍靠 ZooKeeper。
都直接用 ZooKeeper 追蹤 partition 指派,是最典型的「協調服務」派。
不管用路由層還是把請求丟給隨機節點,客戶端終究要先找到能連的 IP 位址。好消息是:IP 的變動遠比分區指派來得慢,所以通常用 DNS 就足夠了——「機器在哪」變動慢交給 DNS,「分區歸誰」變動快才需要 ZooKeeper/gossip/路由層。
小試身手
路由這節看懂了嗎?來兩題檢查一下。
分區、索引、再平衡、路由全部攤開,串成一個查詢真實走過的完整旅程。往下捲,我們把整章地圖收攏成一趟旅程。
大局:一個查詢的完整旅程
把切分區、找路由、查索引串成一條線——看一個查詢怎麼從你手上,走到正確的那一小塊資料
先倒帶:我們為什麼要把資料切開
這一章從頭到尾都在回答一個很現實的問題:當資料多到一台機器 存不下、也跑不動 的時候,該怎麼辦?答案是 分區(partitioning) ——把一份巨大的資料集切成一塊塊,攤到很多台各自獨立的機器上。每一塊都乾淨俐落地只歸屬一個 shared-nothing 節點管,理論上機器加得越多,能扛的資料量與查詢量就跟著往上加。
但光是「切開」還不夠。切完之後還有兩件事必須說清楚:怎麼切(哪一筆資料歸哪一塊),以及切完之後,查詢怎麼找得到路。這正是這一章一路鋪陳下來的骨架:
依 key 範圍切、或依雜湊切——決定「這筆資料該落在哪一塊」。
如果查詢不是靠主鍵,而是「找所有紅色的車」,事情就沒那麼單純了。
節點增減時,怎麼把資料搬來搬去,又不把整個叢集搬到停擺。
客戶端到底該連哪一台機器?誰負責隨時更新這份「地圖」。
前面幾個模組是分開教這四塊拼圖。這裡我們把它們拼回同一張圖——跟著一個真實查詢,看它怎麼從你的應用程式出發,一路走到正確的那一小塊資料,再彙總結果回到你手上。
原文一句話,先定調這一章在忙什麼
這一章開頭就把問題講得很白:複製解決不了「一台機器扛不住」的問題,於是我們得把資料切開。往下捲之前,先看看書裡怎麼講這件事。
面對超大資料集或超高查詢量,光靠複製還不夠:我們得把資料切開分區,這也叫做 sharding(分片)。
通常分區的定義方式是:每一筆資料(每一筆 record、row 或 document)剛好屬於一個分區,不多也不少。
實際上,每一個分區就像自己的一個小型資料庫,儘管資料庫仍可能支援同時碰到多個分區的操作。
想要分區資料,最主要的理由就是可擴展性。
「每一個 partition 就像自己的一個小型資料庫」——這句話是整章的地基。後面不管講 key 範圍、雜湊、次級索引還是路由,本質都在回答同一個問題:這個「小型資料庫」該怎麼切、切完怎麼找。
跑一次完整旅程:帶著次要條件的查詢
假設你在經營前面提過的二手車網站,使用者在畫面上打了「找出所有顏色是紅色的車」——這不是用 主鍵 查一筆,而是靠次級索引找一群。按「下一步」,看這個查詢怎麼一路走到底。
如果使用者查的是「這台車的 document ID」,路由層只要算出它落在哪一個分區,直接把請求送過去那一台就好——不必驚動其他分區。今天走的這條長路,正是因為「顏色」是次級索引,天生橫跨所有分區,才逼出了 scatter/gather 這種「全部問一遍再彙總」的查法。
切法會決定查詢好不好走
剛剛那趟旅程之所以要「問過所有分區」,根源在於次級索引和分區規則本來就對不齊。但「怎麼切」這件事本身,也大有講究——選錯了,連最基本的查詢都會卡住。
把一段連續的 key(例如某個月的時間戳)交給一個分區。範圍查詢超快,因為分區內本來就排好序——但代價是熱門的那一段(例如「今天」)容易被擠爆,形成 熱點。
把「sensor 名稱+時間戳」這種 複合 key 前綴放最前面,寫入就能分散到多個分區——但換來的代價是:跨前綴的範圍查詢,得逐一分開查再合併。
分區是照主鍵切的,但「顏色是紅色」這種條件的資料,其主鍵散落在每一個分區裡——這正是它「不能乾淨對應到分區」,非得 scatter/gather 不可的根本原因。
不是「哪種分區法最好」,而是「我的查詢模式,最常是哪一種?」如果常常要按 user 找他的所有貼文,就把 user_id 放進複合主鍵,讓資料落在同一分區;如果常要按顏色、按標籤篩選,就得接受次級索引跨分區的代價。
誰在幫查詢帶路:三種路由架構回顧
資料切好了、索引也建好了,但客戶端要連到「哪一台機器」,還得靠某個角色隨時盯著這份會變動的 分區對照表。點點看下面三種帶路方式:
不管誰做決策,真正困難的都是同一件事:這個角色要怎麼隨時得知「分區指派變了」,而且所有參與者都得對同一份對照表達成一致——這正是前面那趟查詢旅程裡,路由層要先問過協調服務的原因。
整章總複習
從「為什麼要切」到「查詢怎麼找到路」,你已經走完這一章的完整地圖。來兩題,檢查一下這幾塊拼圖有沒有接對。
資料切好分區、找得到了,下一個現實問題是——當系統出錯、機器之間意見不合時,怎麼還能保證資料正確?