Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions 05-안정해시설계/정리노트/00_인덱스.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# 안정 해시 설계 — STEP별 정리 노트

> 『가상 면접 사례로 배우는 대규모 시스템 설계 기초』 5장 학습 노트
> 설계를 시작하기 전에 알아야 할 개념을 STEP 순서대로 정리한다.

---

## 이 노트의 목적

안정 해시(consistent hashing)는 **"요청·데이터를 여러 서버에 고르게 나눈다"** 는
한 줄짜리 목표를 위한 기술이다. 하지만 이 목표를 **서버가 수시로 늘고 줄어드는(수평 확장)**
환경에서도 지키려는 순간, 단순한 해시로는 무너진다.

각 STEP은 *기능 추가*가 아니라 **"앞 단계가 만든 문제를 푸는 다음 결정"** 이다.

> 📌 이 장은 6장(키-값 저장소)의 데이터 분산에서도 그대로 쓰인다.
> 6장 노트 [STEP2 — 데이터 분산](../../06-키값저장소설계/정리노트/02_STEP2_데이터분산_안정해시.md) 과 함께 보면 좋다.

---

## 왜 "hash % N" 으로 끝나지 않는가

N개의 서버에 부하를 나누는 가장 단순한 방법은 나머지 연산이다.

```
serverIndex = hash(key) % N (N = 서버 개수)
```

서버 수가 **고정**이고 데이터가 균등하면 이걸로 충분하다.
문제는 이 구조가 **서버가 늘거나 줄면 곧바로 무너진다**는 점이다.

| 벽 | 무엇이 막히나 | 결과 |
| --- | --- | --- |
| **재배치 폭발** | 서버 1대만 죽어도 `% N`의 N이 바뀌어 **거의 모든 키**의 서버 인덱스가 달라짐 | 대규모 캐시 미스 → 시스템 마비 |
| **파티션 불균등** | 서버를 링에 그냥 올리면 담당 해시 공간 크기가 제각각 | 특정 서버 과부하 |
| **키 쏠림(hotspot)** | 키가 특정 서버에 몰림 | 부하 불균형·확장 무의미 |

> 그래서 **왜 단순 해시가 안 되는가(STEP1) → 해시 링으로 어떻게 푸는가(STEP2) → 균등 분포를 어떻게 보장하는가(STEP3, 가상 노드)** 라는 결정이 이어진다.

---

## 설계 로드맵

```mermaid
flowchart TD
R[목표: 데이터를 서버에 고르게 + 확장에 강하게] --> S1[STEP1 rehash 문제<br/>단순 hash %% N의 한계]
S1 --> P1[서버가 늘고 줄어도<br/>재배치를 최소화하려면?]
P1 --> S2[STEP2 안정 해시 원리<br/>해시 링·시계방향 조회]
S2 --> P2[근데 파티션·키가<br/>고르게 안 나뉜다면?]
P2 --> S3[STEP3 가상 노드<br/>균등 분포·재배치 범위]
```

---

## STEP 목록

| STEP | 주제 | 핵심 키워드 | 푸는 문제 |
|:---:|------|-----------|----------|
| [STEP 1](01_STEP1_rehash문제_단순해시의한계.md) | 재배치(rehash) 문제 | `hash % N`, 경계 재계산, 캐시 미스 | 단순 해시는 왜 확장에 실패하나 |
| [STEP 2](02_STEP2_안정해시_원리_해시링.md) | 안정 해시 원리 | 해시 공간·해시 링, 시계방향 조회, k/n | 서버가 변해도 이동을 어떻게 최소화하나 |
| [STEP 3](03_STEP3_가상노드_균등분포.md) | 가상 노드 · 균등 분포 | 파티션/키 불균등, virtual node, 표준편차, 재배치 범위 | 데이터를 어떻게 고르게 나누나 |
| [흐름 통합](04_흐름통합_데이터분산부터_벡터시계.md) | 분산 저장 → 벡터 시계 | 복제 N, 정족수 W·R, 벡터 시계 | 5장 파티션이 6장 데이터 흐름으로 어떻게 확장되나 |

### 각 STEP에서 깊게 다루는 것

| STEP | 세부 내용 |
| --- | --- |
| STEP 1 | `hash % N` 동작, 서버 4→3 변동 시 **표 5-1/5-2 재계산 실측**, 왜 캐시 미스 폭풍이 나는지, "안정 해시" 정의(k/n) |
| STEP 2 | SHA-1 해시 공간(0 ~ 2¹⁶⁰-1) → 해시 링, 서버·키를 **modulo 없이** 링에 매핑, 시계방향 최초 서버 조회, 서버 **추가/삭제 시 영향 범위** |
| STEP 3 | 기본 구현의 두 문제(파티션·키 불균등), **가상 노드(replica)**, 가상 노드 수 ↔ 표준편차 트레이드오프, 재배치할 키의 범위 결정, 실사용처 |

---

## 학습 우선순위

1. **핵심**: STEP 2(해시 링 원리)와 STEP 3(가상 노드). 이 장의 본체이자 면접 단골.
2. **먼저 잡을 것**: STEP 1의 `hash % N` 재배치 문제 — "왜 안정 해시가 필요한가"의 근거.
3. **말로 설명 가능해야**: 서버 추가/삭제 시 **k/n 개만 이동**한다는 것, 가상 노드가 푸는 **두 문제**.
4. 레퍼런스 시스템(모두 안정 해시 채택):
- **아마존 다이나모(DynamoDB)**, **아파치 카산드라** — 데이터 파티셔닝
- **디스코드(Discord)** — 대규모 동시 접속 채팅
- **아카마이(Akamai) CDN**, **매그레브(Maglev)** — 부하 분산

---

## 빠른 복습 — 3문장 요약

> 단순 `hash % N`은 서버가 하나만 변해도 **거의 모든 키를 재배치**해 확장이 불가능하다(STEP1).
> 서버와 키를 **같은 해시 링**에 올리고 시계방향 첫 서버에 저장하면, 서버 변동 시 **인접 구간(k/n)만** 이동한다(STEP2).
> 남은 불균등(파티션·키 쏠림)은 서버 하나를 링의 **여러 지점(가상 노드)** 에 올려 해소한다(STEP3).
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# STEP 1. 재배치(rehash) 문제 — 단순 해시는 왜 확장에 실패하나

> 안정 해시가 **왜 필요한지**를 만드는 출발점.
> `hash(key) % N` 은 서버 수가 고정일 땐 완벽하지만, 서버가 하나만 변해도 **거의 모든 키가 이동**한다.

---

## 0. 먼저: 우리가 풀려는 것

수평 확장(horizontal scaling)의 핵심은 **요청·데이터를 여러 서버에 고르게 나누는 것**이다.
그래서 필요한 건 딱 하나 — "이 키는 어느 서버가 담당하는가?"를 정하는 규칙(**키 → 서버 매핑**).

가장 흔한 규칙이 나머지 연산이다.

```
serverIndex = hash(key) % N (N = 서버 개수)
```

- 키를 해시해서 큰 정수로 만들고, 서버 수 N으로 나눈 **나머지**를 서버 번호로 쓴다.
- 서버가 4대면 모든 키는 0~3 중 하나에 결정적으로 매핑된다.

> 핵심: 이 방식은 **N이 절대 안 변할 때만** 안전하다.

---

## 1. 단순 해시가 잘 도는 경우 (N=4)

키 8개를 4대 서버에 `hash % 4` 로 나눈 예시(원문 표 5-1).

| 키 | 해시 | 해시 % 4 (서버 인덱스) |
| ---- | -------- | :-------------------: |
| key0 | 18358617 | 1 |
| key1 | 26143584 | 0 |
| key2 | 18131146 | 2 |
| key3 | 35863496 | 0 |
| key4 | 34085809 | 1 |
| key5 | 27581703 | 3 |
| key6 | 38164978 | 2 |
| key7 | 22530351 | 3 |

```
서버0: key1, key3 서버1: key0, key4
서버2: key2, key6 서버3: key5, key7
```

> 서버 풀 크기가 **고정**이고 데이터 분포가 균등하면 이걸로 충분히 잘 동작한다.

---

## 2. ⚠️ 서버 하나가 죽으면 — 재배치 폭발

