Redis Cluster:16384 個 slot 怎麼分片與擴縮
· tech
📑 目錄
單執行緒那篇說過,一台 Redis 的瓶頸是記憶體與網路。當一台裝不下、或流量頂到單機上限,就要把資料分片(sharding)到多台——這就是 Redis Cluster。但它的分片方式很有個性:不用一致性雜湊,而是把整個 key 空間切成固定的 16384 個 hash slot。這個看似古怪的數字,其實是它擴縮容能又快又乾淨的關鍵。
key 落哪台:CRC16 → 16384 slot → node
要自己驗證很簡單: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 c、MULTI 交易、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 有沒有涵蓋滿、有沒有不一致
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 上跟在別的重武器上,是一模一樣的。