分區:range 還是 hash、二級索引擺哪、以及怎麼重新平衡

· tech

#distributed-systems#book-notes#partitioning

📑 目錄

複製是同一份資料放多台;分區(partitioning,也叫 sharding)是把資料切開,每台只放一部分——當資料量一台裝不下、或寫入 throughput 一台吃不消,這是唯一的出路。兩者幾乎總是並用:資料先切成分區,每個分區自己再做主從複製。你其實已經見過好幾個分區系統了:Redis Cluster 的 16384 個 slotKafka 的 partitionMPP 資料庫的分片——這章給你的,是它們背後共通的三道難題:怎麼切、索引擺哪、怎麼重新平衡。

第一題:怎麼切——range 保順序,hash 打熱點

分區的目標是把資料和負載攤平;最怕的就是傾斜(skew)——大家擠在同一區(熱點),分了等於沒分。兩種主流切法,正好是一對取捨:

按 key 範圍切(range) A–F G–R S–Z 像百科全書分冊,key 有序 ✓ 範圍掃描高效(讀連續一段) ✗ key 是時間戳 → 今天的寫入 全砸最後一區(熱點) HBase / 早期 Bigtable 按 key 的 hash 切 分區 0 分區 1 分區 2 hash 把相鄰 key 均勻噴散 ✓ 負載攤平,熱點被打散 ✗ 順序沒了 → 範圍掃描 得問「所有」分區 Cassandra / Redis Cluster(CRC16)/ Kafka 折衷:複合主鍵(第一欄 hash 選分區,其餘欄位在「分區內」照樣排序)—— Cassandra 的招牌 而 hash 救不了「單一超熱 key」(名人問題)—— 那得在應用層加鹽,把一把 key 拆成多把
Range 切像百科全書分冊:key 有序,「查 7 月所有訂單」這種範圍掃描只讀連續一段、超高效;但 key 若是時間戳,今天的寫入全砸在最後一冊——熱點。Hash 切把相鄰 key 均勻噴散:負載攤得平,但順序沒了,範圍掃描得問遍所有分區。折衷是複合主鍵(hash 決定分區、其餘欄位在分區內排序)。還有一個誰都救不了的:單一超熱 key(名人貼文、爆款商品)——hash 再均勻,同一把 key 就是落同一區,只能在應用層「加鹽」把它拆開,跟 快取擊穿是親戚問題

第二題:二級索引擺哪——local 還是 global

主鍵查詢好辦(key → 分區),麻煩的是二級索引:「找出所有紅色的車」——紅色的車散在每個分區裡,索引該擺哪?兩種答案,又是一對取捨:

Local index(各管各的) 分區 0紅:2 台 分區 1紅:1 台 分區 2紅:3 台 查 color=red 讀:scatter/gather 問遍所有分區再合併 ✗ 寫:只動自己那一區 ✓ MongoDB / Cassandra / Elasticsearch Global index(按詞條分區) 「red」歸這區紅車全名單 「blue」歸這區藍車全名單 查 color=red 讀:只問管「red」的那一區 ✓ 寫:一筆動多個詞條 → 跨區更新(多為非同步)✗ DynamoDB GSI(非同步) local = 寫便宜、讀貴(scatter/gather)· global = 讀便宜、寫貴(跨區)—— 又是選「帳付在哪」
Local index:每個分區只索引自己的資料——寫入只動一區、很便宜;但「找所有紅色的車」得 scatter/gather:問遍所有分區、各自查、再合併,分區越多讀越貴。Global index:把索引本身也分區、但按詞條切——「red」的完整名單歸某一區管,查詢只問那一區;代價是一筆寫入若動到多個詞條,要更新多個分區上的索引(所以實務多為非同步,例如 DynamoDB 的 GSI——索引會慢半拍)。local 把帳記在讀、global 把帳記在寫——跟 儲存引擎那篇「整理費付在哪個時間點」是同一道選擇題

第三題:節點增減了,怎麼重新平衡

加機器、換機器,分區要跟著搬——rebalancing 的鐵則只有一條:千萬別用 hash mod N N 一變,幾乎所有 key 的歸屬都變,等於整庫大搬家。主流解法你其實已經在 Redis Cluster 那篇看過活教材:

  • 固定分區數:一開始就切出遠多於節點數的分區(Redis Cluster 的 16384 個 slot、Kafka 的 partition、Elasticsearch 的 shard),節點增減時只搬整個分區的歸屬,key→分區的對應永遠不變。加一層間接,搬家變成搬整齊的箱子。
  • 動態分區:分區大到門檻就自動分裂、小了就合併(HBase)——分區數跟著資料量長。
  • 自動 vs 手動:全自動 rebalancing 很誘人,但 DDIA 提醒它有個陰暗面——節點只是過載變慢時,自動化若誤判它掛了、開始搬資料,搬移本身的負載會把事情雪上加霜,像極了連鎖失效的劇本。所以成熟系統多半「自動提案、人類按下確認鍵」。

最後是路由:請求怎麼找到對的分區?三種——隨便問一台由它轉發、掛一層路由服務(靠 ZooKeeper 記分區表)、或客戶端自己知道(Redis Cluster 的 MOVED 就是讓客戶端把 slot 表快取起來的這一型)。

反思

「保順序」和「打熱點」不可兼得,複合主鍵是我最愛的折衷

range vs hash 這對取捨,我在真實資料裡撞過:時間序列資料按時間 range 切,結果所有當下的寫入永遠砸在最後一個分區——分了十個區,九個在納涼。想通這章後我才看懂 Cassandra 複合主鍵的漂亮:用 hash 決定「去哪一區」(打散熱點),用其餘欄位在「區內」排序(保住範圍掃描)——兩個要求各給一半,但各給在對的維度上。這也解釋了 Kafka 的 key→partitionRedis 的 hash tag 為什麼都長這樣:先用 hash 公平地分家,再在家裡維持秩序。 遇到「既要均勻又要有序」的需求,先想「能不能把兩個要求拆到兩個層級」,常常就解了。

同一道「帳付在哪」的選擇題,第三次出現了

local vs global index,我讀到一半就笑了——這不就是 LSM vs B-tree、乃至一路以來那道題嗎:同一筆成本,你要付在寫入時,還是付在讀取時? local index 寫入便宜(只動本區),把帳掛在每次讀的 scatter/gather 上;global index 讀取精準,把帳掛在跨區的寫入上(所以多半只敢非同步,索引慢半拍)。判準也跟從前一致:讀寫比例決定一切——讀遠多於寫、查詢模式集中,global 划算;寫入密集、查詢能忍 scatter,local 簡單可靠。一個模式在一本書裡反覆出現三次,那它就不是知識點,是這個領域的地心引力——把它內化,新工具的文件你能一目十行。

過載時的自動搬遷,是好心辦壞事的經典劇本

Rebalancing 那段對「全自動」的警告,踩中我 SRE 的神經:節點過載變慢 → 自動化誤判它死了 → 開始把它的分區搬走 → 搬移吃掉更多頻寬與 I/O → 其他節點也慢了 → 更多誤判、更多搬移——教科書級的連鎖失效,而且每一步都是「為你好」。這跟我在 Sentinel 講的「誤判比不作為更貴」同源,但資料搬移的版本更兇,因為搬資料本身就是重負載。所以我對「資料層的自動化」的立場比對「無狀態層」保守一級:Pod 掛了自動補、沒問題;但要大規模搬資料的決定,自動提案、人來按鍵——在最混亂的時刻,把「踩油門」的權力留給人。這章用一個機制設計,講透了一條維運哲學。