들어가며: 왜 Consistent Hashing이 필요한가?
문제 상황: 해시 기반 샤딩의 함정
캐시 서버 4대를 운영 중이라고 가정하자. 단순한 해싱으로 키를 분산시킨다면:
def get_server(key, num_servers):
return hash(key) % num_servers
# 4대 서버에 분산
get_server("user:1234", 4) # 서버 2
get_server("user:5678", 4) # 서버 0
get_server("user:9012", 4) # 서버 3
평상시엔 잘 동작한다. 그런데 트래픽이 늘어 서버를 5대로 증설하면?
get_server("user:1234", 5) # 서버 4 (기존 2번 → 4번)
get_server("user:5678", 5) # 서버 3 (기존 0번 → 3번)
get_server("user:9012", 5) # 서버 2 (기존 3번 → 2번)
거의 모든 키의 위치가 바뀐다. 캐시 미스가 폭발하고, 데이터베이스에 트래픽이 쏟아지며, 서비스 장애로 이어진다. 이를 리해싱 폭풍(Rehashing Storm) 이라 부른다.
Consistent Hashing의 약속
1997년 MIT의 David Karger 등이 발표한 Consistent Hashing은 이 문제를 우아하게 해결한다:
노드가 N개에서 N+1개로 바뀌어도, 이동해야 할 키는 평균 K/N개뿐이다. (K는 전체 키 개수)
즉, 서버 4대 → 5대로 늘어나면 전체 키의 약 1/5만 이동한다. 나머지 4/5는 그대로다. 이 원리가 Amazon DynamoDB, Apache Cassandra, Riak, Memcached, Akamai CDN 등 수많은 분산 시스템의 기초가 되었다.
graph LR
A[Key] --> B{해싱 방식}
B -->|modulo| C[80% 키 이동<br/>❌ 재앙]
B -->|Consistent Hash| D[20% 키 이동<br/>✅ 정상]
1. 기본 아이디어: 해시 링(Hash Ring)
원형 공간에 노드와 키 배치
Consistent Hashing의 핵심 아이디어는 의외로 간단하다:
- 해시 함수의 출력 공간을 원형(0 ~ 2^32-1)으로 생각한다.
- 각 노드(서버)를 해싱해서 링 위의 한 점에 놓는다.
- 각 키(데이터)도 해싱해서 링 위의 한 점에 놓는다.
- 키는 시계 방향으로 가장 가까운 노드에 저장된다.
0 / 2^32
|
NodeA |
---+----+----+---
| | |
| | |
NodeD NodeB
| | |
| | |
---+----+----+---
NodeC |
|
파이썬 기본 구현
import hashlib
from bisect import bisect_right, insort
class ConsistentHashBasic:
def __init__(self):
self.ring = {} # hash → node
self.sorted_keys = []
def _hash(self, key: str) -> int:
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def add_node(self, node: str):
h = self._hash(node)
self.ring[h] = node
insort(self.sorted_keys, h)
def remove_node(self, node: str):
h = self._hash(node)
del self.ring[h]
self.sorted_keys.remove(h)
def get_node(self, key: str) -> str:
if not self.ring:
return None
h = self._hash(key)
# 시계 방향으로 가장 가까운 노드 찾기
idx = bisect_right(self.sorted_keys, h)
if idx == len(self.sorted_keys):
idx = 0 # 링을 한 바퀴 돌아 처음으로
return self.ring[self.sorted_keys[idx]]
# 사용 예시
ch = ConsistentHashBasic()
for node in ["server-A", "server-B", "server-C", "server-D"]:
ch.add_node(node)
print(ch.get_node("user:1234")) # server-C
print(ch.get_node("user:5678")) # server-A
노드 추가/제거 시 영향
노드 E를 추가하면:
- E의 해시값이 기존 노드들 사이 어딘가에 들어간다.
- E와 직전 노드 사이의 키들만 E로 이동한다.
- 나머지 키들은 그대로다.
평균적으로 K/N개(1/5)의 키만 이동한다. 이게 마법이다.
2. 기본 방식의 문제점: 불균형
기본 Consistent Hashing은 완벽하지 않다. 가장 큰 문제는 키 분포의 불균형이다.
문제 1: 노드 소수일 때 편중
4개 노드만으로 링을 구성하면, 해시값이 균등하게 분포한다고 가정해도 각 노드가 맡는 "구간"의 크기가 천차만별이다:
NodeA: 0 ~ 5 (5%)
NodeB: 5 ~ 40 (35%) ← 많이 할당
NodeC: 40 ~ 50 (10%)
NodeD: 50 ~ 100 (50%) ← 절반을 차지
결과: NodeD가 전체 트래픽의 절반을 받고, NodeA는 거의 놀고 있다.
문제 2: 노드 삭제 시 편중 악화
NodeB가 다운되면, B의 키는 전부 NodeC로 몰린다. 핫스팟(Hotspot)이 발생한다.
문제 3: 이질적인 서버 용량 반영 어려움
어떤 서버는 CPU 16코어 메모리 64GB이고, 어떤 서버는 4코어 16GB일 수 있다. 기본 방식으로는 서버 용량 비율을 반영할 수 없다.
3. 해결책: 가상 노드 (Virtual Nodes, vnodes)
아이디어
각 물리 노드를 여러 개의 가상 노드로 표현해서 링에 뿌린다. 예를 들어 노드당 150개 가상 노드를 만들면:
- 물리 노드 10개 × 150 = 1,500개의 가상 노드가 링에 분산된다.
- 대수의 법칙에 의해 각 물리 노드가 담당하는 구간이 균등해진다.
물리 노드 4개 (각 150 vnode)
링 위 분포:
A-1, B-7, C-3, A-99, D-42, B-15, C-88, D-1, A-15, ...
(거의 균등하게 섞임)
가상 노드 구현
class ConsistentHashWithVNodes:
def __init__(self, vnodes_per_node: int = 150):
self.vnodes_per_node = vnodes_per_node
self.ring = {}
self.sorted_keys = []
def _hash(self, key: str) -> int:
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def add_node(self, node: str, weight: int = 1):
# weight가 높을수록 더 많은 vnode 생성
count = self.vnodes_per_node * weight
for i in range(count):
vnode_key = f"{node}#{i}"
h = self._hash(vnode_key)
self.ring[h] = node
insort(self.sorted_keys, h)
def remove_node(self, node: str, weight: int = 1):
count = self.vnodes_per_node * weight
for i in range(count):
vnode_key = f"{node}#{i}"
h = self._hash(vnode_key)
del self.ring[h]
self.sorted_keys.remove(h)
def get_node(self, key: str) -> str:
if not self.ring:
return None
h = self._hash(key)
idx = bisect_right(self.sorted_keys, h)
if idx == len(self.sorted_keys):
idx = 0
return self.ring[self.sorted_keys[idx]]
# 사용 예시: 용량이 다른 서버들
ch = ConsistentHashWithVNodes(vnodes_per_node=150)
ch.add_node("server-A", weight=2) # 성능 2배 → 300 vnode
ch.add_node("server-B", weight=1) # 표준 → 150 vnode
ch.add_node("server-C", weight=1)
ch.add_node("server-D", weight=1)
# 이제 A는 전체 키의 ~40%, 나머지는 각 ~20%를 받는다
가상 노드 개수 선택 가이드
| 가상 노드 수 | 표준편차 | 메모리 오버헤드 |
|---|---|---|
| 10개 | ±30% | 낮음 |
| 100개 | ±10% | 중간 |
| 160개 | ±6% | 적정 (Ketama 기본값) |
| 1000개 | ±2% | 높음 |
실전에서 100~200개가 스위트스팟이다. Memcached의 libketama는 160개를 사용하며, Cassandra는 기본 256개(num_tokens)를 사용한다.
4. 벤치마크: 실제 분산은 얼마나 균등한가?
import random
import statistics
def benchmark(vnodes_per_node: int, num_nodes: int, num_keys: int):
ch = ConsistentHashWithVNodes(vnodes_per_node)
for i in range(num_nodes):
ch.add_node(f"node-{i}")
counts = {f"node-{i}": 0 for i in range(num_nodes)}
for k in range(num_keys):
key = f"key-{random.random()}"
node = ch.get_node(key)
counts[node] += 1
values = list(counts.values())
avg = statistics.mean(values)
stddev = statistics.stdev(values)
max_val = max(values)
min_val = min(values)
print(f"vnodes={vnodes_per_node}: "
f"min={min_val}, max={max_val}, "
f"stddev={stddev:.0f} ({stddev/avg*100:.1f}% of avg)")
# 1,000,000 키를 10개 노드에 분산
benchmark(vnodes_per_node=1, num_nodes=10, num_keys=1_000_000)
benchmark(vnodes_per_node=10, num_nodes=10, num_keys=1_000_000)
benchmark(vnodes_per_node=100, num_nodes=10, num_keys=1_000_000)
benchmark(vnodes_per_node=500, num_nodes=10, num_keys=1_000_000)
예상 출력:
vnodes=1: min=30000, max=250000, stddev=70000 (70.0% of avg)
vnodes=10: min=60000, max=180000, stddev=35000 (35.0% of avg)
vnodes=100: min=92000, max=110000, stddev=5800 (5.8% of avg)
vnodes=500: min=97000, max=103000, stddev=2000 (2.0% of avg)
100개 가상 노드로도 ±6% 수준의 균형을 달성할 수 있다.
5. Jump Consistent Hash: 가상 노드 없이도 균등하게
2014년 Google의 John Lamping과 Eric Veach가 발표한 Jump Consistent Hash는 가상 노드 없이 O(1) 공간과 거의 완벽한 균등 분포를 달성한다.
놀라운 특성
- 메모리 사용량: O(1) (노드 목록 불필요!)
- 시간 복잡도: O(log N) (N은 노드 수)
- 분포 균등도: 거의 완벽
- 단점: 노드 제거가 불편 (끝에 있는 노드만 제거 가능)
Jump Hash 알고리즘
def jump_consistent_hash(key: int, num_buckets: int) -> int:
"""
Google Jump Consistent Hash
key: 해시된 정수
num_buckets: 버킷(노드) 개수
"""
b = -1
j = 0
while j < num_buckets:
b = j
key = (key * 2862933555777941757 + 1) & 0xFFFFFFFFFFFFFFFF
j = int((b + 1) * (1 << 31) / ((key >> 33) + 1))
return b
# 사용 예시
import hashlib
def hash_key(key: str) -> int:
return int(hashlib.md5(key.encode()).hexdigest()[:16], 16)
num_nodes = 10
key = hash_key("user:1234")
bucket = jump_consistent_hash(key, num_nodes)
print(f"user:1234 → bucket {bucket}")
어떻게 이렇게 작동하는가?
Jump Hash의 핵심 원리는 의사난수 기반 확률적 스킵이다. 각 반복마다 "다음에 점프할 위치"를 확률적으로 결정한다. 수학적으로 증명된 속성:
- 노드 수가 N → N+1로 바뀌면, 정확히 K/(N+1)개의 키만 새 노드로 이동한다.
- 분포는 완벽하게 균등하다 (가상 노드가 전혀 필요 없다).
한계: 노드 제거 문제
Jump Hash는 오직 마지막 노드(가장 높은 인덱스)만 제거 가능하다. 중간 노드를 제거하면 뒷 노드들의 인덱스가 바뀌어 모든 키 매핑이 깨진다.
이를 해결하려면 Lookup Table 을 추가로 관리하거나, 다음에 설명할 Rendezvous Hashing을 사용한다.
6. Rendezvous Hashing (HRW: Highest Random Weight)
1997년 David Thaler와 Chinya Ravishankar가 제안한 Rendezvous Hashing은 또 다른 우아한 해결책이다.
아이디어
각 키에 대해 모든 노드와의 해시값을 계산하고, 그중 가장 높은(또는 낮은) 값을 가진 노드를 선택한다.
def rendezvous_hash(key: str, nodes: list) -> str:
"""
각 키를 가장 높은 가중치를 가진 노드에 할당
"""
if not nodes:
return None
max_weight = -1
selected_node = None
for node in nodes:
combined = f"{key}:{node}"
h = int(hashlib.md5(combined.encode()).hexdigest(), 16)
if h > max_weight:
max_weight = h
selected_node = node
return selected_node
# 사용 예시
nodes = ["server-A", "server-B", "server-C", "server-D"]
print(rendezvous_hash("user:1234", nodes)) # 예: "server-C"
장단점
장점:
- 구현이 매우 간단 (해시 함수만 있으면 됨)
- 완벽하게 균등한 분포
- 노드 추가/제거 시 K/N개 키만 이동
- Jump Hash와 달리 임의의 노드 제거 가능
- Replica 선택에 유용: Top-K 노드를 얻으면 K개의 복제본 위치를 즉시 얻을 수 있다.
단점:
- O(N) 시간 복잡도: 노드가 많으면 느리다.
- 1000개 이상의 노드에서는 부적절.
복제(Replica) 선택
Rendezvous Hashing이 빛나는 순간은 Top-K 노드 선택이다:
def rendezvous_hash_topk(key: str, nodes: list, k: int) -> list:
weights = []
for node in nodes:
h = int(hashlib.md5(f"{key}:{node}".encode()).hexdigest(), 16)
weights.append((h, node))
weights.sort(reverse=True)
return [node for _, node in weights[:k]]
# 3개 복제본을 위한 3개 노드 선택
replicas = rendezvous_hash_topk("user:1234", nodes, k=3)
print(replicas) # ["server-C", "server-A", "server-D"]
이는 Cassandra의 replica 배치 전략에서 사용되는 아이디어와 비슷하다.
7. Anchor Hashing: 최신 알고리즘 (2018)
Anchor Hashing은 2018년 Gal Mendelson 등이 발표한 최신 알고리즘으로, Jump Hash의 속도와 Rendezvous의 유연한 노드 제거를 모두 제공한다.
주요 특성:
- O(log N) 시간 복잡도
- O(N) 공간 (각 노드당 상수 메모리)
- 임의 노드 제거 가능
- 완벽한 균등 분포
실제 구현은 복잡하니, 이 글에서는 존재만 언급하고 넘어간다. Maglev Hash(Google)도 비슷한 범주에 속한다.
8. 실전 시스템에서 어떻게 쓰이나?
DynamoDB: 기본 Consistent Hashing + vnode
Amazon DynamoDB는 Amazon Dynamo 논문(2007)의 설계를 따른다:
- 128비트 해시 공간을 원형으로 구성.
- 각 물리 노드를 **여러 가상 노드(토큰)**로 분산.
- 각 키는 시계 방향 가장 가까운 N개 노드에 복제(Preference List).
- 노드 추가/제거 시 인접 노드에서만 데이터 이동.
Cassandra: 토큰 링
Cassandra도 유사한 구조지만 용어가 조금 다르다:
- 토큰(Token) = 가상 노드 (Murmur3 64비트 해시)
- 기본
num_tokens: 256→ 각 물리 노드가 256개 토큰 소유. - 데이터 복제는 Replication Strategy (SimpleStrategy, NetworkTopologyStrategy)로 설정.
# cassandra.yaml
num_tokens: 256
partitioner: org.apache.cassandra.dht.Murmur3Partitioner
Memcached (libketama): 가상 노드 표준
libketama는 Last.fm이 만든 Memcached 클라이언트 라이브러리로, Consistent Hashing의 사실상 표준이 되었다:
- 노드당 160개 가상 노드.
- MD5 해시 (128비트 → 32비트로 절단).
- 서버 정보 문자열 → MD5 → 4개의 32비트 정수 → 4개의 가상 노드 생성.
Nginx: upstream consistent hash
Nginx도 upstream 로드 밸런싱에 Consistent Hashing을 지원한다:
upstream backend {
hash $request_uri consistent;
server backend1.example.com;
server backend2.example.com;
server backend3.example.com;
}
consistent 키워드가 Ketama 기반 Consistent Hashing을 활성화한다.
Envoy / HAProxy / Istio
Envoy의 Ring Hash LB Policy와 Maglev LB Policy는 각각 Ketama 스타일과 Google Maglev 스타일의 Consistent Hashing을 구현한다.
# Envoy config
load_balancing_policy:
policies:
- typed_extension_config:
name: envoy.load_balancing_policies.ring_hash
typed_config:
"@type": type.googleapis.com/envoy.extensions.load_balancing_policies.ring_hash.v3.RingHash
minimum_ring_size: 1024
9. 흔한 실수와 함정
함정 1: 해시 함수 선택
나쁜 선택:
def bad_hash(key):
return hash(key) # 파이썬 내장 hash()는 재시작 시 바뀜!
파이썬 내장 hash()는 보안상 이유로 프로세스 시작마다 다른 시드를 쓴다. 분산 시스템에서 재시작 시 매핑이 완전히 바뀌면 재앙이다.
좋은 선택:
- MD5, SHA-1: 느리지만 분포 좋음.
- MurmurHash3: 빠르고 분포 좋음 (Cassandra 사용).
- xxHash: 매우 빠름 (최근 선호됨).
함정 2: 가상 노드 수 부족
노드 4개 × 가상 노드 1개 = 4개 → 분포가 매우 불균형. 항상 노드당 최소 100개 이상의 가상 노드를 사용하자.
함정 3: 노드 ID의 문자열 포맷
가상 노드를 만들 때 포맷이 일관되지 않으면 재현 불가:
# 나쁨
f"{node}-{i}" # node1-0, node1-1, ...
f"{node}_{i}" # node1_0, node1_1, ... (다른 시스템에서 사용)
# 좋음: 팀/조직 표준 문서화
f"{node}#{i}" # 문서에 명시
함정 4: 핫키(Hot Key)
Consistent Hashing은 키 분포가 균등할 때만 잘 작동한다. 특정 키(예: user:celebrity_account)가 엄청난 트래픽을 받으면 한 노드에 부하가 집중된다.
해결책:
- Key Salting:
user:celebrity_account:{random_suffix}로 여러 샤드에 분산. - Local Cache: 앱 레벨 캐싱으로 분산 캐시 호출 자체를 줄임.
- Read Replica: 읽기 전용 복제본 추가.
함정 5: 노드 클러스터의 급격한 변동
쿠버네티스 같은 환경에서 노드가 빈번하게 생성/삭제되면, 매번 데이터 이동이 발생한다. Rebalancing Throttling이 필요하다:
- Warm-up 기간: 새 노드는 점진적으로 트래픽을 받는다.
- Cooldown: 노드 제거 후 데이터가 완전히 이동할 때까지 대기.
10. 한 걸음 더: Weighted Consistent Hashing
Maglev (Google)
Google의 Maglev는 다음과 같은 요구사항으로 설계됐다:
- 완벽한 균등성 (비슷한 가중치 서버들 간)
- 최소 disruption (노드 변경 시)
- O(1) lookup (런타임에 조회가 빨라야 함)
Maglev는 각 서버에 대해 **선호 순열(preference permutation)**을 계산하고, 이를 통해 크기 M의 lookup table을 만든다. 조회는 lookup_table[hash(key) % M]로 O(1)이다.
# 개념 수준 의사코드
def build_maglev_table(servers, table_size=65537):
# table_size는 소수(prime) 선택
permutations = {
s: generate_permutation(s, table_size) for s in servers
}
table = [None] * table_size
next_idx = {s: 0 for s in servers}
filled = 0
while filled < table_size:
for s in servers:
while table[permutations[s][next_idx[s]]] is not None:
next_idx[s] += 1
table[permutations[s][next_idx[s]]] = s
next_idx[s] += 1
filled += 1
if filled == table_size:
break
return table
Maglev는 Envoy의 maglev 로드 밸런서 정책으로 사용할 수 있으며, Google Cloud Load Balancer 내부에서도 사용된다.
가중치(Weight) 부여
이질적인 서버가 있을 때(예: 16코어와 4코어) 가중치를 반영하려면:
- 가상 노드 방식:
vnodes = base * weight - Maglev 방식: permutation을
weight배만큼 사용. - Rendezvous 가중치: 해시값에 log(weight)를 곱해 bias.
11. 성능 비교: 어떤 알고리즘을 선택할까?
| 알고리즘 | 시간 복잡도 | 공간 복잡도 | 분포 균등도 | 노드 제거 | 주 사용처 |
|---|---|---|---|---|---|
| Modulo Hash | O(1) | O(1) | 완벽 | ❌ 전체 재분배 | 단순 샤딩 |
| Consistent + vnode | O(log V) | O(V) | 양호 (V↑일수록) | ✅ 임의 | Cassandra, DynamoDB |
| Jump Hash | O(log N) | O(1) | 완벽 | ❌ 마지막만 | Google 내부 |
| Rendezvous Hash | O(N) | O(N) | 완벽 | ✅ 임의 | 소규모 클러스터 |
| Maglev | O(1) 조회 | O(M) | 거의 완벽 | ✅ 임의 | Google Cloud LB |
| Anchor Hashing | O(log N) | O(N) | 완벽 | ✅ 임의 | 최신 시스템 |
선택 가이드
- 노드 수 < 100, 잦은 변동: Rendezvous Hashing (간단, 유연)
- 대규모 캐시/DB 클러스터: Consistent Hashing + vnode (검증됨)
- 정적 노드 집합, 극한 성능: Jump Hash (O(log N))
- L4/L7 로드밸런서: Maglev (O(1) 조회)
- 이질 용량 서버: vnode 가중치 또는 Maglev weighted
12. 실전 팁: 운영하면서 배운 것들
Tip 1: 가상 노드 수는 절대 바꾸지 마라
운영 중에 num_tokens를 150 → 200으로 바꾸면 거의 모든 데이터가 재분배된다. 클러스터 초기 설계 단계에서 신중히 선택하자.
Tip 2: 로그로 분포 모니터링
실제 키 분포가 얼마나 균등한지 주기적으로 측정하자:
# Prometheus 메트릭
consistent_hash_node_keys{node="server-A"} 12050
consistent_hash_node_keys{node="server-B"} 10800
consistent_hash_node_keys{node="server-C"} 11200
consistent_hash_node_keys{node="server-D"} 12500
표준편차가 갑자기 커지면 핫키 의심.
Tip 3: Graceful Shutdown
노드를 제거할 땐 갑자기 죽이지 말고:
- 해당 노드를 "draining" 상태로 표시 (링에서 제거).
- 기존 연결 처리 완료 대기.
- 데이터 이동 확인.
- 프로세스 종료.
Tip 4: 해시 함수 속도 vs 품질
초당 수십만 건의 조회가 있다면, MD5는 느릴 수 있다. 벤치마크:
| 해시 함수 | 속도 (MB/s) | 분포 품질 |
|---|---|---|
| xxHash3 | ~30,000 | 양호 |
| MurmurHash3 | ~6,000 | 양호 |
| CityHash | ~10,000 | 양호 |
| MD5 | ~500 | 우수 |
| SHA-256 | ~400 | 우수 |
캐시 샤딩 같은 용도엔 xxHash나 Murmur가 적합하다.
퀴즈로 복습하기
Q1. 서버 4대에서 5대로 증설할 때, 일반적인 모듈로 해싱(hash % N)은 몇 퍼센트의 키가 이동하는가?
A. 약 **80%**의 키가 이동한다. 기존 hash % 4와 hash % 5의 결과는 거의 무관하기 때문이다. 반면 Consistent Hashing은 약 20%(K/N = K/5)만 이동한다.
Q2. 기본 Consistent Hashing에서 가상 노드가 왜 필요한가?
A. 노드가 적을 때 링 위 노드 분포가 불균등해져서 일부 노드에 키가 편중된다. 각 물리 노드를 100~200개의 가상 노드로 흩뿌리면 대수의 법칙에 의해 거의 균등한 분포를 얻을 수 있다. 또한 서버 용량에 따라 가상 노드 수를 조절해 가중치를 부여할 수도 있다.
Q3. Jump Consistent Hash의 가장 큰 장점과 단점은?
A. 장점: O(1) 공간과 완벽한 균등 분포. 가상 노드 없이도 잘 작동하며 매우 빠르다. 단점: 오직 가장 끝(높은 인덱스) 노드만 제거 가능. 중간 노드를 제거하려면 전체 매핑이 깨진다. 임의 노드 제거가 필요하면 Rendezvous Hashing이나 Anchor Hashing을 써야 한다.
Q4. Rendezvous Hashing이 N개 복제본 선택에 유용한 이유는?
A. Rendezvous Hashing은 각 키에 대해 모든 노드의 가중치(해시값)를 계산한다. 이를 정렬해서 상위 K개 노드를 선택하면 자연스럽게 K개 복제본의 배치가 결정된다. 이는 Consistent Hashing 링에서 "시계 방향으로 다음 K개 노드"를 찾는 것과 유사한 효과지만, 더 균등하게 분포된다.
Q5. Memcached의 libketama는 노드당 몇 개의 가상 노드를 기본으로 사용하며, 그 선택 이유는?
A. Ketama는 노드당 160개의 가상 노드를 사용한다. 이는 균형성(±5% 표준편차) 과 메모리 오버헤드 사이의 타협점이다. 실제로는 MD5 해시의 결과를 4개의 4바이트 정수로 쪼개 각 "replica"당 4개 가상 노드를 만들므로, 40 replicas × 4 = 160이다.
마치며: Consistent Hashing의 교훈
Consistent Hashing은 1997년 MIT에서 CDN의 웹 캐시 문제를 풀기 위해 고안되었다. Akamai를 탄생시켰고, 이후 20년 넘게 분산 시스템의 핵심 빌딩 블록이 되었다.
핵심 원리 5가지
- 원형 해시 공간: 노드와 키를 같은 공간에 배치해 이웃 관계를 정의.
- 시계 방향 할당: 키는 시계 방향 가장 가까운 노드로.
- 가상 노드: 대수의 법칙으로 균등 분포 달성.
- 최소 disruption: 노드 추가/제거 시 K/N개 키만 이동.
- 가중치 유연성: vnode 수로 용량 차이 반영.
현대의 진화
- Jump Hash: O(1) 공간으로 극도의 효율.
- Rendezvous Hashing: 간단함의 극치.
- Maglev: O(1) 조회 + weighted LB.
- Anchor Hashing: 최신 개선판.
기억할 것
"분산 시스템에서 데이터를 어디에 둘지 결정하는 문제" 의 답은 거의 항상 Consistent Hashing의 어떤 변종이다.
당신이 지금 사용하는 Redis Cluster, DynamoDB, Cassandra, Memcached, CDN — 모두 이 30년 된 아이디어 위에서 돌아간다. 기본을 알면 디버깅과 설계의 깊이가 달라진다.
다음번에 "왜 이 키가 저 서버로 가지?"라는 질문이 생기면, 해시 링을 머릿속에 그려보자. 답이 거기에 있다.
참고 자료
- Karger et al., "Consistent Hashing and Random Trees" (1997) - 원 논문
- Dynamo: Amazon's Highly Available Key-value Store (2007)
- Jump Consistent Hash (Google, 2014)
- Maglev: A Fast and Reliable Software Network Load Balancer (Google, 2016)
- Anchor Hashing (2018)
- libketama Source
- Cassandra Data Distribution
- Envoy Ring Hash Load Balancer
현재 단락 (1/380)
캐시 서버 4대를 운영 중이라고 가정하자. 단순한 해싱으로 키를 분산시킨다면: