출시·고도화 중
Algorithm
일관된 해시는 노드가 추가되거나 빠질 때 꼭 필요한 키만 옮기도록 키를 서버에 나누는 방법으로, 분산 캐시·저장소·로드 밸런서에 쓰입니다.
일관된 해시(consistent hashing)는 키를 여러 서버에 나눠 배치하는 방법으로, 1997년 Karger 등이 웹 캐시 분산을 위해 제안했습니다. 해시 값 공간을 원형 링으로 보고 서버와 키를 같은 링 위에 놓은 뒤, 키에서 시계 방향으로 처음 만나는 서버가 그 키를 맡습니다. 서버마다 링 위에 여러 점(가상 노드)을 두어 부하를 고르게 맞춥니다.
hash(key) % N 방식은 서버 수가 바뀌면 거의 모든 키의 자리가 바뀌어 캐시 적중률이 무너지고 대량의 데이터 이동이 생깁니다. 일관된 해시는 서버 하나가 추가될 때 평균적으로 전체 키의 1/(N+1)만, 그것도 새 서버로만 옮기므로 운영 중에 노드를 늘리고 줄이기 쉽습니다. Memcached 클라이언트의 ketama, Amazon Dynamo 논문과 Cassandra, Nginx·HAProxy의 consistent 해시 옵션이 이 원리를 씁니다.
먼저 mod-N 방식에서 키가 얼마나 옮겨지는지 직접 계산해 보고, 정렬된 배열과 이진 탐색으로 해시 링을 구현해 보세요. 이어서 가상 노드 수에 따라 부하 편차가 어떻게 줄어드는지 측정하고 복제본 선택, 랑데부 해시, 점프 해시와 비교해 보면 시스템 설계 면접에서도 차이를 분명히 설명할 수 있습니다.
노드를 추가하거나 뺄 때 기대값으로 약 1/(N+1)의 키만 옮기며, 기존 노드끼리는 키를 주고받지 않습니다.
해시 공간을 원으로 보고 키에서 시계 방향으로 처음 만나는 노드를 주인으로 정합니다. 조회는 정렬된 위치에 대한 이진 탐색입니다.
노드마다 링 위에 여러 점을 두어 부하 편차를 줄이고, 점의 개수로 서버 용량의 차이(가중치)를 표현합니다.
시계 방향으로 서로 다른 노드 R개를 골라 복제본을 두며, 랑데부 해시와 점프 해시도 같은 목표를 다른 방식으로 이룹니다.
MD5 해시의 앞 8바이트를 링 위치로 쓰고, 노드마다 가상 노드 100개를 bisect.insort로 정렬된 목록에 넣습니다. get은 bisect_left로 키 위치 이상인 첫 점을 찾고, 끝을 넘으면 처음으로 감쌉니다. 노드 D를 추가하면 키의 약 4분의 1만 옮겨지고, 다시 빼면 모든 키가 원래 노드로 돌아오는 것을 확인할 수 있습니다.
consistent_hashing.py
import bisect
import hashlib
def h(key: str) -> int:
return int.from_bytes(hashlib.md5(key.encode()).digest()[:8], "big")
class HashRing:
def __init__(self, nodes=(), vnodes=100):
self.vnodes = vnodes
self.points = [] # sorted hash positions on the ring
self.owner = {} # position -> physical node
for node in nodes:
self.add(node)
def add(self, node):
for i in range(self.vnodes):
p = h(f"{node}#{i}")
bisect.insort(self.points, p)
self.owner[p] = node
def remove(self, node):
for i in range(self.vnodes):
p = h(f"{node}#{i}")
self.points.pop(bisect.bisect_left(self.points, p))
del self.owner[p]
def get(self, key):
if not self.points:
raise LookupError("ring is empty")
i = bisect.bisect_left(self.points, h(key)) % len(self.points)
return self.owner[self.points[i]]
ring = HashRing(["A", "B", "C"])
keys = [f"user:{n}" for n in range(10_000)]
before = {k: ring.get(k) for k in keys}
ring.add("D")
moved = sum(before[k] != ring.get(k) for k in keys)
print(f"moved {moved / len(keys):.1%} of keys") # about 25%
ring.remove("D")
print(all(ring.get(k) == before[k] for k in keys)) # True
python consistent_hashing.py설치부터 일관된 해시 의 핵심 개념까지, 여섯 장으로 차근차근 따라 합니다.
일관된 해시 에 관해 묻고, 경험을 나누고, 의견을 주고받는 곳입니다.
아직 토론이 없습니다. 첫 이야기를 시작해 보세요.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.