Consistent hashing maps both nodes and keys onto a circular hash ring (0..2^32-1). Each key is assigned to the first node clockwise from its hash. When nodes join/leave, only adjacent key ranges move, so rebalancing affects ~1/N of keys rather than all.Use virtual nodes: place multiple hashes per physical node (vnodes). That reduces imbalance by averaging many small ranges per node.Algorithm idea:- Hash(node_id + replica_index) -> vnode position on ring- Maintain sorted map of vnode_position -> physical_node- To map key: h = hash(key); find first vnode_position >= h (wrap to smallest); return its physical_node.Pseudocode (Python-like):python
import bisect
from hashlib import md5
class ConsistentHashRing:
def __init__(self, vnode_count=100):
self.vnode_count = vnode_count
self.positions = [] # sorted list of vnode hashes (ints)
self.position_to_node = {} # map hash -> node_id
def _hash(self, s):
return int(md5(s.encode()).hexdigest(), 16)
def add_node(self, node_id):
for i in range(self.vnode_count):
pos = self._hash(f"{node_id}:{i}") % (2**32)
if pos in self.position_to_node:
# rare collision; skip or linear probe
continue
bisect.insort(self.positions, pos)
self.position_to_node[pos] = node_id
def remove_node(self, node_id):
to_remove = [p for p,n in self.position_to_node.items() if n==node_id]
for p in to_remove:
idx = bisect.bisect_left(self.positions, p)
if idx < len(self.positions) and self.positions[idx]==p:
self.positions.pop(idx)
del self.position_to_node[p]
def get_node(self, key):
if not self.positions:
return None
h = self._hash(key) % (2**32)
idx = bisect.bisect_right(self.positions, h)
if idx == len(self.positions):
idx = 0
return self.position_to_node[self.positions[idx]]
Expected movement on membership change:- If there are N physical nodes and no vnodes, adding/removing one node moves ~1/N of keys (on average) because it takes one contiguous arc.- With V virtual nodes per physical node (total M = N*V vnodes), the expected fraction moved when a physical node joins/leaves ≈ V/M = 1/N, but variance is much lower — each vnode owns many small arcs so distribution approaches uniform; maximum imbalance drops roughly as O(1/√V).- Therefore expected key movement per membership change is ~1/N of keys; using vnodes reduces statistical deviation and hot spots.Operational notes:- Choose vnode_count so N*V >> 1 (e.g., V=100-1000) for smooth balance.- Persist ring metadata, use consistent hashing for replication by taking next R distinct nodes clockwise.- Handle concurrency: perform join/leave via coordinated updates (gossip/consensus) and migrate keys for affected key ranges only.