diff --git "a/004.\354\262\230\353\246\254\354\234\250\354\240\234\355\225\234\354\236\245\354\271\230\354\204\244\352\263\204" "b/004.\354\262\230\353\246\254\354\234\250\354\240\234\355\225\234\354\236\245\354\271\230\354\204\244\352\263\204" deleted file mode 100644 index 8b13789..0000000 --- "a/004.\354\262\230\353\246\254\354\234\250\354\240\234\355\225\234\354\236\245\354\271\230\354\204\244\352\263\204" +++ /dev/null @@ -1 +0,0 @@ - diff --git "a/004_\354\262\230\353\246\254\354\234\250\354\240\234\355\225\234\354\236\245\354\271\230\354\204\244\352\263\204/2\354\243\274\354\260\250.md" "b/004_\354\262\230\353\246\254\354\234\250\354\240\234\355\225\234\354\236\245\354\271\230\354\204\244\352\263\204/2\354\243\274\354\260\250.md" new file mode 100644 index 0000000..d813b92 --- /dev/null +++ "b/004_\354\262\230\353\246\254\354\234\250\354\240\234\355\225\234\354\236\245\354\271\230\354\204\244\352\263\204/2\354\243\274\354\260\250.md" @@ -0,0 +1,115 @@ +### 어떠한 처리율 제한 알고리즘을 사용할 것인가 ? + +- 토큰 버킷 알고리즘과 이동 윈도우 카운터 알고리즘에 대해서 고민 +- 토큰 버킷 알고리즘에서 토큰 버킷이란 지정된 용량을 갖는 컨테이너다. 각 요청이 처리될 때마다, 하나의 토큰을 사용한다. 요청이 도착하면, 충분한 토큰이 있으면 시스템에 전달하고, 충분한 토큰이 없으면, 버려지게 되는 경우. +- 토큰 버킷 알고리즘은 2개의 인자를 받는다. + - 버킷 크기 : 버킷에 담을 수 있는 토큰의 최대 개수 + - 토큰 공급률 : 초당 몇개의 토큰이 버킷에 공급되는 가? +- 장점 - 구현이 쉽고, 메모리 사용 측면에서 효율적 + +- 이동 윈도우 카운터 알고리즘. +- 현재 1초간의 요청 수 + 직전 1초간의 요청 수 *이동 윈도와 직전 1초가 겹치는 비율 +- 짧은 시간에 몰리는 트래픽에도 잘 대응한다. +- 직전 시간대에 도착한 요청이 균등하게 분포되어 있다고 가정한 상태에서, 추정치를 계산하기 때문에 느슨하다고 생각이 들지만, 시스템의 실제 상태와 맞지 않게 허용되거나 버려진 요청은 0.03% +- 이동 윈도우 카운터 알고리즘이 낫다고 느낌. 대규모 시스템이므로, 토큰에서 발생할 수 있는 최대 ( 버킷 크기 + 토큰 공급률 ) 이 작다고 느껴질 수도 있지만, 이게 여럿이 모이면, 서버에 큰 부하가 될 수 도 있다고 생각함. + +### 여러 서버로 처리되는 경우에 따른 동시성 이슈 + +- Redis 의 중앙 저장소를 만들고, +- LuaScript 또는 Sorted Set 을 이용하여 해결 + +--- + +### 정말로 이게 최선인가 ? + +- Rate Limiter 에서 중요한 것은 무엇인가 ? +- 실제로 정의한 그만큼만 들어오는 게 중요한 것인 가? +- API Gateway 에서 정체 없이 빠르게 처리되는 게 더 중요한 것인가 ? +- 도메인에 따라서 나뉘지 않을까 싶다. + +### Local + Global 2계층 — 정확히 어떻게 계산하나 + +> 로컬은 "이 노드 한 대가 본 요청"만 안다. 글로벌은 "전 노드 합산"을 안다. +로컬은 빠르지만 부분만 보고, 글로벌은 전체를 보지만 느리다. 2계층은 이 둘을 어떻게 조합하느냐의 문제다. +> + +## 모델 A - "일단 Redis에 물어보되, 명백한 초과만 미리 쳐낸다" + +요청이 들어오면 노드는 먼저 **자기 로컬 카운트만** 본다. 여기서 내릴 수 있는 판단은 단 하나: *"나 이 노드 혼자서 이미 전체 한도(20)를 넘겼나?"* + +- 넘겼으면 → **즉시 거부**. Redis 안 간다. +- 안 넘겼으면(정상 상황에선 거의 다 여기) → **Redis로 위임**. + +그리고 진짜 통과/거부 판정은 **항상 Redis가** 내린다. Redis는 전 노드가 올린 카운트를 합산한 값(슬라이딩 윈도우 추정 `est`)으로 최종 결정하니까 이게 **권위**다. + +→ 즉 로컬은 그냥 **거름망**이다. "누가 봐도 초과인 트래픽"만 걸러서 Redis를 보호할 뿐, 실제 판단은 안 한다. 그래서 **정확하지만(오버슈트 거의 0) 정상 부하에서도 거의 매 요청이 Redis를 친다** → RTT를 못 없앤다. + +```mermaid +sequenceDiagram + participant C as 클라이언트 + participant N as 노드 (로컬) + participant R as Redis (권위) + + C->>N: 요청 + Note over N: 로컬 카운트 확인
나 혼자 20건 이상? + alt 이미 한도 초과 + N-->>C: 즉시 거부 (Redis 0회) + else 애매 — 정상 부하 대부분 + N->>R: 합산 카운트 조회 + 판정 위임 + Note over R: est = 전 노드 합산
최종 통과/거부 결정 + R-->>N: allow / deny + N-->>C: 응답 + end +``` + +> 정상 트래픽은 거의 다 아래 `else`로 빠져 **매번 Redis를 친다** → 이게 "느린 이유". +> + +## 모델 B - "내 몫 안이면 Redis한테 안 물어본다" + +중앙이 한도 20을 노드들에게 **미리 쪼개서 나눠준다** (slice, 예: 3노드면 각자 6~7장). + +요청이 들어오면 노드는 **자기 slice가 남았는지만** 본다. + +- 남았으면 → **즉시 허용**. Redis 안 간다. +- 다 썼으면 → 그때 Redis에 "내 몫 더 줘" 요청. + +노드들은 별도로 **주기적으로(100ms마다 등)** 자기가 처리한 수를 Redis에 보고하고, Redis는 그걸 합산해서 **남은 예산을 바쁜 노드에 재분배**한다. + +→ 핫패스에서 **Redis를 아예 안 친다** → 빠름. 대신 노드가 들고 있는 정보가 "살짝 과거"라 **오버슈트(한도 초과)가 생길 수 있다** (오버슈트 폭 ≈ 동기화 주기 × 노드 수). 그리고 LB가 불균등하면 한 노드만 slice 소진하고 나머진 노는 사태가 나서 → **동적 재분배가 필수**다. + +```mermaid +sequenceDiagram + participant C as 클라이언트 + participant N as 노드 (slice 보유) + participant R as Redis (예산 분배) + + Note over C,N: 핫패스 — 요청마다 + C->>N: 요청 + Note over N: 내 slice 남았나? + alt slice 남음 (대부분) + N-->>C: 즉시 허용 (Redis 0회) + else slice 소진 + N->>R: 내 몫 더 줘 + R-->>N: 추가 slice + N-->>C: 응답 + end + + Note over N,R: 백그라운드 — 100ms 주기 (핫패스와 별개) + N->>R: 처리량 보고 + R->>N: slice 재분배 +``` + +> 핫패스(요청마다)에선 Redis 화살표가 거의 안 생기고, Redis 통신은 백그라운드로 분리된다 → "빠르지만 정보가 살짝 과거 → 오버슈트". +> + +## 비유로 한 방에 + +- **모델 A** = 매번 본사에 전화해서 승인받는 방식. 단, "이미 내 책상에 쌓인 게 한도 넘으면" 전화 안 하고 바로 반려. → 정확하지만 전화를 계속 함. +- **모델 B** = 본사가 아침에 "넌 오늘 6건까지 OK" 쿠폰을 미리 나눠줌. 쿠폰 있으면 본사 안 물어보고 바로 처리, 떨어지면 그때 더 달라고 전화. 본사는 가끔 누가 많이 썼나 보고 쿠폰을 다시 분배. → 빠르지만 정보가 약간 늦어서 살짝 넘칠 수 있음. + +> **핵심 한 줄**: A는 Redis가 매번 답하니 정확하지만 느리고, B는 Redis를 가끔만 부르니 빠르지만 근사다. +한도를 빡빡하게 지켜야 하면 A, 정상 부하 보호가 목적이면 B. +> + +--- \ No newline at end of file diff --git "a/005_\354\225\210\354\240\225\355\225\264\354\213\234/img.png" "b/005_\354\225\210\354\240\225\355\225\264\354\213\234/img.png" new file mode 100644 index 0000000..a9b6f10 Binary files /dev/null and "b/005_\354\225\210\354\240\225\355\225\264\354\213\234/img.png" differ diff --git "a/005_\354\225\210\354\240\225\355\225\264\354\213\234/\352\260\200\354\203\201\353\205\270\353\223\234_\354\233\220\353\246\254.html" "b/005_\354\225\210\354\240\225\355\225\264\354\213\234/\352\260\200\354\203\201\353\205\270\353\223\234_\354\233\220\353\246\254.html" new file mode 100644 index 0000000..7e83efc --- /dev/null +++ "b/005_\354\225\210\354\240\225\355\225\264\354\213\234/\352\260\200\354\203\201\353\205\270\353\223\234_\354\233\220\353\246\254.html" @@ -0,0 +1,665 @@ + + + + + +가상 노드(Virtual Node) 작동 원리 + + + + +
+
SYSTEM DESIGN · CH05 안정 해시
+

가상 노드는 어떻게 동작할까?

+

안정 해시의 "쏠림 문제"를 가상 노드가 어떻게 해결하는지 눈으로 확인해봐요

+
+ +
+ + +
+

0 한 문장 요약

+

먼저 결론부터 기억하고 시작해요.

+
+
💡 핵심 아이디어
+

서버 하나를 해시 링 위에 한 점으로 놓는 대신, + 여러 개의 "가짜 점"(가상 노드)으로 쪼개서 링 곳곳에 흩뿌리면 + 데이터가 서버들에게 훨씬 골고루 나뉜다.

+
+

왜 그게 필요한지, 아래에서 하나씩 볼게요. 👇

+
+ + +
+

1 먼저, 안정 해시 링이란?

+

가상 노드를 이해하려면 "해시 링"부터 알아야 해요.

+

서버와 데이터(키)를 모두 같은 해시 함수에 넣어서, 그 결과값을 + 동그란 링(0 → 최대값 → 다시 0) 위의 한 지점에 배치해요.

+
+
🕐 시계로 비유하면
+

링을 시계라고 생각하세요. 서버 A는 3시, 서버 B는 7시 위치에 붙어 있어요. + 데이터 키를 링에 놓은 뒤, 시계 방향으로 돌면서 처음 만나는 서버가 그 데이터를 담당해요.

+
+

이 방식의 장점은 서버를 추가/삭제해도 대부분의 키가 그대로 유지된다는 것. + (일반 해시 hash % N 방식은 서버 수 N이 바뀌면 거의 모든 키가 재배치돼요.)

+
+ + +
+

2 그런데 문제가 있어요: 쏠림

+

서버를 링 위에 "딱 한 점"씩만 놓으면 생기는 일.

+

해시 함수는 서버를 링 위 랜덤한 위치에 떨어뜨려요. + 운이 나쁘면 서버들이 한쪽에 몰리고, 그 사이 구간이 제각각이 됩니다.

+ +
+

서버 3대 — 각각 링에 딱 1점씩 (한쪽으로 쏠린 예시)

+ +
+

+ 🔴 S1이 담당하는 구간(호)이 유독 넓죠? → + S1에 데이터·트래픽이 몰림 = 핫스팟 +

+ +
+
😱 왜 위험할까
+

한 서버에만 부하가 쏠리면 그 서버가 먼저 뻗어요. + 게다가 서버가 죽거나 추가될 때 담당 구간 크기가 들쭉날쭉이라 + 재배치되는 데이터 양도 예측이 안 됩니다.

+
+
+ + +
+

3 해결책: 서버 하나를 여러 점으로 쪼개기 🎯

+

아래 슬라이더를 움직여서 직접 확인해보세요. 이게 이 페이지의 핵심이에요!

+ +

서버 S1을 링에 1점만 놓는 대신, + S1#0, S1#1, S1#2 ... 처럼 이름을 바꿔가며 해시해서 + 여러 점(가상 노드)으로 링 곳곳에 흩뿌려요. + 이 점들은 전부 실제로는 같은 서버 S1을 가리켜요.

+ +
+ + + 1 + +
+

1 → 200으로 올려보세요. 각 서버의 점이 많아져 링에 골고루 박히면서 아래 막대가 평평해지는 걸 보세요.

+ +
+ 서버 1 + 서버 2 + 서버 3 + 서버 4 +
+ +
+ +
+ + +
+
+
+ + +
+

4 노드를 추가하면 키가 얼마나 이사할까? 🚚

+

노드 3대 → 4대로 늘릴 때 세 방식을 나란히 비교해요. 님이 헷갈린 바로 그 지점이에요.

+ +
+ 노드1 + 노드2 + 노드3 + 노드4 (새로 추가) + 🚚 = 이사한 키 +
+ +
+ + +
+

지금은 노드 3대 상태예요. 버튼을 누르면 4대가 되고, 이사한 키가 빨갛게 튀어나와요. 각 칸의 24개 키가 어느 노드로 가는지 색으로 표시돼요.

+ +
+ +
+
👀 결과 읽는 법
+

hash % N은 거의 다 이사(빨강 폭탄 💥) — 순진한 방식. + 해시 슬롯안정 해시는 딱 1/4만 이사예요. + 슬롯 ≈ 가상 노드라서 이 둘의 결과가 비슷하게 나오는 게 핵심 포인트!

+
+
+ + +
+

5 그래서 데이터는 어떻게 찾아가나요?

+

가상 노드가 있어도 "찾는 방법"은 똑같이 단순해요.

+
+
+
키를 해시한다
+

데이터 키 "user_1234"를 해시 함수에 넣어 링 위 한 지점을 구해요.

+
+
+
시계 방향으로 돈다
+

그 지점에서 시계 방향으로 이동하며 처음 만나는 점(가상 노드)을 찾아요.

+
+
+
가상 노드 → 실제 서버로 변환
+

만난 점이 S3#7이라면? "#7"은 무시하고 실제 서버 S3가 담당해요.

+
+
+

즉, 가상 노드는 "링 위에서만 존재하는 라벨"일 뿐, + 실제 서버 대수가 늘어난 게 아니에요. 서버 3대는 그대로 3대예요.

+
+ + +
+

6 정리: 있을 때 vs 없을 때

+ + + + + + + + + + +
항목가상 노드 없음 (서버당 1점)가상 노드 있음 (서버당 여러 점)
데이터 분포들쭉날쭉 (쏠림 위험)골고루 균등
서버 추가/삭제 시재배치량 예측 어려움부드럽고 균등하게 이동
핫스팟잘 생김잘 안 생김
메모리·관리 비용적음점이 많아질수록 증가
+
+
⚖️ 트레이드오프
+

가상 노드가 많을수록 분포는 균등해지지만, 링에서 관리할 점이 많아져 + 메모리와 탐색 비용이 늘어요. 그래서 무한정 늘리지 않고 + 실무에선 보통 서버당 100~200개 수준을 씁니다.

+
+
+ + +
+

🎁 이것만 기억하세요

+ +
+ + +
+ + + + + diff --git "a/005_\354\225\210\354\240\225\355\225\264\354\213\234/\354\225\210\354\240\225\355\225\264\354\213\234.md" "b/005_\354\225\210\354\240\225\355\225\264\354\213\234/\354\225\210\354\240\225\355\225\264\354\213\234.md" new file mode 100644 index 0000000..dbce41a --- /dev/null +++ "b/005_\354\225\210\354\240\225\355\225\264\354\213\234/\354\225\210\354\240\225\355\225\264\354\213\234.md" @@ -0,0 +1,109 @@ +# 안정 해시 & 가상 노드 정리 + +> 💡 **수평적 규모 확장성** 달성 = 요청 또는 데이터를 서버에 균등하게 나누는 것 + +--- + +## 안정 해시란? + +- 해시 테이블 크기가 변경될 때 +- 오직 **`k`(키의 개수) / `n`(슬롯의 개수)** 의 키만 재배치하는 해시 기술 + +### 기본 절차 + +1. 서버와 키를 균등 분포 해시 함수를 사용해 **해시 링**에 배치한다. +2. 키의 위치에서 링을 **시계 방향**으로 탐색하다 만나는 최초의 서버가 키가 저장될 서버다. + +### 이랬을 때의 문제점 + +- 파티션의 크기를 균등하게 유지하는 게 불가능하다. +- 키의 균등 분포를 달성하기가 어렵다. + +### 해결 방법 — 가상 노드(virtual node) / 복제(replica) + +- 키의 위치로부터 시계방향으로 링을 탐색하다 만나는 **최초의 가상 노드**가 해당 키가 저장될 서버. +- 가상 노드의 개수 증가 → 키의 분포가 점점 더 균등. +- 그러나 가상 노드 데이터를 저장할 공간이 더 필요 → **tradeoff (타협이 필요)** + +--- + +## 안정 해시의 이점 + +- 서버 추가/삭제 시 재배치되는 키의 수가 최소화 +- 데이터가 균등 분포 → 수평적 규모 확장 +- 핫스팟 키 문제 감소 + +--- + +## 가상 노드가 실제로 어떻게 작동하는가? + +> 🖼️ `image.png` + +--- + +## 실제로 가상 노드 / 안정 해시가 어디서 사용되는가? + +- 수평적 규모 확장 달성에 용이하다고 하는데, 우리가 주로 사용하는 수평적 규모 확장으로는 무엇이 있는가? +- 우리가 자주 파티션을 나누는 것들로서 무엇이 있을까? +- **Redis Cluster**, **Kafka** 각각은 파티션의 개수가 바뀔 때 어떻게 작동할까? +- 둘 다 안정 해시를 고르는 대신, **고정 개수의 중간층(슬롯/파티션)** 을 두는 방식을 택함. + +--- + +## Hash Slot이란 무엇인가? + +예시) **Redis Cluster** + +- Redis Cluster는 전체 키 공간을 **16,384개의 슬롯**으로 나눔. +- 각 키는 **CRC16** 해시 함수를 통해 슬롯 번호로 매핑. +- 각 슬롯은 클러스터의 특정 노드에 할당. +- 따라서 **슬롯 단위로 데이터 이동(Migration)이 가능**해짐. + +--- + +## Redis Cluster에서 노드가 추가/삭제될 때 + +### ➕ 노드를 추가할 때 + +1. 새 노드는 **"빈손(슬롯 0개)"** 으로 합류. + 그냥 추가하면 슬롯이 하나도 없어서 아무 데이터도 안 받음. 반드시 **리샤딩(reshard)** 필요. +2. 기존 노드들에서 슬롯을 **"걷어와서"** 새 노드에 줌. + +**구체적 예시 (슬롯 16,384개)** + +``` +[추가 전] 마스터 3대 +A: 5,461 B: 5,461 C: 5,462 + +[추가 후] 마스터 4대 → 목표: 각 4,096 +A: 4,096 B: 4,096 C: 4,096 D(신규): 4,096 +→ A에서 ~1,365, B에서 ~1,365, C에서 ~1,366을 걷어서 + D에게 4,096개 몰아줌 +``` + +**"어디서 몇 개?"는 누가 정하나?** + +- `redis-cli --cluster reshard` → **운영자가 직접 지정** + ("A·B·C에서 총 4,096개를 D로" 식으로 개수·출발노드·도착노드 입력) +- `redis-cli --cluster rebalance` → **도구가 자동 계산** + ("각 노드가 몇 개여야 균등한가"를 보고 최소한의 슬롯만 이동) + +--- + +## 🏁 마지막 정리 + +**1. 문제의 출발점** +`hash % N`은 서버 수가 바뀌면 거의 모든 키가 재배치됨. 이 재배치를 최소화하는 게 핵심. + +**2. 안정 해시의 답** +링 + 시계방향 탐색으로 재배치를 `k/n`(최소)로 줄임. 쏠림은 **가상 노드**로 해결하되, 균등함과 저장 공간 사이에 tradeoff가 있음. + +**3. 현실 시스템은 "같은 목표, 다른 방식"** +Redis·Kafka는 순수 안정 해시 대신 **고정 슬롯/파티션 + 운영자 통제** 방식을 택함. 이동량은 `k/n`으로 동일하지만, 경계를 **자동으로 정하느냐(링) vs 통제하느냐(슬롯)** 가 차이. 그래서 **슬롯 ≈ 가상 노드**. + +**4. Redis Cluster의 노드 추가** +새 노드는 빈손 → `reshard`로 기존 노드에서 슬롯을 결정적으로 걷어옴(랜덤 아님). `reshard`는 운영자 지정, `rebalance`는 도구 자동 계산. + +**5. 한 문장 요약** +> 안정 해시·가상 노드는 서버 수가 바뀌어도 이동을 최소화하는 기술이고, +> Redis는 같은 목표를 **16,384 고정 슬롯 + 운영자 통제**라는 다른 방식으로 달성한다. \ No newline at end of file