서버 1번이 장애로 빠져 **N이 4 → 3** 으로 바뀌면, 해시 값은 그대로여도 **나누는 수가 바뀌어** 서버 인덱스가 재계산된다(원문 표 5-2, `hash % 3`).

| 키 | 해시 | %4 (이전) | %3 (이후) | 이동? |
| ---- | -------- | :-------: | :-------: | :---: |
| key0 | 18358617 | 1 | **0** | ✅ |
| key1 | 26143584 | 0 | 0 | — |
| key2 | 18131146 | 2 | **1** | ✅ |
| key3 | 35863496 | 0 | **2** | ✅ |
| key4 | 34085809 | 1 | 1 | — |
| key5 | 27581703 | 3 | **0** | ✅ |
| key6 | 38164978 | 2 | **1** | ✅ |
| key7 | 22530351 | 3 | **0** | ✅ |

**8개 중 6개**의 위치가 바뀌었다. 죽은 서버(1번)에 있던 키뿐 아니라 **멀쩡한 서버의 키까지** 대부분 재배치된다.

```mermaid
flowchart LR
A["서버 1대 장애<br/>(N: 4 → 3)"] --> B["나누는 수 변경<br/>hash %% 4 → hash %% 3"]
B --> C["거의 모든 키의<br/>서버 인덱스 재계산"]
C --> D["대부분 클라이언트가<br/>데이터 없는 엉뚱한 서버 접속"]
D --> E["대규모 캐시 미스 폭풍<br/>→ 시스템 마비"]
```

> 핵심: `% N` 은 **N에 전역적으로 의존**하기 때문에, N이 1만 변해도 매핑 전체가 흔들린다.

---

## 3. 그래서 필요한 성질 — "안정" 해시

원하는 것은 **서버가 변해도 최소한의 키만 움직이는** 매핑이다. 위키피디아의 정의:

> 안정 해시는 해시 테이블 크기가 조정될 때 평균적으로 **오직 k/n 개의 키만 재배치**하는 해시 기술이다.
> (k = 키의 개수, n = 슬롯(서버)의 개수)

| | 단순 해시 (`% N`) | 안정 해시 |
| --- | --- | --- |
| 서버 변동 시 이동량 | **거의 전부** (≈ k) | 평균 **k/n** |
| N에 대한 의존 | 전역 의존 | 국소적(인접 구간만) |
| 확장성 | ❌ 사실상 불가 | ✅ 무중단 확장 |

> 즉, 전통적 해시 테이블은 슬롯 수가 바뀌면 대부분 키를 옮기지만, 안정 해시는 **평균 k/n 개만** 옮긴다.
> "어떻게 그게 가능한가"가 STEP 2다.

---

## ✅ STEP 1 체크리스트

- [ ] `serverIndex = hash(key) % N` 이 무엇을 하는지 한 줄로 설명할 수 있다
- [ ] 단순 해시가 **언제** 잘 동작하는지(고정 N·균등 분포) 안다
- [ ] 서버 4→3 변동 시 **왜 대부분 키가 이동**하는지 표로 설명할 수 있다
- [ ] 재배치가 왜 "대규모 캐시 미스"로 이어지는지 안다
- [ ] 안정 해시의 정의(**평균 k/n 개만 이동**)를 말할 수 있다

---

## 💬 예상 면접 질문

**Q1. `hash(key) % N` 방식의 문제는 무엇인가?**
> 서버 수 N이 바뀌면(추가/장애) 나누는 수가 달라져 **거의 모든 키의 서버 인덱스가 재계산**된다. 대부분의 클라이언트가 데이터 없는 서버로 접속해 대규모 캐시 미스가 발생하고, 사실상 확장이 불가능하다.

**Q2. 왜 죽지 않은 서버의 키까지 이동하나?**
> `% N`은 **N에 전역적으로 의존**한다. N이 4→3으로 바뀌면 나머지 값 자체가 달라지므로, 장애 서버와 무관한 키도 새 서버 인덱스로 재계산된다. 예시(표 5-1→5-2)에선 8개 중 6개가 이동한다.

**Q3. "안정 해시"의 정의를 말해달라.**
> 해시 테이블(서버) 크기가 조정될 때 **평균적으로 k/n 개의 키만 재배치**하는 해시 기술이다(k=키 수, n=서버 수). 전통적 해시가 슬롯 변경 시 거의 전부를 옮기는 것과 대비된다.

