Redis Cluster:16384 個 slot 怎麼分片與擴縮

· tech

#redis#distributed-systems

📑 目錄

單執行緒那篇說過,一台 Redis 的瓶頸是記憶體與網路。當一台裝不下、或流量頂到單機上限,就要把資料分片(sharding)到多台——這就是 Redis Cluster。但它的分片方式很有個性:不用一致性雜湊,而是把整個 key 空間切成固定的 16384 個 hash slot。這個看似古怪的數字,其實是它擴縮容能又快又乾淨的關鍵。

key 落哪台:CRC16 → 16384 slot → node

key 落哪台:CRC16 → 16384 slot → node key:「user:1000」 CRC16(key) % 16384= slot 5798 Node Aslot 0 – 5460(+ 一個 replica) Node B ✓slot 5461 – 109225798 在這 → 由 B 保管 Node Cslot 10923 – 16383(+ 一個 replica) 16384 個固定 slot 是「中間層」,node 只是認領一段 slot 所以搬資料 = 搬 slot;擴縮容乾淨可控,不必重算全部 key 的位置
找一把 key 的家分三步:CRC16(key) % 16384 算出 slot 編號,再看哪個 node 認領了這個 slot。這裡的巧思是那層固定的 16384 個 slot——key 對應到 slot 是永遠不變的,會變的只是「哪個 node 認領哪些 slot」。所以擴縮容時,你搬的是整段 slot(連同裡面的 key),而不是像一致性雜湊那樣重算一堆 key 的落點。這跟 Kafka 的 partition 是同一種「固定分片單位」的智慧

要自己驗證很簡單:CLUSTER KEYSLOT user:1000 就會回傳那把 key 的 slot 編號。key→slot 是純函數、永遠不變;slot→node 才是會隨叢集變動的那一半。

客戶端不會迷路:MOVED 與 ASK

分片之後,客戶端怎麼知道該連哪台?答案是節點會糾正你。你隨便打一台,如果那把 key 的 slot 不歸它管,它不會幫你轉發,而是回一個 MOVED <slot> <正確節點>——聰明的客戶端收到後,會把整張 slot→node 對照表快取起來,之後直接打對的節點,不再繞路。

有個特別的變體在擴縮容遷移中出現:某個 slot 正在從舊節點搬去新節點,這時對「已經搬走的 key」,舊節點回的是 ASK(暫時性、只導這一次),而不是 MOVED(永久性、更新對照表)。這個 MOVED / ASK 的分工,正好對應下面擴縮容那張圖。

一個限制:multi-key 操作與 hash tag

分片帶來一個逃不掉的限制:跨 slot 的 multi-key 操作不允許MGET a b cMULTI 交易、Lua 腳本裡碰多把 key——只要這些 key 落在不同 slot(多半在不同 node),Redis 直接回錯,因為它不做跨節點的協調。

解法是 hash tag:在 key 裡用大括號 {} 圈一段,Redis 就只拿大括號裡的內容去算 slot。把相關的 key 綁同一個 tag,它們就保證落在同一個 slot、同一台:

# user:1000 的多把 key,用 {1000} 綁到同一個 slot
SET {user:1000}:profile "..."
SET {user:1000}:cart    "..."
MGET {user:1000}:profile {user:1000}:cart   # ✓ 同 slot,multi-key 可用

要對一組 key 做原子操作,就在設計 key 時先用 hash tag 把它們綁在一起——這是 Cluster 下寫程式最該內化的一條習慣。

各種操作:建叢集、擴容、縮容

重頭戲。Cluster 的日常維運幾乎都靠 redis-cli --cluster 這組工具:

# 建叢集:6 個節點,每個 master 配 1 個 replica(→ 3 主 3 從)
redis-cli --cluster create \
  10.0.0.1:6379 10.0.0.2:6379 10.0.0.3:6379 \
  10.0.0.4:6379 10.0.0.5:6379 10.0.0.6:6379 \
  --cluster-replicas 1

# 檢視(-c 讓 redis-cli 自動跟隨 MOVED/ASK 重定向)
redis-cli -c -p 6379 CLUSTER INFO       # 叢集狀態,看 cluster_state:ok
redis-cli -c -p 6379 CLUSTER NODES      # 每個節點的 id、角色(master/replica)、負責的 slot
redis-cli -c -p 6379 CLUSTER SLOTS      # slot 範圍 → node 對照
redis-cli -c CLUSTER KEYSLOT user:1000  # 某 key 落在哪個 slot

