본문 바로가기
Backend2026년 8월 14일6분 읽기

일관성 해싱(Consistent Hashing) — 노드가 늘어도 캐시가 무너지지 않는 이유

YS
김영삼
조회 7
일관성 해싱(Consistent Hashing) — 노드가 늘어도 캐시가 무너지지 않는 이유

일관성 해싱(Consistent Hashing)은 서버(노드)가 늘거나 줄어도 키의 재배치를 최소화하도록 설계된 분산 배치 기법입니다. 키와 노드를 같은 해시 공간의 원(링) 위에 올려두고, 각 키를 시계 방향으로 만나는 첫 번째 노드에 맡깁니다. 노드 하나가 빠져도 그 노드가 담당하던 키만 옆으로 넘어갈 뿐, 전체가 흔들리지 않습니다.

이걸 처음 제대로 이해하게 된 건 캐시 서버를 한 대 늘렸다가 사고를 친 뒤였습니다. 트래픽이 몰려서 memcached 노드를 3대에서 4대로 늘렸을 뿐인데, 캐시 적중률이 순간 20%대로 곤두박질쳤죠. 원인은 단순한 hash(key) % N 방식이었습니다.

왜 나머지 연산(modulo)이 문제인가

가장 흔한 분산 방식은 키 해시를 노드 수로 나눈 나머지를 쓰는 겁니다. 노드가 고정이면 완벽하게 균등하고 빠릅니다. 문제는 N이 바뀌는 순간입니다. N이 3에서 4로 바뀌면 거의 모든 키의 나머지가 달라져, 사실상 캐시 전체가 무효화됩니다.

# 노드 3대일 때
hash("user:42") % 3 = 1   → node1
# 노드 4대로 늘리면
hash("user:42") % 4 = 2   → node2  (자리가 통째로 바뀜)

캐시라면 대량 미스가 뒤따르는 캐시 스탬피드로 이어지고, 샤딩된 저장소라면 데이터 대이동이 벌어집니다. 노드 한 대 추가에 전체의 3/4가 이사를 가는 셈이니, 실무에선 감당하기 어렵습니다.

링 위에 올린다는 발상

일관성 해싱은 관점을 바꿉니다. 0부터 2^32-1까지의 해시 값을 원형으로 이어붙인 해시 링을 상상하세요. 노드도 키도 같은 해시 함수로 이 링 위의 한 점에 배치합니다. 키가 담길 노드는 그 키의 위치에서 시계 방향으로 가장 먼저 만나는 노드입니다.

이 구조의 핵심은, 노드 하나가 죽거나 새로 들어와도 영향받는 키가 링에서 그 노드 주변 구간으로 국한된다는 점입니다. 평균적으로 K/N개의 키(K는 전체 키, N은 노드 수)만 재배치됩니다. modulo 방식이 거의 전부를 흔드는 것과 대비됩니다.

항목hash % N일관성 해싱
노드 추가 시 이동량거의 전부약 K/N
분포 균등성매우 좋음가상 노드 필요
조회 비용O(1)O(log N)

가상 노드로 쏠림을 없앤다

순진하게 노드 하나당 링 위에 점 하나만 찍으면 분포가 들쭉날쭉해집니다. 운 나쁘게 두 노드가 링에서 가까이 붙으면, 그 사이 좁은 구간을 맡는 노드는 놀고 넓은 구간을 맡는 노드는 과부하가 걸리죠. 그래서 실무에선 가상 노드(virtual node)를 씁니다. 물리 노드 하나를 node1#0, node1#1… 처럼 수백 개의 가상 점으로 링에 흩뿌리는 겁니다.

import hashlib, bisect
class HashRing:
    def __init__(self, nodes, vnodes=150):
        self.ring = {}          # 해시값 -> 노드
        self.keys = []          # 정렬된 해시값
        for n in nodes:
            for i in range(vnodes):
                h = self._hash(f"{n}#{i}")
                self.ring[h] = n
                bisect.insort(self.keys, h)
    def _hash(self, s):
        return int(hashlib.md5(s.encode()).hexdigest(), 16)
    def get(self, key):
        h = self._hash(key)
        idx = bisect.bisect(self.keys, h) % len(self.keys)
        return self.ring[self.keys[idx]]

가상 노드 수를 150~200개쯤 주면 물리 노드 간 부하 편차가 통계적으로 크게 줄어듭니다. Redis Cluster, Cassandra, DynamoDB, 그리고 상당수의 로드밸런서가 이 아이디어의 변형을 씁니다. 참고로 구글이 2014년에 낸 점프 일관성 해싱(Jump Consistent Hash)은 메모리 없이 짧은 함수만으로 같은 성질을 내는 변형인데, 노드에 임의 이름을 붙이거나 중간 노드를 빼는 시나리오엔 안 맞아서 케이스를 봐야 합니다.

실전에서 데는 지점

  • 가상 노드를 너무 적게 잡으면 균등성이 무너집니다. 반대로 수천 개씩 잡으면 메모리와 조회 비용이 커집니다. 150 근처에서 시작해 측정하세요.
  • 해시 함수의 분포가 나쁘면 링 자체가 쏠립니다. MD5/무르무르해시(MurmurHash)처럼 분포 좋은 함수를 쓰되, 보안 목적이 아니면 굳이 SHA-256까지 갈 필요는 없습니다.
  • 가중치(weight)가 다른 노드가 섞이면, 스펙 좋은 노드에 가상 노드를 더 많이 배정해 용량에 비례시키는 게 좋습니다.

자주 묻는 질문

일관성 해싱은 캐시에만 쓰나요?

아니요. 분산 캐시가 대표 사례지만, 샤딩된 데이터베이스의 파티션 배치, 로드밸런서의 세션 고정(sticky session), P2P 시스템의 키 라우팅 등 "키를 노드에 안정적으로 매핑"하는 모든 곳에 쓰입니다.

가상 노드는 몇 개가 적당한가요?

물리 노드당 100~200개가 흔한 시작점입니다. 노드 수가 적을수록(예: 3~5대) 편차가 크게 나타나니 가상 노드를 넉넉히 주는 편이 안전합니다. 실제 부하 분포를 측정해 조정하세요.

노드가 죽으면 그 데이터는 사라지나요?

일관성 해싱 자체는 배치 규칙일 뿐 복제를 보장하지 않습니다. 그래서 보통 링에서 다음 N개 노드에 복제본을 두는 방식과 함께 씁니다. Cassandra의 복제 계수(replication factor)가 이 조합의 예입니다.

Jump Consistent Hash가 항상 더 낫나요?

메모리를 거의 안 쓰고 분포도 좋지만, 노드가 순차적 정수 인덱스라는 전제가 있습니다. 노드에 임의 문자열 이름을 붙이거나, 중간 노드만 콕 집어 제거하는 유연함이 필요하면 전통적인 링 방식이 더 편합니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록