Thuật toán Ứng dụng — DP, String, Big Data & hơn nữa/Mini-challenge — consistent hashing ring có virtual node
36/66
Bài 36 / 66~30 phútThuật toán phân tánMiễn phí lượt xem

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).
  • position tính bằng hash(nodeName + "#" + vnodeIndex) với vnodeIndex từ 0 đến vnodeCount - 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 lookup trước và sau khi thêm node.

🔍 Input-Process-Output

Consistent hashing ringModulo hashing
Inputhash(key) ánh xạ lên [0, 2^32)hash(key) % N
ProcessBinary search trên sorted ringPhép chia modulo
OutputNode vật lý của vnode kế tiếpIndex node trực tiếp
Khi thêm nodeChỉ key trong đoạn của node mới di chuyểnGần như toàn bộ key đổi node

📦 Concept mapping

Bước thực hiệnLearning outcome từ module
Implement buildRing — sắp xếp vnodeImplement consistent hashing ring (bài 01)
Implement lookup — binary search trên ringImplement lookup key → node O(log N)
Implement measureMigration — so sánh trước/sauTính tỷ lệ K/N key di chuyển vs modulo 100%
So sánh với modulo hashingCompare consistent hashing và modulo theo migration rate

📋 Test cases

ScenarionodesaddNodevnodeCountExpected migration
Thêm node vào cluster 3 nodeA, B, CD100~25% (lý thuyết 1/4)
Thêm node vào cluster 9 nodeA..IJ100~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 ý

💡 Bước 1 — buildRing: sorted array of vnodes (đọc khi chưa biết bắt đầu)

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ử.

💡 Bước 2 — lookup: binary search + wrap-around (đọc sau khi buildRing đúng)

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.

💡 Bước 3 — measureMigration: so sánh hai snapshot (đọc sau khi lookup đúng)

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

✅ Lời giải — xem sau khi đã tự làm

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

Đặt 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