포스트

일관된 해싱 — 노드가 들고날 때 데이터를 통째로 옮기지 않는 법

일관된 해싱은 노드 추가·삭제 시 키 이동을 어떻게 1/N로 줄이나. 해시 링·가상 노드·랑데부 해싱과 DynamoDB·Cassandra 샤딩을 애니메이션으로 정리합니다.

일관된 해싱 — 노드가 들고날 때 데이터를 통째로 옮기지 않는 법

hash % N의 저주

키를 여러 서버에 나눠 담는 가장 단순한 방법은 서버 = hash(key) % N 입니다. N개 서버에 고르게 퍼지고 조회도 O(1)이죠. 문제는 N이 바뀌는 순간 터집니다. 서버를 4대에서 5대로 늘리면 나누는 수가 바뀌어, 거의 모든 키의 귀속 서버가 재계산됩니다. 분산 캐시라면 캐시 거의 전체가 한순간에 미스가 되어(캐시 스탬피드) 백엔드가 폭주하고, 분산 DB라면 데이터 대부분이 노드 사이를 이사 다닙니다.

스케일 아웃·장애 복구가 일상인 분산 시스템에서 이건 치명적입니다. 일관된 해싱(consistent hashing) 은 “노드 1대가 늘거나 줄 때, 이동하는 키를 전체의 약 1/N로 최소화한다”는 목표로 이 저주를 풉니다.

해시 링 — 키와 노드를 같은 원 위에 놓는다

아이디어는 우아합니다. 키도 노드도 같은 해시 공간(예: 0 ~ 2³²)에 올린 뒤, 그 공간을 원(ring) 으로 잇습니다. 각 키는 자기 위치에서 시계 방향으로 처음 만나는 노드에 귀속됩니다. 노드를 하나 제거하면, 그 노드가 갖던 키들만 다음 노드로 넘어가고 — 나머지 키들은 전혀 움직이지 않습니다.

A B C B 제거 → B가 갖던 키만 시계방향 다음 노드(A)로. 나머지는 그대로 해시 링 (0 ~ 2³²)

이게 전부입니다. 조회는 “키 해시 ≥ 노드 해시인 첫 노드”를 찾는 것이라, 노드 위치를 정렬해두면 이진 탐색으로 O(log N) 에 끝납니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import bisect, hashlib

class HashRing:
    def __init__(self, nodes, vnodes=150):
        self.ring = {}              # 해시 → 노드
        self.sorted_keys = []
        for n in nodes:
            for v in range(vnodes): # 가상 노드로 균등 분산
                h = self._hash(f"{n}#{v}")
                self.ring[h] = n
                bisect.insort(self.sorted_keys, h)

    def _hash(self, key):
        return int(hashlib.md5(key.encode()).hexdigest(), 16)

    def get(self, key):
        h = self._hash(key)
        i = bisect.bisect(self.sorted_keys, h) % len(self.sorted_keys)
        return self.ring[self.sorted_keys[i]]   # 시계방향 첫 노드

모듈로 vs 일관된 해싱 — 노드가 하나 바뀔 때

차이는 노드가 변할 때 몇 개의 키가 이사하는가입니다. 아래에서 직접 비교해 보세요. 위쪽 hash % N은 N이 4→5로 바뀌자 거의 모든 칸의 색(귀속 노드)이 바뀌고, 아래쪽 일관된 해싱은 일부만 바뀝니다.

노드 4 → 5 추가! hash % N (빨강 = 이사한 키) 거의 전부 재배치 → 캐시 전멸 일관된 해싱 (초록 = 이사한 키) 약 1/N만 이동 → 나머지 캐시 생존

평균적으로 일관된 해싱은 노드 변동 시 K/N개의 키(K=전체 키, N=노드 수)만 이동시킵니다. 4→5로 갈 때 약 20%만 움직이고 80%는 그대로 — 캐시 적중률이 보존됩니다.

가상 노드 — 균등 분산과 부드러운 부하 이전

순진하게 노드를 링에 1점씩만 올리면 두 가지 문제가 생깁니다. (1) 노드들이 링에 불균등하게 떨어져 어떤 노드가 훨씬 큰 구간을 떠맡습니다. (2) 노드가 죽으면 그 부하가 다음 한 노드에 통째로 쏠립니다.

해결책은 각 물리 노드를 여러 개의 가상 노드(virtual node) 로 링에 흩뿌리는 것입니다(노드 A를 A#0, A#1, … A#149처럼). 그러면 부하가 고르게 펴지고, 한 노드가 빠질 때 그 부하가 여러 노드로 분산되어 이전됩니다. 가상 노드 수가 많을수록 표준편차는 $\frac{1}{\sqrt{vnodes}}$로 줄어듭니다.

flowchart TD
    P["물리 노드 A"] --> V1["A#0"]
    P --> V2["A#1"]
    P --> V3["A#…"]
    V1 --> R["해시 링 곳곳에 분산 배치"]
    V2 --> R
    V3 --> R
    R --> B["부하 균등 + 장애 시 여러 노드로 분산 이전"]

랑데부(HRW) 해싱 — 링 없이 같은 목표를

같은 문제를 푸는 또 다른 방법이 랑데부 해싱(Rendezvous / HRW, Highest Random Weight) 입니다. 링을 만들지 않고, 키마다 모든 노드와의 hash(key, node) 점수를 계산해 최댓값인 노드를 고릅니다. 노드가 빠지면 그 노드를 1등으로 뽑았던 키들만 차순위로 옮겨가, 일관된 해싱과 같은 최소 이동성을 얻습니다. 구현이 간결하고 가중치 부여가 자연스러워, 노드 수가 적당할 때 즐겨 쓰입니다(조회는 O(N)).

방식조회 비용균등성특징
hash % NO(1)좋음N 변하면 거의 전부 이동 ✗
일관된 해싱 + vnodeO(log N)vnode로 우수표준, 부하 이전 부드러움
랑데부(HRW)O(N)우수링 불필요, 가중치 용이

어디에 쓰이나 — DynamoDB부터 CDN까지

일관된 해싱은 분산 시스템의 기본 골격입니다. Amazon DynamoDB·Cassandra가 파티션을 노드에 배치하는 방식, memcached 클라이언트(Ketama) 의 키 분배, CDN/로드밸런서가 같은 사용자를 같은 엣지/오리진에 붙이는 sticky 라우팅이 모두 이 계열입니다. 노드가 수시로 들고나는(오토스케일·장애) 환경에서 데이터 이동을 최소화한다는 한 가지 성질이 이 모든 곳에서 결정적이기 때문입니다.

프로덕션 함정

함정증상해법
가상 노드 없음/과소노드 간 부하 불균등(핫노드)vnode 100~200개 이상
핫키 편중인기 키 하나가 한 노드를 폭주핫키만 별도 복제/분할, 요청 코얼레싱
노드 추가 시 스탬피드이전 구간 키가 일시에 미스점진적 데이터 워밍, 백엔드 요청 병합
해시 비균등특정 구간 쏠림MD5/Murmur 등 균등 해시
복제 시 같은 노드 중복복제본이 물리적으로 한 노드에링에서 서로 다른 물리 노드로 N개 선택

면접/리뷰 단골 질문

  • Q. hash % N의 문제와 일관된 해싱의 해결? → N이 바뀌면 거의 전 키 재배치(캐시 전멸). 일관된 해싱은 노드 변동 시 약 1/N만 이동.
  • Q. 가상 노드는 왜 필요한가? → 링 상 균등 분산과, 노드 장애 시 부하를 여러 노드로 분산 이전하기 위해. 표준편차를 1/√vnode로 감소.
  • Q. 조회 복잡도? → 노드 해시를 정렬해두고 이진 탐색 → O(log(노드·vnode 수)). 랑데부는 O(N).
  • Q. 랑데부 해싱과의 차이? → 둘 다 최소 이동. 랑데부는 링 없이 키-노드 점수 최대값 선택, 가중치/구현이 간결하나 조회 O(N).
  • Q. 복제는 어떻게? → 시계방향으로 만나는 서로 다른 물리 노드 N개에 사본을 둔다(vnode가 같은 물리 노드면 건너뜀).

정리

  • hash % N은 노드 수가 변하면 거의 전부 재배치 — 분산 캐시·DB에서 치명적.
  • 일관된 해싱은 키·노드를 같은 해시 링에 올려 “시계방향 첫 노드”에 귀속시켜, 노드 변동 시 약 1/N만 이동.
  • 가상 노드로 부하를 균등화하고 장애 시 부하를 여러 노드로 분산 이전한다.
  • 랑데부 해싱은 링 없이 같은 최소 이동성을 얻는 대안. DynamoDB·Cassandra·Ketama·CDN의 공통 골격.

같은 해시 함수가 자료구조 안에서는 충돌 회피에, 분산 시스템에서는 데이터 배치에 쓰입니다. 노드들이 데이터뿐 아니라 상태에 대한 합의까지 맞춰야 할 때, 다음 글 합의 알고리즘으로 이어집니다.

</content>

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

시리즈 · 알고리즘 A-Z (20편)

  1. 복잡도 분석 — Big-O, 점근 표기, 그리고 분할상환이라는 거짓말 같은 진실
  2. 배열과 연결 리스트 — 같은 O(n)인데 왜 한쪽이 수십 배 느린가
  3. 스택·큐·힙 — 접근 규칙 하나가 만드는 강력함
  4. 해시 테이블 — 평균 O(1)의 비밀과 그 평균이 깨지는 순간
  5. 비교 정렬 — 퀵·머지·힙, 그리고 Ω(n log n)이라는 넘을 수 없는 벽
  6. 선형 시간 정렬 — 비교를 버리면 Ω(n log n) 벽이 사라진다
  7. 이진 탐색 — 반씩 버리는 마법, 그리고 "답을 이분하라"
  8. 재귀와 분할정복 — 콜스택에 쌓이는 신뢰, 마스터 정리로 푸는 비용
  9. 이진 탐색 트리와 균형 — 한쪽으로 쏠리는 트리를 회전으로 바로 세우기
  10. 그래프 순회 — BFS의 물결, DFS의 깊이, 그리고 위상정렬
  11. 최단 경로 — 다익스트라의 확신, A*의 직감, 벨만-포드의 인내
  12. 최소 신장 트리와 Union-Find — 가장 싸게 잇기, 그리고 거의 O(1)의 마법
  13. 동적계획법 — 같은 계산을 두 번 하지 않는다는 단 하나의 원칙
  14. 그리디 — 눈앞의 최선만 집었는데 전체가 최적일 때
  15. 문자열 매칭 — 어긋났을 때 얼마나 똑똑하게 다시 시작하는가
  16. 확률적 자료구조 — 정확함을 메모리와 맞바꾸는 기술 (Bloom·HyperLogLog·Count-Min)
  17. 일관된 해싱 — 노드가 들고날 때 데이터를 통째로 옮기지 않는 법 지금 보는 글
  18. 합의 알고리즘 — 죽는 서버들 사이에서 하나의 진실을 정하는 법 (Raft·Paxos)
  19. 레이트 리미팅과 스트리밍 알고리즘 — 폭주를 막고, 끝없는 흐름에서 표본을 뽑다
  20. 의미로 검색하다 — 벡터 검색(ANN·HNSW)과 PageRank, 그리고 시리즈의 끝