# 擴容:加一台 → 把一部分 slot(連同 key)搬過去
redis-cli --cluster add-node 10.0.0.7:6379 10.0.0.1:6379   # 先加入(預設 master、0 個 slot)
redis-cli --cluster reshard  10.0.0.1:6379                 # 互動式:搬幾個 slot、從誰搬、給誰
redis-cli --cluster rebalance 10.0.0.1:6379               # 或自動把 slot 攤平到所有 master

# 縮容:先把 slot 全搬走,再踢節點(直接 del 有 slot 的節點會出事)
redis-cli --cluster reshard 10.0.0.1:6379 --cluster-from <退休node-id> --cluster-to <其他node-id> --cluster-slots 16384 --cluster-yes
redis-cli --cluster del-node 10.0.0.1:6379 <退休node-id>

# 健檢
redis-cli --cluster check 10.0.0.1:6379   # 檢查 slot 有沒有涵蓋滿、有沒有不一致
擴容 = 搬 slot:加一台 D,從 A/B/C 各撥一段過去 Node A0 – 5460 Node B5461 – 10922 Node C10923 – 16383 Node D(新)認領搬來的 slot reshard:各撥一段 slot(連同裡面的 key)搬到 D 搬遷中:舊節點對已搬走的 key 回 ASK <D>(臨時導這一次) 搬完:對照表更新,之後改回永久的 MOVED
擴容的本質就是搬 slot:加一台 Node D,再 reshard 從既有節點各撥一段 slot(連同裡面的 key)給它。因為分片單位是固定的 slot,你能精準決定「搬多少、從誰搬、給誰」;縮容則相反——先把要退休節點的 slot 全 reshard 走,再 del-node。搬遷過程中那把正在搬的 key,舊節點用 ASK 把請求臨時導去新家,搬完才改回 MOVED,全程不中斷服務

gossip 與故障轉移

節點之間怎麼知道彼此的狀態?靠 gossip 協定——每個節點透過 cluster bus 不斷跟其他節點交換「誰活著、誰負責哪些 slot」,不需要一個中央協調者。當過半的 master 都認為某個 master 掛了(客觀下線),它的 replica 就會升上來接手那段 slot——這又是 過半數決在真實系統裡的現身。所以 Cluster 的 master 數量建議是奇數、且每個 master 都要有 replica,否則一個 master 連同它的 slot 一起消失,整個叢集會因為「有 slot 沒人管」而拒絕服務。

反思

16384 個固定 slot,是「加一層間接」的經典勝利

第一次看到「16384 個 slot」我覺得很莫名——為什麼不直接把 key 雜湊到節點就好?想通之後非常佩服:它在 key 和 node 之間插了一層固定的 slot,而這一層間接,換來了整個擴縮容的乾淨。key→slot 永遠不變,擴縮容只動 slot→node 的歸屬,你能精準地說「把這 1000 個 slot 從 A 搬到 D」,而不是像一致性雜湊那樣「加一個節點,一堆 key 的落點被連帶重算」。這印證了那句電腦科學的老話——「任何問題都可以用加一層間接來解決」。slot 就是那層間接,它讓「分片」這件本來很亂的事,變成「搬整齊的箱子」。我後來設計任何要分片、要重新平衡的系統,都會先想:我的『slot』該是什麼? 找到那個固定的中間單位,擴縮容的難題就解一半。

分散式的便利,總在邊界處收費

Cluster 給你水平擴展,但它在 multi-key 這個邊界上明碼標價:跨 slot 不能一起操作。這讓我想起一條反覆出現的規律——分散式系統的能力,幾乎都在某個邊界處對你收費。單機 Redis 你可以隨意 MULTI 一堆 key,上了 Cluster 就得先用 hash tag 把相關 key 綁在一起、否則免談。這不是 Redis 的缺陷,是分片的本質:資料被切開放在不同機器,「一起操作」就一定有代價。所以我現在上任何分散式方案前,都會先問一句:它把便利收在哪個邊界?——是 multi-key、是跨分片交易、還是跨區延遲?看清楚那道收費站,才不會上線後才被帳單嚇到。

先確認你真的需要 Cluster

最後照例潑冷水。Cluster 很強,但它把很多東西變複雜了:multi-key 限制、客戶端要支援重定向、維運多了 slot 遷移這門功課。很多以為需要 Cluster 的場景,其實一組 夠大的單機 + replica 就解決了——單機 Redis 一秒扛十萬級請求、幾十 GB 記憶體都很平常。真正需要 Cluster,是當你的資料量大到單機記憶體裝不下,或寫入 throughput 高到單核吃不消——這兩個是硬邊界,到了才上。在那之前,加記憶體、加 replica 分讀、優化大 key,通常更划算。先確認痛點是「一台真的不夠」,再走進分片這座山——這條原則,我在 Redis 上跟在別的重武器上,是一模一樣的。