Introduction: Why Hash Tables Break at Scale
In traditional distributed systems, when you need to distribute data across multiple nodes, the simplest approach is modulo-based hashing: hash(key) % N. This works fine until you add or remove a node. When N changes, nearly all keys are remapped, causing a complete cache invalidation or data reshuffling. Consistent Hashing solves this fundamental problem.
Introduced by Karger et al. at MIT in 1997, consistent hashing has become a cornerstone technique used in Amazon Dynamo, Apache Cassandra, Riak, Akamitan, Discords real-time infrastructure, and most modern distributed caching layers.
The Core Concept: Hash Ring
Consistent hashing maps both nodes and keys onto a conceptually circular hash space (a "ring"). The algorithm works in four steps:
- Hash each nodes identifier and place it on the ring
- Hash each key and place it on the ring
- Walk clockwise from the keys position
- The first node encountered "owns" that key
This means that when a node is added, only keys between the new node and its predecessor are remapped. When a node is removed, only its own keys are reassigned to the next node clockwise. The fraction of keys that move is at most k/n where k is the number of keys and n is the number of nodes.
Virtual Nodes: Solving the Non-Uniform Distribution Problem
The basic ring approach has a critical flaw: with few nodes, keys distribute unevenly according to random chance. The solution is virtual nodes (vnodes) -- each physical node is assigned multiple positions on the ring.
| Virtual Nodes per Physical Node | Typical Imbalance Factor |
|---|---|
| 1 | Highly variable (can exceed 2x) |
| 100 | ~1.5x in worst case |
| 256 | ~1.2x, commonly used in practice |
| 1024 | ~1.05x, Cassandra default equivalent |
More virtual nodes means better load balance at the cost of more memory and computation for ring maintenance.
Implementation: Building a Production-Ready Consistent Hash Ring
Here is a complete, production-grade implementation in Python 3.11+ that includes virtual nodes, weighted distribution, and support for multi-get operations:
import hashlib
import bisect
from typing import List, Dict, Optional
class ConsistentHashRing:
"""
Production-grade consistent hash ring with virtual node support.
"""
def __init__(self, nodes: Optional[Dict[str, int]] = None,
replicas: int = 128, hash_fn=None):
self.replicas = replicas
self.hash_fn = hash_fn or self._default_hash
self.ring: Dict[int, str] = {}
self.sorted_keys: List[int] = []
self.nodes: Dict[str, int] = {}
if nodes:
for node, weight in nodes.items():
self.add_node(node, weight)
@staticmethod
def _default_hash(key: str) -> int:
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def add_node(self, node: str, weight: int = 1) -> None:
self.nodes[node] = weight
num_vnodes = self.replicas * weight
for i in range(num_vnodes):
vnode_key = f"{node}:VN{i}"
position = self.hash_fn(vnode_key)
self.ring[position] = node
bisect.insort(self.sorted_keys, position)
def remove_node(self, node: str) -> None:
if node not in self.nodes:
return
weight = self.nodes.pop(node)
num_vnodes = self.replicas * weight
prefixes_to_remove = set()
for i in range(num_vnodes):
vnode_key = f"{node}:VN{i}"
pos = self.hash_fn(vnode_key)
prefixes_to_remove.add(pos)
self.sorted_keys = [k for k in self.sorted_keys if k not in prefixes_to_remove]
for pos in prefixes_to_remove:
del self.ring[pos]
def get_node(self, key: str) -> Optional[str]:
if not self.ring:
return None
hash_val = self._default_hash(key)
idx = bisect.bisect_right(self.sorted_keys, hash_val)
if idx == len(self.sorted_keys):
idx = 0
return self.ring[self.sorted_keys[idx]]
def get_nodes(self, key: str, count: int) -> List[str]:
if not self.ring or count <= 0:
return []
hash_val = self._default_hash(key)
idx = bisect.bisect_right(self.sorted_keys, hash_val)
result = []
seen = set()
while len(result) < count>= len(self.sorted_keys):
idx = 0
node = self.ring[self.sorted_keys[idx]]
if node not in seen:
seen.add(node)
result.append(node)
idx += 1
return result
The Jump Consistent Hash: Googles Elegant O(1) Solution
In 2014, John Lamping and Eric Veach from Google published "A Fast, Minimal Memory, Consistent Hash Algorithm" (arXiv:1406.2294). Jump Hash is now the default in projects like Envoy, Grafana Tempo, and Minio.
The key insight: instead of storing a ring (O(n) space), use a pseudo-random sequence to "jump" through node IDs. It requires zero memory beyond the node count and runs in O(ln n) time -- essentially constant for any realistic cluster size.
def jump_consistent_hash(key: int, num_buckets: int) -> int:
"""
Jump consistent hash algorithm (Lamping & Veach, 2014).
Properties: O(ln n) time, O(1) memory, minimal redistribution.
"""
b = -1
j = 0
while j < num xss=removed xss=removed xss=removed>> 33) + 1))
return b
Consistent Hashing vs Jump Hash: When to Use Which
| Criterion | Ring-based CH | Jump Hash |
|---|---|---|
| Supports weighted nodes | Y (different vnode counts) | N (equal distribution only) |
| Replication factor (N replicas) | Y via get_nodes() | N (single bucket per key) |
| Memory consumption | O(vnodes x nodes) | O(1) |
| Rebalancing granularity | Only between neighbors | Keys spread across all nodes |
| Minimum key movement | ~k/n keys moved | ~k/(n+1) keys moved |
| Supports heterogeneous hardware | Y (weight-based) | N |
| Implementation complexity | Medium (ring data structure) | Minimal (~15 lines) |
Real-World Architecture: How Discord Scales Elixir with Consistent Hashing
Discord handles millions of concurrent WebSocket connections on Elixir/Erlang clusters. Their architecture uses consistent hashing for process registry -- mapping a Guild ID to the specific node holding that Guild process.
- Ring updated via cluster-wide broadcast (Erlang distribution protocol) with ~200ms convergence
- When a node dies, only its Guilds migrate; users reconnect transparently
- They use a VRF-style hash (Verifiable Random Function) for security -- preventing crafted keys that land on specific nodes
- Virtual node count: 200 per physical node, providing
Advanced Topics and Edge Cases
1. Bounded Load Consistent Hashing
Standard CH does not guarantee worst-case load. With bounded load CH (Facebook 2019), each node has a capacity cap. When a node is full, keys that would map to it instead spill to the next node. This guarantees: load(node) ≤ max(avg_load x (1+epsilon), avg_load + max_item_weight).
2. Rendezvous Hashing (Highest Random Weight)
An alternative approach: for each key, hash it with every node identifier and pick the node with the highest composite score. While harder to distribute (O(n) per key), it provides perfect balance and supports weights trivially. Used in Grafana Tempo and Apple FoundationDB.
3. Consistent Hashing with Bounded Loads + Locality
In geo-distributed systems (CDN, multi-region databases), you want keys to prefer nearby nodes. Solutions like CRUSH (Ceph) and Maglev (Google) combine consistent hashing with a locality-aware lookup table and fast symmetric permutation.
Conclusion: Choosing the Right Tool
Consistent hashing is not a single algorithm but a design pattern. Here is a decision framework:
- Caching / CDN (uniform hardware): Jump Hash -- simplest, zero state, best redistribution
- Database sharding (weighted nodes, heterogeneous hardware): Ring-based CH with virtual nodes
- Key-value store replication: Ring-based CH with N= replication factor
- Geo-distributed systems: Locality-aware variants (Maglev, CRUSH)
- Minimal-latency requirement: Rendezvous HRW (compute is faster than ring traversal at small scale)
The beauty of consistent hashing lies in its mathematical guarantee: when the cluster topology changes, only a provably small fraction of state moves. That guarantee, proven across three decades of distributed systems engineering, is why it remains essential infrastructure today.
References
- Karger et al., "Consistent Hashing and Random Trees" -- STOC 1997
- Lamping and Veach, "A Fast, Minimal Memory, Consistent Hash Algorithm" -- arXiv:1406.2294
- Discord Engineering, "Elixir at Scale" -- blog.discord.com
- Facebook Engineering, "Bounded Load Consistent Hashing" -- 2019
- Google, "Maglev: A Fast and Reliable Software Network Load Balancer" -- NSDI 2016

发表评论 取消回复