➡️ 다음: [STEP 2 — 안정 해시 원리(해시 링)](02_STEP2_안정해시_원리_해시링.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# STEP 2. 안정 해시 원리 — 해시 링으로 어떻게 이동을 최소화하나

> STEP1의 답: 서버 수 N에 의존하는 `% N`을 버리고, **서버와 키를 같은 원(해시 링)** 위에 올린다.
> 그러면 서버가 변해도 **인접 구간의 키(평균 k/n)만** 이동한다.

---

## 0. 먼저: 핵심 전환 — "나눗셈"에서 "링 위의 위치"로

단순 해시는 `hash(key) % N` 처럼 **N으로 나눈다**. 그래서 N이 흔들리면 전부 흔들렸다.
안정 해시는 나머지 연산을 **쓰지 않는다.** 대신 서버도 키도 링 위의 **한 점**으로 보고,
"이 키에서 가장 가까운(시계방향) 서버는 누구인가"라는 **국소적 질문**으로 바꾼다.

> 핵심: 매핑을 **전역(N에 의존)** 에서 **국소(이웃 서버에만 의존)** 로 바꾼 것이 안정 해시의 전부다.

---

## 1. 해시 공간과 해시 링

- 해시 함수 f로 **SHA-1**을 쓴다고 하자. 출력 범위(해시 공간)는 **0 ~ 2¹⁶⁰ - 1**.
- 이 일직선 공간의 **양 끝(0과 2¹⁶⁰-1)을 구부려 맞붙이면** 원형의 **해시 링(hash ring)** 이 된다.

```mermaid
flowchart LR
L["일직선 해시 공간<br/>x0=0 ················ xn=2¹⁶⁰-1"] -->|양 끝을 접는다| R["원형 해시 링<br/>(xn과 x0가 한 점에서 만남)"]
```

> 링에는 시작도 끝도 없다. 시계방향으로 돌면 값이 커지다가 최대값 다음 0으로 돌아온다.

---

## 2. 서버와 키를 링에 올린다 (modulo 없음)

- **서버**: 서버의 IP나 이름을 해시 f에 넣어 나온 값 → 링 위의 위치. (예: 서버 0~3 → s0, s1, s2, s3)
- **키**: 같은 방식으로 키도 링 위의 한 점. (예: key0~3 → k0, k1, k2, k3)

> ⚠️ 여기 쓰는 해시 함수는 STEP1의 함수와 **다르며, 나머지 연산 %를 쓰지 않는다.** 서버와 키를 같은 링 위에 "그냥 뿌린다".

---

## 3. 서버 조회 — 시계방향 최초 서버 규칙

키가 저장되는 서버는, **그 키의 위치에서 시계방향으로 링을 돌다 처음 만나는 서버**다.

```mermaid
flowchart LR
K["키 위치"] -->|시계방향 탐색| S["처음 만난 서버 = 담당 서버"]
```

원문 그림 5-7 예시(서버 s0~s3, 키 k0~k3가 번갈아 배치):

| 키 | 시계방향 첫 서버 | 저장 위치 |
| --- | --- | --- |
| key0 | s0 | 서버 0 |
| key1 | s1 | 서버 1 |
| key2 | s2 | 서버 2 |
| key3 | s3 | 서버 3 |

> 핵심: 담당 서버를 찾는 데 **N이 등장하지 않는다.** "내 위치에서 시계방향 이웃"만 보면 된다.

---

## 4. 서버 추가 — 영향받는 키는 국소적

새 서버를 링에 올리면, **그 서버와 반시계 방향 직전 서버 사이 구간의 키만** 새 서버로 이동한다. 나머지는 그대로.

원문 그림 5-8: 서버 4(s4)가 **s0과 k0 사이**에 추가되는 경우.

```mermaid
flowchart LR
B["추가 전<br/>key0 → 시계방향 첫 서버 s0"] --> A["추가 후<br/>key0 → 이제 s4가 먼저 만남<br/>→ key0만 s4로 이동"]
A --> N["k1, k2, k3은 담당 서버 그대로"]
```

- 추가 전 key0은 서버 0에 있었지만, s4가 k0 바로 뒤(시계방향)에 끼어들면서 **key0만** s4로 재배치.
- 다른 키들은 자기 시계방향 첫 서버가 그대로라 **이동 없음**.

---

## 5. 서버 제거 — 역시 국소적

서버 하나가 빠지면, **그 서버가 담당하던 구간의 키만** 시계방향 다음 서버로 넘어간다.

원문 그림 5-9: 서버 1(s1)이 삭제되는 경우.

- s1이 담당하던 key1만 시계방향 다음 서버인 **서버 2로 재배치**.
- 나머지 키(key0, key2, key3)는 영향 없음.

```mermaid
flowchart LR
D["서버 1(s1) 삭제"] --> M["s1 담당 구간의 key1만<br/>시계방향 다음 서버 s2로 이동"]
M --> U["k0, k2, k3은 그대로"]
```

> 핵심: 추가든 삭제든 **바뀌는 건 인접한 한 구간뿐.** 평균 이동량이 **k/n** 인 이유가 여기 있다.

---

## 6. 단순 해시 vs 안정 해시 — 서버 변동 대응

| 상황 | 단순 해시 (`% N`) | 안정 해시 (해시 링) |
| --- | --- | --- |
| 서버 추가 | 거의 모든 키 재계산 | 새 서버~직전 서버 **구간의 키만** |
| 서버 삭제 | 거의 모든 키 재계산 | 삭제 서버 담당 **구간의 키만** |
| 평균 이동량 | ≈ k (전부) | **k/n** |
| 담당 서버 계산 | N에 의존 | 링 위 시계방향 이웃 |

> 이것만으로 "서버가 변할 때 이동 최소화" 요구는 해결됐다.
> 하지만 아직 **"고르게 나뉜다"** 는 보장은 없다 — 그게 STEP 3.

---

## ✅ STEP 2 체크리스트

- [ ] 해시 공간(0 ~ 2¹⁶⁰-1)을 링으로 만드는 과정을 설명할 수 있다
- [ ] 안정 해시는 **나머지 연산 %를 쓰지 않는다**는 점을 안다
- [ ] 키의 담당 서버를 찾는 규칙(**시계방향 최초 서버**)을 말할 수 있다
- [ ] 서버 추가 시 **key0만 이동**하는 그림 5-8을 설명할 수 있다
- [ ] 서버 삭제 시 **key1만 이동**하는 그림 5-9를 설명할 수 있다
- [ ] 평균 이동량이 **k/n** 인 이유(인접 구간만 변함)를 안다

---

## 💬 예상 면접 질문

**Q1. 안정 해시는 키가 저장될 서버를 어떻게 정하나?**
> 서버와 키를 같은 **해시 링** 위에 매핑하고, 키 위치에서 **시계방향으로 돌다 처음 만나는 서버**에 저장한다. 나머지 연산 없이 링 위의 이웃 관계만 본다.

**Q2. 단순 해시와 달리 왜 재배치가 적은가?**
> 담당 서버가 **N이 아니라 링 위 인접 이웃**에만 의존하기 때문이다. 서버를 추가/삭제해도 변하는 건 그 서버에 인접한 한 구간뿐이라, 평균 **k/n 개**만 이동한다.

**Q3. 서버를 추가할 때 정확히 어떤 키가 이동하나?**
> 새 서버 위치에서 **반시계 방향으로 직전 서버까지의 구간**에 있던 키들만 새 서버로 넘어간다. 그림 5-8에선 s4가 s0 앞에 끼면서 그 구간의 key0만 이동하고 나머지는 그대로다.

**Q4. 여기서 SHA-1을 쓰는 이유와 해시 공간 크기는?**
> 출력이 **0 ~ 2¹⁶⁰-1** 로 매우 넓어 서버·키를 링 위에 충돌 없이 고르게 흩뿌리기 좋다. 이 공간의 양 끝을 이어 원형 링으로 사용한다.

➡️ 이전: [STEP 1 — rehash 문제](01_STEP1_rehash문제_단순해시의한계.md) | 다음: [STEP 3 — 가상 노드](03_STEP3_가상노드_균등분포.md)
Loading