Mini-challenge — consistent hashing ring có virtual node
Lab: cài hash ring với virtual node, lookup key → node O(log N), đo tỷ lệ key di chuyển khi thêm/bớt node so với modulo hashing.
Bạn đang xây phần lõi của một distributed cache gồm N node vật lý. Mỗi key cần được ánh xạ đến đúng node — và khi cluster scale (thêm hoặc bớt node), tỷ lệ key phải di chuyển phải nhỏ nhất có thể để tránh cache stampede và data migration tốn kém.
Bài toán này xuất hiện trong:
- Distributed cache (Memcached, Redis Cluster) — tìm node lưu key mà không cần coordinator tập trung.
- Cassandra token ring — mỗi node sở hữu một đoạn trên vòng tròn hash; thêm node chỉ lấy key từ node kế tiếp.
- CDN edge selection — ánh xạ URL đến edge server gần nhất theo consistent hashing để cache hit rate cao.
Dành 20–25 phút tự implement trước khi xem gợi ý.
🎯 Đề bài
Viết ba hàm tạo thành một hash ring có virtual node:
Hàm 1 — buildRing(nodes, vnodeCount)
- Input: danh sách tên node
nodes(ví dụ["A", "B", "C"]), số virtual node mỗi node vật lývnodeCount(ví dụ 100). - Output: cấu trúc ring — mảng các cặp
(position, nodeName)đã sắp xếp theo position tăng dần trên vòng tròn[0, 2^32). positiontính bằnghash(nodeName + "#" + vnodeIndex)vớivnodeIndextừ 0 đếnvnodeCount - 1.
Hàm 2 — lookup(ring, key)
- Input: ring đã build, chuỗi
key. - Output: tên node vật lý chịu trách nhiệm cho
key— là node vật lý của virtual node đầu tiên trên ring cóposition >= hash(key)(wrap-around về node đầu nếu không tìm thấy). - Yêu cầu độ phức tạp: O(log(N × vnodeCount)) — dùng binary search.
Hàm 3 — measureMigration(nodes, addNode, vnodeCount, sampleKeys)
- Input: danh sách node ban đầu, tên node mới thêm vào, số vnode, danh sách key mẫu.
- Output: tỷ lệ key phải di chuyển (số key đổi node / tổng key).
- Tính bằng cách so sánh
lookuptrước và sau khi thêm node.
🔍 Input-Process-Output
| Consistent hashing ring | Modulo hashing | |
|---|---|---|
| Input | hash(key) ánh xạ lên [0, 2^32) | hash(key) % N |
| Process | Binary search trên sorted ring | Phép chia modulo |
| Output | Node vật lý của vnode kế tiếp | Index node trực tiếp |
| Khi thêm node | Chỉ key trong đoạn của node mới di chuyển | Gần như toàn bộ key đổi node |
📦 Concept mapping
| Bước thực hiện | Learning outcome từ module |
|---|---|
Implement buildRing — sắp xếp vnode | Implement consistent hashing ring (bài 01) |
Implement lookup — binary search trên ring | Implement lookup key → node O(log N) |
Implement measureMigration — so sánh trước/sau | Tính tỷ lệ K/N key di chuyển vs modulo 100% |
| So sánh với modulo hashing | Compare consistent hashing và modulo theo migration rate |
📋 Test cases
| Scenario | nodes | addNode | vnodeCount | Expected migration |
|---|---|---|---|---|
| Thêm node vào cluster 3 node | A, B, C | D | 100 | ~25% (lý thuyết 1/4) |
| Thêm node vào cluster 9 node | A..I | J | 100 | ~10% (lý thuyết 1/10) |
| Modulo hashing baseline 3→4 node | — | — | — | ~75% đổi node |
| Modulo hashing baseline 9→10 node | — | — | — | ~90% đổi node |
Test case modulo baseline: với hash(key) % 3, thêm node thứ 4 làm hash(key) % 4 cho kết quả khác hoàn toàn — gần như mọi key đổi bucket. Consistent hashing chỉ dời key trong đoạn [predecessor_của_node_mới, node_mới].
▶️ Starter pseudocode
function buildRing(nodes, vnodeCount):
ring <- []
for each node trong nodes:
for v from 0 to vnodeCount - 1:
pos <- hash(node + "#" + v) -- position trên vòng tròn [0, 2^32)
ring.append((pos, node))
-- TODO: sắp xếp ring theo pos tăng dần
return ring
function lookup(ring, key):
target <- hash(key)
-- TODO: binary search tìm vị trí đầu tiên trong ring có pos >= target
-- Nếu không tìm thấy (target vượt qua pos lớn nhất): wrap về ring[0]
return ring[idx][1] -- ring[i] = (pos, node), [1] = node
function measureMigration(nodes, addNode, vnodeCount, sampleKeys):
ringBefore <- buildRing(nodes, vnodeCount)
ringAfter <- buildRing(nodes + [addNode], vnodeCount)
moved <- 0
for each key trong sampleKeys:
if lookup(ringBefore, key) != lookup(ringAfter, key):
moved <- moved + 1
return moved / sampleKeys.length
💡 Gợi ý
Mỗi node vật lý được "nhân bản" thành vnodeCount virtual node, mỗi vnode có position riêng trên vòng tròn. Việc này đảm bảo tải phân bố đều: nếu chỉ có 1 position mỗi node, node đặt ở "vùng thưa" trên ring sẽ nhận ít key hơn.
Tạo ring bằng cách tạo tất cả (position, nodeName) rồi sort theo position. Position tính bằng bất kỳ hash function 32-bit nào — quan trọng là cùng hash function cho cả node lẫn key.
Kiểm tra nhanh: với 3 node và 1 vnode mỗi node, ring phải có đúng 3 phần tử đã sort. Với 3 node và 100 vnode, ring có 300 phần tử.
Hàm lookup cần tìm vnode đầu tiên có position >= hash(key). Đây là bài toán "lower bound" — binary search tìm vị trí chèn trong sorted array.
-- Lower bound binary search
lo <- 0
hi <- ring.length
while lo < hi:
mid <- (lo + hi) / 2
if ring[mid][0] < target: -- ring[i] = (pos, node), [0] = pos
lo <- mid + 1
else:
hi <- mid
-- lo là vị trí đầu tiên có ring[lo][0] >= target
Wrap-around: nếu lo = ring.length (target vượt qua position lớn nhất trên ring), vnode chịu trách nhiệm là ring[0] — vì ring là vòng tròn. Đây là trường hợp hay bị quên nhất.
Build ring trước và sau khi thêm node, rồi với mỗi key trong danh sách mẫu, so sánh node được trả về. Nếu khác nhau, key đó phải di chuyển.
Để có kết quả thống kê tốt, danh sách sampleKeys nên đủ lớn (ít nhất 10 000 key) và phân bố đều — ví dụ sinh ngẫu nhiên hoặc dùng chuỗi "key_0" đến "key_9999".
Lý thuyết: khi thêm 1 node vào cluster N node, tỷ lệ di chuyển xấp xỉ 1/(N+1). Với N=3 thêm node thứ 4, kỳ vọng ~25%. Nếu kết quả đo lệch nhiều (vd 40%), kiểm tra lại phân bố của hash function hoặc tăng vnodeCount.
✅ Lời giải
buildRing
function buildRing(nodes, vnodeCount):
ring <- []
for each node trong nodes:
for v from 0 to vnodeCount - 1:
label <- node + "#" + toString(v)
pos <- hash32(label) -- hash 32-bit, kết quả trong [0, 2^32)
ring.append((pos, node))
sort ring theo pos tăng dần -- O(N × vnodeCount × log(N × vnodeCount))
return ring
// Time: O(N·V·log(N·V)) Space: O(N·V) với V = vnodeCount
lookup
function lookup(ring, key):
target <- hash32(key)
lo <- 0
hi <- ring.length
while lo < hi: -- lower bound binary search
mid <- (lo + hi) / 2
if ring[mid][0] < target: -- ring[i] = (pos, node), [0] = pos
lo <- mid + 1
else:
hi <- mid
if lo = ring.length: -- wrap-around: target vượt pos cuối
return ring[0][1] -- [1] = node
return ring[lo][1]
// Time: O(log(N·V)) Space: O(1)
Trace ví dụ (ring đơn giản, 3 node, 1 vnode mỗi node):
Ring (đã sort): [(120, "B"), (450, "C"), (800, "A")]
lookup(ring, "user:42"):
target <- hash32("user:42") = 300
Binary search → lo = 1 (ring[1][0] = 450 >= 300)
return "C"
lookup(ring, "user:99"):
target <- hash32("user:99") = 850
Binary search → lo = 3 (vượt cuối) → wrap
return ring[0][1] = "B"
measureMigration
function measureMigration(nodes, addNode, vnodeCount, sampleKeys):
ringBefore <- buildRing(nodes, vnodeCount)
ringAfter <- buildRing(nodes + [addNode], vnodeCount)
moved <- 0
for each key trong sampleKeys:
nodeBefore <- lookup(ringBefore, key)
nodeAfter <- lookup(ringAfter, key)
if nodeBefore != nodeAfter:
moved <- moved + 1
return moved / sampleKeys.length
// Time: O(K × log(N·V)) với K = sampleKeys.length
Pipeline đầy đủ
-- Khởi tạo cluster 3 node
nodes <- ["NodeA", "NodeB", "NodeC"]
ring <- buildRing(nodes, vnodeCount=150)
-- Lookup key
node <- lookup(ring, "user:12345") -- O(log 450)
-- Đo migration khi thêm NodeD
sampleKeys <- ["key_0", "key_1", ..., "key_9999"]
rate <- measureMigration(nodes, "NodeD", 150, sampleKeys)
-- Kỳ vọng: rate ≈ 0.25 (25%)
🎓 Mở rộng
So sánh định lượng với modulo hashing:
-- Modulo hashing 3 → 4 node
moved <- 0
for each key trong sampleKeys:
if hash32(key) % 3 != hash32(key) % 4:
moved <- moved + 1
-- Kỳ vọng: ~75% key đổi bucket
Consistent hashing ~25% so với modulo ~75% khi 3→4 node. Khi 9→10 node: consistent ~10% so với modulo ~90%. Độ chênh lệch tăng mạnh khi cluster lớn — đây là lý do hệ thống như Cassandra, Redis Cluster không dùng modulo.
Weighted node — node mạnh hơn nhận nhiều vnodes hơn:
-- Node A có 4 CPU → 200 vnode
-- Node B có 2 CPU → 100 vnode
ring <- buildRing([("A", 200), ("B", 100)], ...)
-- Node A nhận ~67% key, Node B nhận ~33%
Xóa node — remove tất cả vnode của node đó khỏi ring (lọc), lookup còn lại vẫn đúng ngay lập tức. Key của node bị xóa tự động chuyển sang node kế tiếp trên ring.
Replication factor R — thay vì lookup 1 node, tìm R node kế tiếp trên ring (bỏ qua vnode cùng node vật lý). Cassandra dùng cơ chế này để đặt replica trên R node vật lý khác nhau.
✨ Điều bạn vừa làm được
- Cài hash ring với virtual node, lookup O(log N) bằng binary search trên sorted array.
- Xử lý đúng wrap-around khi hash(key) vượt qua position lớn nhất trên ring.
- Đo định lượng tỷ lệ key di chuyển: consistent hashing ~K/N so với modulo ~100%.
- Nhận ra tại sao vnodeCount cao hơn giúp phân bố đều hơn — đổi lấy bộ nhớ lưu ring lớn hơn.
- Thấy nền tảng để hiểu Cassandra token ring trong case study tiếp theo.
Bài tiếp theo: Case study — Cassandra ring & etcd Raft
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên