Software Architecture/Distributed Systems
Distributed Systems - 10. 일관적 해싱
일관적 해싱 — 노드 추가/제거 시 최소 이동
2014년 한 시스템이 4개 노드로 운영되다가 트래픽이 늘어 5개로 확장했다. 해시 파티셔닝(05편)을 썼기 때문에, 노드가 4→5로 바뀌자 전체 데이터의 80%가 다른 노드로 이동해야 했다. 클러스터가 몇 시간 동안 비정상적으로 동작했고, 확장은 실패로 끝났다. 노드 하나를 추가했을 뿐인데 데이터 대부분이 움직이는 현상 — 이건 해시 분할의 근본적 한계다. 이 한계를 해결하는 기법이 일관적 해싱(consistent hashing)이다. 1997년 David Karger 등이 발표한 이 기법은 — 노드가 추가·제거될 때 데이터 이동을 최소화한다. 오늘날 Dynamo, Cassandra, Memcached, CDN이 쓰는 핵심 기법이다.
비유로 감 잡기 — 원형 테이블의 자리 배정
원형 테이블에 사람들이 앉아 있다고 상상하자. 새 사람이 들어와 자리가 필요하다. 직사각형 테이블이었다면 — 빈 자리를 만들려고 모든 사람이 한 자리씩 옆으로 이동해야 할 수 있다. 원형 테이블이면 — 한 사람이 두 사람 사이에 끼어들면, 그 두 사람만 살짝 벌리면 된다. 나머지 사람들은 그대로.
일관적 해싱이 이 원형 테이블과 비슷하다. 노드와 데이터(키)를 모두 원형(해시 링) 위의 점으로 대응시킨다. 각 키는 "원형 위에서 시계 방향으로 만나는 첫 번째 노드"가 담당한다. 새 노드가 추가되면 — 원형 위의 한 점에 배치되고, 그 점 다음의 기존 노드가 담당하던 일부 키만 새 노드로 옮겨온다. 전체 데이터의 1/N만 움직이면 된다 (N은 노드 수).
flowchart TD
subgraph Hash["해시 링 (원형 공간)"]
N1[Node A]
N2[Node B]
N3[Node C]
K1[Key 1]
K2[Key 2]
K3[Key 3]
K4[Key 4]
end
원형이라서 어디가 "처음"인지 정하기 위해 — 시계 방향으로 "각 노드 다음의 첫 키"를 그 노드가 담당한다고 정한다. Node A가 담당하는 키는 Node A 위치에서 시계 방향으로 Node B 위치 직전까지의 키들이다.
비유의 한계 — 원형 테이블 비유는 '균등 배치'를 가정하지만, 실제 일관적 해싱은 노드의 해시 위치가 불균등할 수 있다 (한 노드가 원형의 넓은 영역을 차지). 가상 노드(virtual nodes) 기법으로 이를 보정한다. 비유는 '균등 배치 가정'에서 무너진다.
일관적 해싱의 핵심 — 노드 추가/제거 시 이동 최소화
해시 분할(05편)의 hash(key) % N 방식은 — N이 바뀌면 (노드 추가·제거) 거의 모든 키의 담당 노드가 바뀐다. 데이터의 대부분이 이동해야 한다.
// 해시 분할 — 노드 수 변경 시 대규모 이동
class HashModPartitioner {
private int nodeCount;
int nodeForKey(String key) {
return Math.abs(key.hashCode()) % nodeCount;
}
// nodeCount가 4에서 5로 바뀌면:
// key=1: 1%4=1 → 1%5=1 (유지)
// key=2: 2%4=2 → 2%5=2 (유지)
// key=4: 4%4=0 → 4%5=4 (이동)
// key=5: 5%4=1 → 5%5=0 (이동)
// 약 80% 키가 이동
}
일관적 해싱은 이 문제를 노드와 키를 같은 해시 공간에 배치해서 해결한다.
// 일관적 해싱 — 노드 수 변경 시 최소 이동
class ConsistentHashPartitioner {
private final SortedMap<Integer, Node> ring = new TreeMap<>();
private final HashFunction hash;
void addNode(Node node) {
int position = hash.hash(node.id());
ring.put(position, node);
// 기존 노드가 담당하던 일부 키만 이 노드로 이동
}
void removeNode(Node node) {
int position = hash.hash(node.id());
Node nextNode = ring.higherEntry(position).getValue();
ring.remove(position);
// 이 노드가 담당하던 키만 nextNode로 이동
}
Node nodeForKey(String key) {
int position = hash.hash(key);
// 시계 방향으로 첫 번째 노드 찾기
SortedMap<Integer, Node> tail = ring.tailMap(position);
if (tail.isEmpty()) {
return ring.get(ring.firstKey()); // 원형이므로 처음으로
}
return tail.get(tail.firstKey());
}
}
이 구조에서 — 노드 하나가 추가되면 그 노드의 해시 위치 다음 영역을 담당하던 기존 노드의 일부 키만 새 노드로 옮겨온다. 전체 키의 약 1/N만 이동 (N은 노드 수). 5번 노드가 4개에서 5개로 갈 때, 약 20%만 이동.
가상 노드 (Virtual Nodes) — 균등 분산의 핵심
단순한 일관적 해싱의 문제 — 노드의 해시 위치가 불균등하다. 한 노드가 원형의 넓은 영역을 차지하고, 다른 노드는 좁은 영역을 차지할 수 있다. 그러면 부하가 균등하지 않다.
해결책 — 각 물리 노드를 원형 위의 여러 점에 배치하는 것. 이를 가상 노드(virtual node) 또는 vnode라 부른다.
class ConsistentHashWithVNodes {
private final SortedMap<Integer, Node> ring = new TreeMap<>();
private final int vnodesPerNode = 150; // 물리 노드당 150개 가상 노드
void addNode(Node node) {
for (int i = 0; i < vnodesPerNode; i++) {
int position = hash.hash(node.id() + "-" + i);
ring.put(position, node);
}
}
void removeNode(Node node) {
// 이 노드의 모든 가상 노드 제거
ring.entrySet().removeIf(e -> e.getValue().equals(node));
}
Node nodeForKey(String key) {
int position = hash.hash(key);
SortedMap<Integer, Node> tail = ring.tailMap(position);
return tail.isEmpty() ? ring.get(ring.firstKey()) : tail.get(tail.firstKey());
}
}
가상 노드 수는 — 많을수록 균등 분산이 좋아지지만, 라우팅 비용(메모리·검색)이 증가한다. Cassandra는 기본 256개, Riak은 기본 64개. 실제로 100~300 사이가 흔한 설정.
가상 노드의 추가 이점 — 이질적 하드웨어
가상 노드는 또 다른 이점을 준다 — 물리 노드의 용량이 다를 때, 가상 노드 수를 다르게 해서 부하를 용량에 비례하게 배분.
// 용량에 비례한 가상 노드 수
void addNode(Node node) {
int vnodes = (int) (baseVNodes * node.capacityRatio());
// 강력한 노드는 더 많은 가상 노드 → 더 많은 부하
// 약한 노드는 더 적은 가상 노드 → 더 적은 부하
for (int i = 0; i < vnodes; i++) {
ring.put(hash.hash(node.id() + "-" + i), node);
}
}
예: 2배 강력한 노드는 2배 많은 가상 노드 → 2배 많은 키를 담당. 클러스터에 이질적 하드웨어가 섞여 있을 때 유용하다.
설계 사례 — Dynamo 스타일 키-값 저장소
Dynamo(DeCandia et al. 2007)가 정립한 패턴을 간소화해 본다. 일관적 해싱 + 복제 + 쿼럼의 조합.
class DynamoStyleStore {
private final ConsistentHashRing ring;
private final int replicationFactor = 3; // 각 키 3개 노드에 복제
Set<Node> nodesForKey(String key) {
// 시계 방향으로 N개 노드 선택
return ring.nextNNodes(hash(key), replicationFactor);
}
void put(String key, String value) {
Set<Node> nodes = nodesForKey(key);
// 코디네이터 (첫 번째 노드)가 다른 노드에 전파
Node coordinator = nodes.iterator().next();
coordinator.putLocal(key, value);
coordinator.replicateTo(key, value, nodes); // 비동기 전파
}
String get(String key) {
Set<Node> nodes = nodesForKey(key);
// 여러 노드에서 읽어 최신 선택 (vector clock으로 버전 비교)
List<VersionedValue> values = new ArrayList<>();
for (Node n : nodes) {
values.add(n.getLocal(key));
}
return reconcile(values); // 충돌 해결
}
}
이 패턴에서 — 각 키는 원형 위에서 시계 방향으로 연속한 3개 노드에 복제된다. 한 노드가 죽어도 나머지 2개가 서비스. 새 노드가 추가되면, 원형 위의 그 영역을 담당하던 기존 노드 1개가 일부 키를 새 노드에 넘겨주고, 새 노드는 그 키를 자기 다음 노드 2개에 복제한다. 노드 추가가 거의 영향을 안 미치는 이유가 여기에.
일관적 해싱 위반/오용 감지
| 신호 | 의미 |
|---|---|
| 노드 추가 시 데이터의 50% 이상 이동 | 일관적 해싱 미사용 또는 가상 노드 부족 |
| 한 노드의 부하가 다른 노드의 3배 | 가상 노드 수 부족 — 균등 분산 실패 |
| 노드 제거 시 그 노드의 데이터 유실 | 다음 노드로 승계 안 됨 |
| 키 조회가 O(N) (N=노드 수) | 라우팅 자료구조 미흡 — TreeMap 사용 안 함 |
| 가상 노드 수가 물리 노드에 따라 안 변함 | 이질적 하드웨어에서 부하 불균등 |
| 노드 추가 직후 일시적 일관성 위반 | 복제 동기화 전에 읽기 허용 |
일관적 해싱이 쓰이는 시스템
일관적 해싱은 특정 라이브러리가 아니라 — 분산 시스템의 기본 패턴이다. 거의 모든 수평 확장 시스템이 변형해서 쓴다.
- DynamoDB — Dynamo 논문의 직계 후손. 일관적 해싱 + 복제 + 쿼럼.
- Cassandra — Dynamo 모델 + 컬럼 패밀리. 가상 노드(vnode) 256개 기본.
- Memcached — 클라이언트사이드 일관적 해싱 (Ketama 알고리즘).
- CDN (Akamai 등) — 사용자를 가장 가까운 에지 서버로 라우팅.
- Riak — 일관적 해싱 + vector clock + CRDT.
- Redis Cluster — 16384개 슬롯을 해시 슬롯으로 분할. 일관적 해싱의 단순화된 변형.
각각의 구현은 세부에서 다르지만 — "노드 추가/제거 시 최소 이동"이라는 핵심 아이디어는 같다.
다른 패턴과의 관계
일관적 해싱은 다른 분산 패턴과 자주 짝한다.
- 파티셔닝(05편) — 일관적 해싱은 파티셔닝의 한 형태. 노드 추가 시 재밸런싱 비용 최소화.
- 복제(06편) — 각 키를 담당하는 N개 노드를 일관적 해싱으로 선택. Dynamo의 패턴.
- 일관성 모델(03편) — 일관적 해싱은 어디로 보낼까를 다루고, 일관성 모델은 보낸 뒤 어떻게 일치시킬까를 다룸. 직교하는 관심사.
- 합의(04편) — 일관적 해싱 자체는 합의 없이 동작 (각 클라이언트가 독립적으로 계산). 합의는 "링 구성원 변화"를 합의할 때 쓰임.
일관적 해싱의 한계
일관적 해싱이 만능은 아니다. 한계가 뚜렷하다.
- 핫스팟을 완전히 없애지 못함 — 키의 해시 분포가 안 좋으면, 한 노드가 우연히 인기 키를 많이 담당할 수 있다. 가상 노드는 완화하지만 제거하진 못한다.
- 복잡한 쿼리에 부적합 — 범위 쿼리(05편 참조)가 어렵다. 인접 키가 서로 다른 노드에 있을 수 있어, 범위 조회가 모든 노드에 가야 할 수 있다.
- 운영 복잡성 — 링 구성원 관리, 가상 노드 조정, 복제본 동기화가 복잡하다. 단순한 시스템엔 오버엔지니어링.
그래서 — 데이터가 단일 노드에 들어가거나, 범위 쿼리가 주된 패턴이면, 단순한 범위 분할이 더 나을 수 있다. 일관적 해싱은 — 데이터가 크고, 노드 추가/제거가 빈번하고, 키 단위 조회가 주된 패턴일 때 빛을 발한다.
다음으로 — 시간과 순서
일관적 해싱은 "어디로 보낼까"를 다뤘다면, 다음 편들(합의·시간·분산 트랜잭션)은 "보낸 뒤 어떻게 일치시킬까"를 다룬다. 특히 09편(시간·순서)은 — 분산 시스템에서 "어떤 일이 먼저 일어났나"를 어떻게 정할까를 다룬다. 일관적 해싱이 각 클라이언트의 독립적 계산으로 동작할 수 있는 건, "어디로"가 시간과 무관하기 때문. 하지만 "어떤 쓰기가 최신인가"는 시간에 의존한다 — 그래서 일관성 모델의 강한 쪽(linearizability)은 합의·시계·버전 벡터 같은 도구를 필요로 한다.
일관적 해싱의 핵심 통찰 — 데이터를 어디에 둘까라는 문제를 "원형 위의 점"이라는 우아한 추상화로 풀었다는 점이다. 노드와 키를 같은 공간에 배치하면, 추가/제거의 영향이 자연스럽게 최소화된다. 이 단순한 아이디어가 오늘날 분산 데이터 인프라의 근간이다.
참고
- Karger et al. — "Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web" (ACM STOC 1997), 일관적 해싱 원 논문 — 접근 2026-07-20
- DeCandia et al. — "Dynamo: Amazon's Highly Available Key-value Store" (SOSP 2007), 가상 노드·복제·쿼럼 결합 — 접근 2026-07-20
- Lakshman, Malik — "Cassandra: A Decentralized Structured Storage System" (ACM SIGOPS Operating Systems Review, 2010) — 접근 2026-07-20
- Nishtala et al. — "Scaling Memcache at Facebook" (NSDI 2013), Ketama 알고리즘 — 접근 2026-07-20
- Kleppmann —
(O'Reilly, 2017), Ch.6 (파티셔닝 — 일관적 해싱 절)
'Software Architecture > Distributed Systems' 카테고리의 다른 글
| Distributed Systems - 12. 회복탄력성 (0) | 2026.07.20 |
|---|---|
| Distributed Systems - 11. 서비스 디스커버리·게이트웨이 (0) | 2026.07.20 |
| Distributed Systems - 09. 시간·순서·클럭 (0) | 2026.07.20 |
| Distributed Systems - 08. 멱등성·정확히 한 번 (0) | 2026.07.20 |
| Distributed Systems - 07. 분산 트랜잭션 (0) | 2026.07.20 |