Lançado · em melhoria
Guia de Hashing consistente · 1/6
Por enquanto, este capítulo está disponível apenas em inglês.
The first idea most people have for spreading keys over several servers is hash(key) % N: hash the key and use the remainder modulo the server count as the server index. It is fast and evenly balanced, but it has one serious flaw: when N changes, almost every key lands on a different server. Consistent hashing, introduced by Karger et al. in 1997 for distributed web caching, is designed so that adding or removing a server moves only the keys that really have to move.
Picture a cache with 10,000 keys spread over 4 servers. Grow it to 5 servers and only keys with h % 4 == h % 5 stay put. With well-spread hashes that is about 1/(N+1) of them, so roughly N/(N+1) of the keys, 80% here, move. Going from 10 to 11 servers moves about 91%.
import hashlib
def h(key: str) -> int:
return int.from_bytes(hashlib.md5(key.encode()).digest()[:8], "big")
keys = [h(f"user:{i}") for i in range(100_000)]
for n in (4, 10):
moved = sum(k % n != k % (n + 1) for k in keys)
print(n, "->", n + 1, f"{moved / len(keys):.0%}")
# 4 -> 5 80%
# 10 -> 11 91%For a cache, that means the hit rate collapses to nearly zero and every request falls through to the database at once. For a data store it means reshuffling the whole dataset. Losing a server to a failure triggers exactly the same storm.
When N servers become N+1, the newcomer needs a fair share, so 1/(N+1) of the keys must move to it. Consistent hashing aims to move exactly that much, and only onto the new server; existing servers never trade keys among themselves. When a server leaves, only its own keys are redistributed.
The original paper lists the properties a good distribution scheme should have:
The best-known form is the hash ring. Treat the whole hash space, say 0 to 2^64 - 1, as a circle whose end wraps around to the start.
When a server joins, only the keys between its position and the previous server (counter-clockwise) change owner. The rest of the ring is untouched.
0 ---- A(10) ---- B(45) ---- C(80) ---- 99, then back to 0
key 7 -> first node clockwise is A
key 33 -> B
key 52 -> C
key 91 -> past the end, wrap around to AWith a single point per server, the gaps between points are left to chance. With 10 servers and one point each, one server may own several times the average while another sits nearly idle. The fix is to hash each server under many names, A#0, A#1, and so on, giving it many points on the ring. These are virtual nodes (vnodes). The more points, the closer each server's total share gets to the average, and when a server leaves, its load spreads over many servers instead of one neighbour. Giving a bigger machine more vnodes expresses a capacity weight.
node = "cache-a"
names = [f"{node}#{i}" for i in range(4)]
print(names) # ['cache-a#0', 'cache-a#1', 'cache-a#2', 'cache-a#3']
# each name is hashed separately: 4 points on the ring for one serverThe ring is not the only design.
hash(server, key) and pick the highest. No ring is needed and the code is tiny, but each lookup touches every server.CRC16(key) mod 16384) explicitly assigned to nodes. The goal is similar, the design is different.| Term | Meaning |
|---|---|
| Node | A physical server or shard that owns keys |
| Ring | The hash space viewed as a circle |
| Token | A position a node occupies on the ring |
| Virtual node | One of several points a node has on the ring |
| Preference list | Ordered list of nodes holding a key's replicas (Dynamo term) |
| Rebalancing | Moving keys to their new owners after a membership change |
Reach for it when membership changes at runtime and moving a key is expensive: a cache miss, a data copy, a lost session. Distributed cache clients, Dynamo-style key-value stores and load balancers that need request affinity are the classic cases. If the node count is fixed, or moving keys costs almost nothing, plain % N is fine.
hash % N moves about N/(N+1) of the keys whenever N changes.1/(N+1).
0 comentários
Fazer login · Faça login para deixar um comentário.
Seja o primeiro a comentar.