From 85c6e7589ab262c59ce9feabce0925879d319d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=9D=B8=EC=B2=A0?= Date: Sun, 28 Jun 2026 18:58:11 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[4=EC=9E=A5]=20=EC=B2=98=EB=A6=AC=EC=9C=A8?= =?UTF-8?q?=20=EC=A0=9C=ED=95=9C=20=EC=9E=A5=EC=B9=98=20=EC=84=A4=EA=B3=84?= =?UTF-8?q?=201=EC=B0=A8=20=EB=B3=B4=EA=B0=95=20=EB=B0=8F=202=EC=B0=A8=20?= =?UTF-8?q?=EC=84=A4=EA=B3=84=20=EC=B6=94=EA=B0=80=20=E2=80=94=20=EC=A0=95?= =?UTF-8?q?=EC=9D=B8=EC=B2=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 1차: 각 알고리즘 파라미터 명시, Redis 원자성 문제 심화, 규칙 로딩 아키텍처 추가 - 2차: 책 학습 후 심화 설계안 신규 작성 (알고리즘 선택 트리, Lua 구현, 분산 설계 등) --- ...\354\204\244\352\263\204_1\354\260\250.md" | 376 +++++++-- ...\354\204\244\352\263\204_2\354\260\250.md" | 732 ++++++++++++++++++ 2 files changed, 1060 insertions(+), 48 deletions(-) create mode 100644 "04-\354\262\230\353\246\254\354\234\250-\354\240\234\355\225\234\354\236\245\354\271\230/\354\240\225\354\235\270\354\262\240/02_\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\260\250.md" diff --git "a/04-\354\262\230\353\246\254\354\234\250-\354\240\234\355\225\234\354\236\245\354\271\230/\354\240\225\354\235\270\354\262\240/01_\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_1\354\260\250.md" "b/04-\354\262\230\353\246\254\354\234\250-\354\240\234\355\225\234\354\236\245\354\271\230/\354\240\225\354\235\270\354\262\240/01_\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_1\354\260\250.md" index 6ae5973..5590f65 100644 --- "a/04-\354\262\230\353\246\254\354\234\250-\354\240\234\355\225\234\354\236\245\354\271\230/\354\240\225\354\235\270\354\262\240/01_\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_1\354\260\250.md" +++ "b/04-\354\262\230\353\246\254\354\234\250-\354\240\234\355\225\234\354\236\245\354\271\230/\354\240\225\354\235\270\354\262\240/01_\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_1\354\260\250.md" @@ -133,9 +133,25 @@ Q. 요청이 거부된 경우 사용자에게 알려야 하는가? [토큰 없음] → 요청 버림 ``` -- **버킷 단위**: API endpoint별, IP별, 전역 등 -- 장점: 구현 쉬움, 메모리 효율적, burst traffic 허용 -- 단점: 버킷 크기·토큰 공급률 튜닝이 까다로움 +**파라미터 (2개)** +| 파라미터 | 설명 | 예시 | +|---------|------|------| +| `bucket_size` | 버킷 최대 토큰 수 | 4 | +| `refill_rate` | 초당 추가되는 토큰 수 | 4/s | + +**버킷 단위 설계** +- API endpoint별 버킷: `/post/new` — 분당 1개, `/search` — 분당 5개 +- IP별 버킷: IP 주소당 하루 10만 요청 +- 전역 버킷: 전체 시스템 초당 요청 상한 + +**장점** +- 구현 쉽고, 메모리 효율적 +- **burst traffic 허용**: 짧은 시간 내 토큰이 충분하면 한꺼번에 처리 가능 + +**단점** +- `bucket_size`와 `refill_rate` 두 파라미터를 적절히 튜닝하기 어려움 + +> 실제 사용: **Amazon API Gateway**, **Stripe** API — 초당 요청 제한에 토큰 버킷 사용 --- @@ -151,8 +167,21 @@ Q. 요청이 거부된 경우 사용자에게 알려야 하는가? [API 서버] ``` -- 장점: 안정적 출력(stable outflow rate) 보장, 메모리 효율적 -- 단점: 트래픽 급증 시 큐에 오래된 요청 적체 → 새 요청 드롭 +**파라미터 (2개)** +| 파라미터 | 설명 | +|---------|------| +| `bucket_size` | 큐 최대 크기 (요청 수) | +| `outflow_rate` | 초당 꺼내는 요청 수 (고정) | + +**장점** +- 안정적 출력(stable outflow rate) 보장 → 다운스트림 서버에 일정한 부하 +- 메모리 효율적 (큐 크기 고정) + +**단점** +- 트래픽 급증 시 큐가 오래된 요청으로 꽉 차고, **최신 요청이 드롭됨** +- 두 파라미터 튜닝이 까다로움 + +> 실제 사용: **Shopify** — 초당 outflow rate 기반으로 API 요청 처리 --- @@ -167,17 +196,29 @@ Q. 요청이 거부된 경우 사용자에게 알려야 하는가? 임계치=5 → 윈도 3에서 초과 요청 차단 ``` -- 장점: 메모리 효율, 구현 쉬움, 특정 트래픽 패턴 처리에 적합 -- 단점: **윈도 경계 부근**에서 2배 트래픽 허용 가능 +**파라미터 (2개)** +| 파라미터 | 설명 | +|---------|------| +| `window_size` | 윈도 크기 (예: 1분) | +| `max_requests` | 윈도당 최대 허용 요청 수 | + +**장점**: 메모리 효율, 구현 쉬움, 특정 트래픽 패턴 처리에 적합 +**단점**: **윈도 경계 부근**에서 순간적으로 2배 트래픽 허용 가능 ``` 예) 임계치 = 분당 5건 - 59초 60초(새 윈도) 61초 - |||| ||||| - (5건) (5건) → 실제로는 2초 안에 10건 처리됨! +타임라인: 00:59 01:00(새 윈도) 01:01 + ───────────────────────────────── + ||||| ||||| + (5건) (5건) + ← 1분 기준 각각 허용 → + ↑ 실제 1:59 ~ 1:01, 2초 안에 10건 처리됨! ``` +> 이 경계 문제(boundary condition)는 고정 윈도의 구조적 결함이다. +> 정확도가 중요한 서비스에서는 이 알고리즘을 피해야 한다. + --- #### 이동 윈도 로깅 (Sliding Window Log) @@ -194,25 +235,61 @@ Q. 요청이 거부된 경우 사용자에게 알려야 하는가? 만료 타임스탬프 제거 후 로그 크기 ≤ 임계치 → 요청 허용 ``` -- 장점: 어느 순간이든 처리 한도를 넘지 않음 (정확) -- 단점: 거부된 요청의 타임스탬프도 보관 → **메모리 많이 사용** +**동작 방식** +1. 새 요청이 오면 현재 타임스탬프를 Sorted Set에 추가 +2. 만료된 타임스탬프 (현재시각 - 윈도 크기 이전) 제거 +3. Sorted Set 크기(요청 수) ≤ 임계치 → 허용 / 초과 → 거부 + (거부된 경우에도 타임스탬프는 로그에 유지) + +**장점**: 어느 순간이든 처리 한도를 넘지 않음 (정확) + +**단점**: 거부된 요청의 타임스탬프도 보관 → **메모리를 많이 사용** + +``` +예) 유저 1명이 분당 10건 제한 받고 1만 요청 시도한다면? +→ 거부된 9,990건의 타임스탬프도 Redis Sorted Set에 적재됨 +→ 대규모 DDoS 상황에서 Redis 메모리 급증 위험 +``` --- #### 이동 윈도 카운터 (Sliding Window Counter) +고정 윈도 카운터 + 이동 윈도 로깅의 장점을 결합한 하이브리드 방식. + +**공식** + ``` -이전 윈도(1분) 요청 수 = 8 -현재 윈도(1분) 경과 비율 = 25% +이전 윈도 요청 수 = 8 +현재 윈도 경과 비율 = 25% (현재 윈도 시작 후 15초 경과 / 60초) -현재 윈도에서의 예상 요청 수 -= (8 × (1 - 0.25)) + 현재 윈도 요청 수 +현재 시점의 예상 요청 수 += (이전 윈도 요청 수 × (1 - 현재 윈도 경과 비율)) + 현재 윈도 요청 수 += (8 × 0.75) + α = 6 + α ``` -- 고정 윈도 카운터 + 이동 윈도 로깅의 장점 결합 -- 장점: 짧은 시간 트래픽 대응, 메모리 효율적 -- 단점: 이전 윈도 요청이 균등 분포라고 가정 (느슨하지만 실험적으로 무해) +**구체적 예시** + +``` +임계치: 분당 10건 + +이전 윈도 (00:00 ~ 01:00) 요청 = 8건 +현재 윈도 (01:00 ~ 02:00)에서 01:15 시점에 새 요청 도착 + → 경과 비율 = 15/60 = 25% + +예상 요청 수 = (8 × 0.75) + (현재 윈도 기존 요청 수 α) + = 6 + α + +α = 3이면 → 6 + 3 = 9 < 10 → 허용 +α = 4이면 → 6 + 4 = 10 = 10 → 거부 +``` + +**장점**: 메모리 효율적 (카운터 2개만 유지), 짧은 시간 트래픽 급증 완화 +**단점**: 이전 윈도의 요청이 균등 분포라는 가정이 있음 (느슨하지만 실험적으로 무해) + +> 실제 사용: **Cloudflare** — 동일 원리로 초당 요청 제한 적용 +> Cloudflare 실험 결과: 4억 건 샘플 중 0.003%만 경계 오차 발생 --- @@ -254,36 +331,76 @@ Client ──→ Rate Limiter Middleware ─┤ ``` **카운터 저장소로 Redis를 선택하는 이유** -- DB(디스크 접근)는 너무 느림 -- Redis는 인메모리 → 빠름 +- DB(디스크 접근)는 너무 느림 → 매 요청마다 DB 조회는 병목 +- Redis는 인메모리 → 빠름 (싱글 스레드 → 자체적으로 race condition 없음) +- `INCR` 명령으로 원자적 카운터 증가 가능 - `EXPIRE` 명령으로 시간 기반 만료 정책 구현 가능 +**규칙 로딩 아키텍처 (Rule Cache)** + +``` +┌──────────────┐ (1) 주기적으로 규칙 로드 +│ 규칙 저장소 │ ─────────────────────→ ┌───────────────┐ +│ (YAML/Disk) │ │ Rate Limiter │ +└──────────────┘ │ Worker │ + │ (메모리 캐시) │ + └───────┬───────┘ + │ (2) 요청 시 캐시에서 규칙 조회 + ▼ + ┌───────────────┐ + │ Redis Counter │ + └───────────────┘ +``` + +- 규칙 파일은 디스크에 저장, **워커 스레드가 주기적으로 갱신** +- 요청 처리 시 디스크 I/O 없이 **인메모리 캐시에서 규칙 조회** +- Redis에는 규칙이 아닌 **카운터만 저장** (역할 분리) + --- ## 3. 상세 설계 ### 3-1. 처리율 제한 규칙 -설정 파일(YAML) 형태로 디스크에 저장, 오픈소스 활용 가능 (e.g. Envoy, Kong) +설정 파일(YAML) 형태로 디스크에 저장, 오픈소스 활용 가능 (e.g. Envoy, Lyft ratelimit, Kong) + +**Lyft ratelimit 스타일 규칙 예시** ```yaml -# 예시 규칙 설정 -rate_limits: - - domain: auth +domain: messaging # 규칙이 적용되는 도메인 +descriptors: + # 마케팅 메시지: IP당 하루 5건 제한 + - key: message_type + value: marketing descriptors: - - key: user_id + - key: to_number rate_limit: - unit: minute + unit: day requests_per_unit: 5 - - domain: post + # 인증 API: 클라이언트 IP당 분당 5건 + - key: auth descriptors: - key: ip_address rate_limit: - unit: day - requests_per_unit: 10 + unit: minute + requests_per_unit: 5 + + # 전역 로그인 시도: 사용자당 하루 3건 + - key: user_login + rate_limit: + unit: day + requests_per_unit: 3 ``` +**규칙 설계 원칙** +- `domain`: 규칙을 논리적으로 그룹화 (API 도메인별 분리) +- `descriptors`: 제한 기준 (IP, user_id, 요청 타입 등 조합 가능) +- `unit`: `second` / `minute` / `hour` / `day` +- **계층적 적용 가능**: 전역 제한 + 사용자별 제한을 동시에 적용 + +**실제 운영 팁**: 로그인 시도 제한, 가입 제한, SMS 발송 제한은 별도 domain으로 분리하는 것이 운영 편의상 유리함 + --- ### 3-2. 제한 초과 요청 처리 (Throttle) @@ -295,14 +412,19 @@ HTTP/1.1 429 Too Many Requests X-RateLimit-Limit: 100 X-RateLimit-Remaining: 0 +X-RateLimit-Reset: 1609459200 X-RateLimit-Retry-After: 30 ``` | 헤더 | 의미 | |------|------| | `X-RateLimit-Limit` | 허용 최대 요청 수 | -| `X-RateLimit-Remaining` | 남은 요청 수 | -| `X-RateLimit-Retry-After` | 몇 초 후 재시도 가능 | +| `X-RateLimit-Remaining` | 현재 윈도에서 남은 요청 수 | +| `X-RateLimit-Reset` | 한도가 재설정되는 UTC 에포크 타임스탬프 | +| `X-RateLimit-Retry-After` | 몇 초 후 재시도 가능 (429 응답 시에만) | + +> 거부되지 않은 정상 응답에도 `X-RateLimit-Limit`, `X-RateLimit-Remaining`은 포함하는 것이 좋다. +> 클라이언트가 자신의 사용량을 사전에 파악하여 스로틀링 전략을 세울 수 있도록. #### 처리 방법 @@ -330,6 +452,29 @@ Thread A: counter+1 = 5, 저장 Thread B: counter+1 = 5, 저장 ← 실제로는 6이어야 함! ``` +**왜 Redis INCR만으론 부족한가?** + +`INCR`는 원자적이지만, **"읽기 → 판단 → 증가"** 흐름은 원자적이지 않다. + +``` +# 문제 시나리오 (임계치 = 5) +GET counter → 4 (A가 읽음) +GET counter → 4 (B가 읽음, 동시) +INCR counter → 5 (A가 증가, 5 ≤ 5이므로 허용) +INCR counter → 6 (B가 증가, 하지만 판단은 이미 4 기준으로 했음!) + → 6이지만 B도 허용되어 버림! +``` + +**EXPIRE와 INCR의 원자성 문제** + +``` +# 처음 키 생성 시 INCR → EXPIRE 사이에 서버 크래시 나면? +INCR counter → 1 +── 여기서 크래시 ── +EXPIRE counter 60 → (실행 안 됨) +→ counter 키가 만료 없이 영구 존재! +``` + **해결책 선택 기준: 알고리즘에 따라 다르다** > Sorted Set과 Lua 스크립트는 경쟁 관계가 아니다. @@ -357,8 +502,14 @@ Thread B: counter+1 = 5, 저장 ← 실제로는 6이어야 함! **본 설계(토큰 버킷) → Lua 스크립트 선택** +Lua 스크립트는 Redis 서버에서 단일 명령처럼 원자적으로 실행된다. +읽기 → 판단 → 쓰기 사이에 다른 명령이 끼어들 수 없다. + ```lua -- Redis에서 원자적으로 실행 (읽기-수정-쓰기가 끊기지 않음) +-- KEYS[1]: 버킷 키 (예: "rate:user:123") +-- ARGV[1]: 버킷 최대 토큰 수 (bucket_size) +-- ARGV[2]: TTL (초) local tokens = tonumber(redis.call('GET', KEYS[1]) or ARGV[1]) if tokens < 1 then return 0 -- 토큰 없음 → 거부 @@ -368,42 +519,92 @@ redis.call('EXPIRE', KEYS[1], ARGV[2]) return 1 -- 허용 ``` +**Redis 키 설계 예시** + +``` +rate:{domain}:{identifier}:{window} + +예시: +rate:auth:user:42:1609459200 ← 사용자 42의 1분 윈도 카운터 +rate:post:ip:192.168.1.1:1609459200 ← IP 기반 카운터 +rate:global:2024-01-01 ← 전역 일별 카운터 + +키 설계 원칙: +- TTL을 반드시 설정 → 윈도 종료 후 자동 삭제 +- identifier는 해싱 처리 (PII 보호) +``` + **만약 이동 윈도 로깅을 선택했다면 → Sorted Set 사용** ``` +# 세 명령을 Lua로 묶어 원자적으로 실행 ZREMRANGEBYSCORE key 0 (현재시각 - 윈도크기) ← 만료 항목 제거 -ZADD key 현재시각 요청ID ← 새 요청 기록 -ZCARD key ← 현재 요청 수 확인 -→ 임계치 초과 여부 판단 +ZADD key 현재시각 요청UUID ← 새 요청 기록 (score=타임스탬프) +ZCARD key ← 현재 요청 수 확인 +→ ZCARD 결과 ≤ 임계치이면 허용 ``` +**MULTI/EXEC(트랜잭션) vs Lua 스크립트 비교** + +| | MULTI/EXEC | Lua 스크립트 | +|--|-----------|------------| +| 원자성 | 완전하지 않음 (다른 클라이언트가 중간에 읽기 가능) | 완전한 원자성 | +| 조건부 실행 | 불가 (WATCH로 우회 가능) | 가능 (if/else 가능) | +| 성능 | 왕복 2회 필요 | 단일 왕복 | +| 권장 여부 | △ | **✓ 권장** | + --- #### 동기화 (Synchronization) 문제 +여러 처리율 제한 장치(서버)가 동일 사용자의 요청을 처리할 때, 각자 다른 카운터를 보면 제한이 무의미해진다. + ``` -처리율 제한 장치 A ─────┐ - ├──→ 각자 다른 카운터? -처리율 제한 장치 B ─────┘ +사용자가 rate_limiter_1, rate_limiter_2에 각각 5건씩 요청 +→ 각 서버는 "5건 미만"으로 판단하여 모두 허용 +→ 실제로는 10건이 처리됨 (임계치 = 5건 위반) ``` **해결책 비교** ``` -[고정 세션 방식] [중앙 집중형 저장소 방식] -Client A → 장치 A (항상) Client A ─┐ -Client B → 장치 B (항상) ├──→ [Redis] - Client B ─┘ -장점: 단순 장점: 확장 가능, 일관성 보장 -단점: 확장성 없음 단점: Redis 단일 장애점 +[고정 세션 방식 (Sticky Session)] [중앙 집중형 저장소 방식] +Client A → 장치 A (항상) Client A ─┐ +Client B → 장치 B (항상) ├──→ [Redis] + Client B ─┘ +장점: 단순, 카운터 분산 없음 장점: 수평 확장 가능, 일관성 보장 +단점: 확장성 없음, 단점: Redis가 단일 장애점 + 장치 장애 시 세션 유실 Redis Cluster로 해결 가능 ``` > **선택: 중앙 집중형 저장소 (Redis Cluster)** +> +> Redis Cluster(최소 3 마스터)로 단일 장애점 제거. +> 클러스터 장애 시 처리율 제한을 일시적으로 우회할지, 차단할지는 정책으로 결정. --- ### 3-4. 성능 최적화 +#### 로컬 캐시 + 비동기 동기화 + +중앙 Redis로의 매 요청 네트워크 왕복을 줄이기 위해 **로컬 캐시**를 도입한다. + +``` +[요청 흐름] +요청 → Rate Limiter + │ + ▼ + [로컬 인메모리 캐시] + (카운터를 로컬에서 먼저 증가) + │ 비동기로 주기적 동기화 + ▼ + [중앙 Redis Cluster] + +장점: Redis 네트워크 왕복 제거 → 지연 시간 감소 +단점: 동기화 지연 동안 임계치를 약간 초과할 수 있음 (Soft Limit 수용 가능) +``` + #### 멀티 데이터센터 지원 ``` @@ -411,15 +612,19 @@ Client B → 장치 B (항상) ├──→ [Redis] │ │ ▼ ▼ 에지 서버 (Seoul PoP) 에지 서버 (NY PoP) + + 로컬 Redis + 로컬 Redis │ │ └──────────┬──────────────┘ ▼ - [Redis Cluster] + [글로벌 Redis Cluster] (최종 일관성 모델) ``` - 가까운 에지 서버에서 처리율 제한 → **지연 시간 최소화** +- 서울 사용자가 우연히 뉴욕 서버로 라우팅되어도, 글로벌 Redis에 집계되어 일관성 유지 - 제한 장치 간 데이터 동기화: **최종 일관성 모델(Eventual Consistency)** 채택 + - 짧은 시간(수백 ms) 동안 약간의 초과 허용 가능 + - 보안 민감 API(결제 등)에는 강한 일관성이 필요 → 지역 Redis만 사용하거나 Global Redis에 직접 접근 --- @@ -428,15 +633,39 @@ Client B → 장치 B (항상) ├──→ [Redis] ``` 모니터링 대상: ├── 알고리즘 효과성 - │ └── 실제 트래픽 패턴 vs. 설정 임계치 적합성 + │ ├── 실제 트래픽 패턴 vs. 설정 임계치 적합성 + │ └── burst 트래픽 발생 빈도 └── 규칙 효과성 - ├── 정상 요청이 과도하게 차단되지 않는지 - └── DDoS 패턴이 실제로 차단되는지 + ├── 정상 요청이 과도하게 차단되지 않는지 (false positive) + └── DDoS 패턴이 실제로 차단되는지 (false negative) +``` + +**핵심 메트릭** + +| 메트릭 | 설명 | 알람 기준 | +|--------|------|-----------| +| `rate_limited_requests_total` | 429 반환된 요청 수 | 갑자기 급증 시 규칙 재검토 | +| `rate_limit_hit_ratio` | 전체 요청 중 제한된 비율 | 5% 초과 시 임계치 조정 검토 | +| `redis_latency_p99` | Redis 응답 지연 99th percentile | 5ms 초과 시 Redis 부하 점검 | +| `rule_evaluation_latency` | 규칙 평가 소요 시간 | 1ms 초과 시 캐시 미스 점검 | + +**운영 시나리오** + +``` +[시나리오 1] 플래시 세일 이벤트 +→ 정상 유저의 요청도 급증 → 429가 과도하게 발생 +→ 조치: 이벤트 기간 임시 임계치 상향 (규칙 파일 핫리로드) + +[시나리오 2] 알고리즘 변경 후 +→ 토큰버킷 → 이동윈도카운터로 교체 +→ burst traffic 패턴이 달라질 수 있음 +→ 교체 전후 429 비율 비교 모니터링 필수 ``` **알람 기준 예시** - 429 응답 비율이 갑자기 급증 → 규칙 재검토 - 특정 IP의 429 응답 집중 → DDoS 가능성 확인 +- Redis p99 지연 > 5ms → 처리율 제한 장치 자체가 병목 --- @@ -509,6 +738,57 @@ Client → [Load Balancer] → [Rate Limiter Middleware] --- +--- + +## 추가 논의 포인트 (면접 심화) + +### 처리율 제한 장치 자체의 고가용성 + +``` +처리율 제한 장치가 다운되면? + +선택 1. Fail Open (장치 장애 시 모든 요청 허용) + → 서비스 가용성 우선 + → 장애 시간 동안 DDoS 취약 + +선택 2. Fail Closed (장치 장애 시 모든 요청 차단) + → 보안 우선 + → 서비스 다운타임 발생 + +→ 대부분의 서비스: Fail Open 선택 (헬스체크 + 알람으로 빠른 복구) +→ 금융/보안 민감 서비스: Fail Closed 고려 +``` + +### 처리율 제한 장치 우회 (Bypass) 설계 + +내부 서비스 간 호출, 관리자 API 등은 처리율 제한을 우회할 수 있어야 한다. + +```yaml +# 규칙에 whitelist 추가 +bypass_rules: + - key: service_name + value: internal-payment-service # 내부 서비스 식별자 + action: bypass # 처리율 제한 건너뜀 + + - key: user_role + value: admin + action: bypass +``` + +### 요청 비용 차등 적용 (Weighted Rate Limiting) + +모든 요청이 동일한 "비용"을 갖지 않는다. + +``` +일반 GET 요청 → 토큰 1개 소비 +대용량 파일 업로드 → 토큰 10개 소비 +AI 추론 API 호출 → 토큰 50개 소비 + +→ 토큰 버킷 알고리즘에서 자연스럽게 표현 가능 +``` + +--- + ## 참고: 주요 의사결정 흐름 ### 알고리즘 선택 diff --git "a/04-\354\262\230\353\246\254\354\234\250-\354\240\234\355\225\234\354\236\245\354\271\230/\354\240\225\354\235\270\354\262\240/02_\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\260\250.md" "b/04-\354\262\230\353\246\254\354\234\250-\354\240\234\355\225\234\354\236\245\354\271\230/\354\240\225\354\235\270\354\262\240/02_\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\260\250.md" new file mode 100644 index 0000000..b877e9c --- /dev/null +++ "b/04-\354\262\230\353\246\254\354\234\250-\354\240\234\355\225\234\354\236\245\354\271\230/\354\240\225\354\235\270\354\262\240/02_\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\260\250.md" @@ -0,0 +1,732 @@ +# 4장. 처리율 제한 장치 설계 (Rate Limiter) + +--- + +## 문제 이해 및 설계 범위 확정하기 + +키 포인트: 면접 초반에 반드시 질문으로 요구사항을 좁히기 + +``` +지원자: 클라이언트 측 제한인가요, 서버 측 제한인가요? +면접관: 서버 측입니다. + +지원자: 어떤 기준으로 요청을 제한하나요? IP? 사용자 ID? 전역? +면접관: 여러 기준을 유연하게 지원할 수 있어야 합니다. + +지원자: 시스템 규모는 어느 정도인가요? +면접관: 대규모 분산 시스템입니다. 가용성과 일관성 모두 중요합니다. + +지원자: 처리율 제한 장치를 독립 서비스로 만드나요, 아니면 서비스 코드에 넣나요? +면접관: 독립 컴포넌트로 만들어 주세요. + +지원자: 제한된 요청을 클라이언트에게 알려야 하나요? +면접관: 네, 알려야 합니다. +``` + +**확정된 요구사항** + +- 서버 측 처리율 제한 장치 +- IP, 사용자 ID, 전역 등 다양한 기준 지원 +- 대규모 분산 환경, 낮은 지연 시간 필수 +- 가능한 한 메모리를 적게 사용 +- 분산 환경 지원 (여러 서버에서 상태 공유) +- 제한 초과 시 HTTP 헤더로 사용자에게 명확히 안내 +- 높은 내결함성 (처리율 제한 장치 장애가 전체 시스템에 영향 미치면 안 됨) + +--- + +## 개략적 설계안 + +### 처리율 제한 장치의 위치 + +처리율 제한 장치는 클라이언트, 서버, 미들웨어 세 곳에 둘 수 있다. + +``` +┌─────────────────────────────────────────────────────┐ +│ Client │ +└─────────────────────────┬───────────────────────────┘ + │ + ┌───────────▼───────────┐ + │ API Gateway │ ← 여기를 선택 + │ (Rate Limiter 내장) │ + └───────────┬───────────┘ + │ + ┌───────────▼───────────┐ + │ API Servers │ + └───────────────────────┘ +``` + +**클라이언트 측을 선택하지 않는 이유** +- 클라이언트 요청은 위변조가 가능 (신뢰 불가) +- 클라이언트가 모든 구현을 통제하기 어려움 + +**서버 측(코드 내 구현)을 선택하지 않는 이유** +- 언어·프레임워크마다 구현 중복 +- 마이크로서비스 환경에서 N개 서비스 각각 구현 필요 + +**API Gateway(미들웨어)를 선택하는 이유** +- 이미 SSL 종단, 인증, IP 화이트리스트 등이 집중된 단일 진입점 +- 처리율 제한을 언어 무관하게 중앙에서 관리 +- 마이크로서비스 환경에서 표준 구성 + +> 단, 처리율 제한 알고리즘을 직접 제어해야 하거나, +> 자체 인증 서버가 없는 소규모 팀이라면 서버 측 구현이 더 적합할 수 있다. + +--- + +### 처리율 제한 알고리즘 5종 + +#### 1. 토큰 버킷 (Token Bucket) + +**파라미터 2개**: `bucket_size` (최대 토큰 수), `refill_rate` (초당 추가 토큰 수) + +``` + refill_rate (토큰/초) + │ + ▼ + ┌──────────────┐ + │ ● ● ● ● ● │ capacity = bucket_size + │ ● ● ● │ 꽉 차면 추가 토큰 버림 (overflow) + └──────┬───────┘ + │ 요청마다 토큰 1개 소비 + ▼ + 토큰 있음 → 요청 통과 + 토큰 없음 → 요청 버림 (429) +``` + +**버킷 단위 전략** +- API endpoint별: `/v1/orders` — 분당 1건, `/v1/search` — 분당 5건 +- 사용자별 전역: 사용자 ID당 하루 10만 건 +- IP별: IP당 초당 50건 + +**장점**: 구현 단순, 메모리 효율, **burst traffic 허용** (토큰이 쌓여 있으면 한꺼번에 처리) +**단점**: `bucket_size`와 `refill_rate` 튜닝이 까다로움 + +> 실사용: Amazon API Gateway, Stripe + +--- + +#### 2. 누출 버킷 (Leaky Bucket) + +**파라미터 2개**: `bucket_size` (큐 최대 크기), `outflow_rate` (초당 꺼내는 요청 수) + +``` + 새 요청 → ┌──────────────┐ + │ FIFO Queue │ ← 가득 차면 새 요청 즉시 버림 + │ ■ ■ ■ ■ │ + └──────┬───────┘ + │ outflow_rate (고정 속도) + ▼ + [API Server] +``` + +**장점**: 다운스트림 서버에 일정한 부하 (stable outflow rate), 메모리 효율적 +**단점**: 트래픽 급증 시 큐가 오래된 요청으로 채워지고, **최신 요청이 드롭됨** + +> 실사용: Shopify (초당 2건 출력 제한) + +--- + +#### 3. 고정 윈도 카운터 (Fixed Window Counter) + +**파라미터 2개**: `window_size` (윈도 크기, 예: 1분), `max_requests` (윈도당 최대 요청 수) + +``` +시간축: ──[ 윈도 1 ]──[ 윈도 2 ]──[ 윈도 3 ]── + 0s 60s 60s 120s 120s 180s + count=3 count=1 count=5 + 임계치=5 → 초과 차단 +``` + +**치명적 단점: 윈도 경계 공격** + +``` +임계치 = 분당 5건 + + 00:59 ─────────── 01:00 ─────────── 01:01 + ||||| ||||| + (5건, 윈도1 기준 허용) (5건, 윈도2 기준 허용) + ↑ + 2초 안에 실제 10건 처리됨 → 2배 허용! +``` + +**장점**: 구현 단순, 메모리 효율, 특정 패턴에서 적합 +**단점**: 윈도 경계 부근 트래픽 급증 허용 (설계적 결함) + +--- + +#### 4. 이동 윈도 로깅 (Sliding Window Log) + +**파라미터 2개**: `window_size` (윈도 크기), `max_requests` (최대 요청 수) + +**동작 방식** +1. 요청이 오면 현재 타임스탬프를 Sorted Set에 추가 (score = 타임스탬프) +2. `현재시각 - window_size` 이전의 만료된 항목 제거 +3. Sorted Set의 크기 ≤ max_requests → 허용 / 초과 → 거부 +4. **거부된 요청의 타임스탬프도 Sorted Set에 유지** + +``` +현재시각: 1:00:30, 윈도 = 1분, 임계치 = 2건 + + Sorted Set: + score=1:00:05 ○ (1분 전~현재 사이 → 유효) + score=1:00:30 ○ (새 요청) + + 크기 = 2 ≤ 2 → 허용 + + 다음 요청 (1:00:40): + 크기 = 3 > 2 → 거부 (하지만 1:00:40도 Sorted Set에 추가됨) +``` + +**장점**: 어느 순간이든 임계치를 정확히 준수 (높은 정확도) +**단점**: 거부된 요청의 타임스탬프도 보관 → **메모리 낭비** + +``` +예) 사용자 1명이 분당 5건 제한, 1만 건 시도 +→ 거부된 9,995건의 타임스탬프도 Redis에 저장 +→ DDoS 상황에서 Redis 메모리 급증 +``` + +--- + +#### 5. 이동 윈도 카운터 (Sliding Window Counter) + +고정 윈도 카운터(메모리 효율) + 이동 윈도 로깅(정확도)의 하이브리드. + +**파라미터 2개**: `window_size`, `max_requests` + +**공식** + +``` +현재 윈도 요청 수 예측값 += (이전 윈도 요청 수 × (1 - 현재 윈도 경과율)) + 현재 윈도 요청 수 +``` + +**예시** + +``` +임계치: 분당 7건 +이전 윈도 (00:00 ~ 01:00) 요청 = 5건 +현재 윈도 시작(01:00) 후 30초 경과 → 경과율 = 30/60 = 50% +현재 윈도 요청 = 3건 + +예측값 = (5 × 0.5) + 3 = 2.5 + 3 = 5.5 < 7 → 허용 + +현재 윈도 요청이 4건이면: +예측값 = 2.5 + 4 = 6.5 < 7 → 허용 + +현재 윈도 요청이 5건이면: +예측값 = 2.5 + 5 = 7.5 > 7 → 거부 +``` + +**가정**: 이전 윈도의 요청이 균등하게 분포했다고 가정 +→ 느슨하지만 실험적으로 무해 (Cloudflare 데이터: 4억 건 중 0.003%만 오차) + +**장점**: 메모리 효율적 (카운터 2개만 유지), 고정 윈도의 경계 문제 해결 +**단점**: 균등 분포 가정 (burst + 경계 조합의 극단적 케이스에서 약간 부정확) + +> 실사용: Cloudflare + +--- + +#### 알고리즘 비교 요약 + +| 알고리즘 | 메모리 | Burst 허용 | 정확도 | Redis 자료구조 | 추천 상황 | +|----------|--------|-----------|--------|----------------|-----------| +| 토큰 버킷 | ✓ 좋음 | ✓ 허용 | 보통 | String + Lua | **범용 (기본 선택)** | +| 누출 버킷 | ✓ 좋음 | ✗ 불허 | 보통 | List (FIFO) | 일정 출력이 필요한 경우 | +| 고정 윈도 카운터 | ✓ 좋음 | △ (경계 위험) | 낮음 | String + Lua | 단순성이 최우선일 때 | +| 이동 윈도 로깅 | ✗ 나쁨 | ✗ 불허 | 높음 | Sorted Set | 정확도 최우선, 소규모 | +| 이동 윈도 카운터 | ✓ 좋음 | ✗ 불허 | 높음 | String + Lua | 정확도 + 메모리 균형 | + +**알고리즘 선택 트리** + +``` +burst를 허용해야 하나? + YES → 토큰 버킷 + NO → 출력이 일정해야 하나? + YES → 누출 버킷 + NO → 메모리가 충분한가? + NO → 고정 윈도 카운터 (경계 문제 감수) + YES → 정확도가 중요한가? + HIGH → 이동 윈도 로깅 + MEDIUM → 이동 윈도 카운터 (권장) +``` + +> **본 설계 선택: 토큰 버킷** — burst traffic 허용, 구현 단순, 업계 표준 + +--- + +### 개략적 아키텍처 + +``` + ┌──────────────────────┐ + │ 규칙 저장소 │ + │ (YAML/Disk) │ + └──────────┬───────────┘ + │ (1) 주기적 로드 + ▼ +Client ──→ Rate Limiter ──────[Rule Cache] + │ (Worker Thread) + │ │ (2) 캐시에서 규칙 조회 + │ ┌────▼────┐ + │ │ Redis │ ← 카운터만 저장 + │ └─────────┘ + │ + ┌──────▼──────┐ + │ 한도 초과? │ + └──┬───────┬──┘ + YES│ │NO + ▼ ▼ + [선택 A] 즉시 429 [API Server] + [선택 B] 메시지 큐에 적재 +``` + +**역할 분리 원칙** +- 규칙 저장소: 디스크의 YAML 파일 (변경 빈도 낮음) +- Rule Cache: 워커 스레드가 주기적으로 갱신하는 메모리 캐시 (요청 처리 시 I/O 없음) +- Redis: 카운터만 저장, 규칙은 저장하지 않음 + +--- + +## 상세 설계 + +### 처리율 제한 규칙 + +Lyft ratelimit 오픈소스 형식을 참고한 YAML 규칙 예시 + +```yaml +domain: auth +descriptors: + # 로그인 시도: 분당 5건 + - key: user_login + rate_limit: + unit: minute + requests_per_unit: 5 + + # 계정 생성: IP당 하루 10건 + - key: account_create + descriptors: + - key: ip_address + rate_limit: + unit: day + requests_per_unit: 10 + + # 마케팅 SMS: 수신자당 하루 5건 + - key: message_type + value: marketing + descriptors: + - key: to_number + rate_limit: + unit: day + requests_per_unit: 5 +``` + +**규칙 설계 원칙** +- domain으로 논리적 그룹화 → 운영 편의 +- descriptors 중첩으로 복합 조건 표현 가능 +- 규칙 변경 → 디스크 파일만 수정 → 워커가 자동 갱신 + +--- + +### 제한 초과 응답 처리 + +#### HTTP 응답 헤더 + +``` +HTTP/1.1 429 Too Many Requests +Content-Type: application/json + +X-RateLimit-Limit: 100 +X-RateLimit-Remaining: 0 +X-RateLimit-Reset: 1609459260 +X-RateLimit-Retry-After: 30 +``` + +| 헤더 | 의미 | 포함 시점 | +|------|------|-----------| +| `X-RateLimit-Limit` | 윈도당 최대 요청 수 | 모든 응답 | +| `X-RateLimit-Remaining` | 남은 요청 수 | 모든 응답 | +| `X-RateLimit-Reset` | 리셋 UTC 에포크 타임스탬프 | 모든 응답 | +| `X-RateLimit-Retry-After` | 재시도 가능 시간(초) | 429 응답에만 | + +> 정상 응답에도 `Limit`, `Remaining`, `Reset` 헤더를 포함해야 클라이언트가 사전에 대응 가능. + +#### 초과 요청 처리 방법 + +``` +초과 요청 도착 + │ + ┌───▼────────────────┐ + │ 즉시 버릴까? │──YES──→ 429 즉시 반환 + └───┬────────────────┘ + │NO (나중에 처리해도 되는 요청) + ▼ + 메시지 큐에 적재 → 나중에 재처리 + (배치 작업, 이메일 발송 등) +``` + +--- + +### 상세 처리 흐름 + +``` +요청 수신 + │ + ▼ +[1] 처리율 제한 규칙 확인 + (Rule Cache에서 조회, 캐시 미스 시 디스크 로드) + │ + ▼ +[2] Redis에서 카운터 조회 (Lua 스크립트로 원자적 실행) + ├── GET counter + ├── 임계치 초과? → YES → 429 반환 + 응답 헤더 설정 + │ NO → 계속 + ├── INCR counter + └── EXPIRE counter (첫 요청인 경우) + │ + ▼ +[3] 요청을 API 서버로 포워딩 + │ + ▼ +[4] 응답 헤더에 현재 사용량 추가 후 클라이언트 반환 +``` + +--- + +### 분산 환경에서의 문제와 해결 + +#### 경쟁 조건 (Race Condition) + +**문제 재현** + +``` +임계치 = 5, 현재 counter = 4 + +Thread A: GET counter → 4 (읽기) +Thread B: GET counter → 4 (동시에 읽기) +Thread A: 4 < 5 → 허용, SET counter 5 +Thread B: 4 < 5 → 허용, SET counter 5 ← 실제론 6이어야 하는데 5로 기록됨 +→ 두 요청 모두 허용됨 (의도한 동작 아님) +``` + +**INCR만으로 부족한 이유** + +`INCR`는 원자적이지만 "읽기 → 판단 → 증가"의 전체 흐름은 원자적이지 않다. + +``` +# 올바른 순서이지만 원자성 없음 +1. GET counter → 4 +2. 4 < 5이므로 허용 결정 +3. INCR counter → 5 +← 2번과 3번 사이에 다른 스레드가 끼어들 수 있음 +``` + +**INCR + EXPIRE 사이의 크래시 문제** + +``` +INCR counter → 1 (키 생성) +─── 서버 크래시 ─── +EXPIRE counter 60 → (실행 안 됨) +→ counter 키가 TTL 없이 영구 존재 → 카운터가 리셋되지 않음! +``` + +**해결책: Lua 스크립트** + +Lua 스크립트는 Redis 서버에서 단일 명령처럼 원자적으로 실행된다. + +```lua +-- 토큰 버킷: Lua로 원자적 실행 +-- KEYS[1]: Redis 키 (예: "rate:user:42:60") +-- ARGV[1]: bucket_size (최대 토큰 수) +-- ARGV[2]: TTL (초) + +local key = KEYS[1] +local capacity = tonumber(ARGV[1]) +local ttl = tonumber(ARGV[2]) + +local tokens = tonumber(redis.call('GET', key)) + +if tokens == nil then + -- 키가 없으면 (첫 요청) 버킷 초기화 + tokens = capacity +end + +if tokens < 1 then + return 0 -- 토큰 없음 → 거부 +end + +-- 원자적으로 토큰 차감 및 TTL 설정 +redis.call('SET', key, tokens - 1) +redis.call('EXPIRE', key, ttl) +return 1 -- 허용 +``` + +**이동 윈도 로깅 알고리즘이라면 → Sorted Set** + +```lua +-- Sorted Set으로 이동 윈도 로깅 구현 (Lua로 원자적 실행) +local key = KEYS[1] +local now = tonumber(ARGV[1]) +local window = tonumber(ARGV[2]) +local limit = tonumber(ARGV[3]) +local request_id = ARGV[4] + +-- 만료된 항목 제거 +redis.call('ZREMRANGEBYSCORE', key, 0, now - window) + +-- 현재 요청 수 확인 +local count = redis.call('ZCARD', key) + +if count >= limit then + return 0 -- 거부 +end + +-- 새 요청 기록 +redis.call('ZADD', key, now, request_id) +redis.call('EXPIRE', key, window) +return 1 -- 허용 +``` + +**MULTI/EXEC vs Lua 스크립트** + +| | MULTI/EXEC | Lua 스크립트 | +|--|-----------|------------| +| 원자성 | 불완전 (다른 클라이언트가 중간 읽기 가능) | 완전한 원자성 | +| 조건부 실행 | 불가 (WATCH + 재시도로 우회) | if/else 가능 | +| 네트워크 왕복 | 2회 이상 | 1회 | +| **권장 여부** | △ | **✓ 권장** | + +--- + +#### 동기화 (Synchronization) + +여러 처리율 제한 서버가 각자 다른 카운터를 보면 제한이 무의미해진다. + +``` +사용자가 분당 10건 제한, 서버 2대에 각각 5건 요청 +→ 서버 A: "5건 미만 → 허용" +→ 서버 B: "5건 미만 → 허용" +→ 실제 10건 처리 (제한 무력화) +``` + +**해결책 비교** + +``` +[고정 세션 (Sticky Session)] [중앙 집중형 저장소] +Client A → 서버 A (고정) Client A ─┐ +Client B → 서버 B (고정) ├──→ [Redis Cluster] + Client B ─┘ + +장점: 구현 단순 장점: 수평 확장, 일관성 보장 +단점: 확장성 없음, 서버 장애 시 유실 단점: Redis Cluster 운영 필요 +``` + +> **선택: 중앙 집중형 저장소 (Redis Cluster)** +> Redis Cluster는 최소 3개의 마스터 노드로 구성, 단일 장애점 제거. + +--- + +**Redis 키 설계** + +``` +rate:{domain}:{dimension}:{identifier}:{window_start} + +예시: +rate:auth:user:42:1609459200 ← 사용자 42, 특정 분 윈도 +rate:auth:ip:192.168.1.1:1609459200 ← IP 기반 카운터 +rate:post:global:1609459200 ← 전역 카운터 + +설계 원칙: +- TTL은 반드시 window_size와 동일하게 설정 +- identifier는 해싱 처리 (개인정보 보호) +- 멀티 기준 제한 시 키를 분리하여 독립적으로 관리 +``` + +--- + +### 성능 최적화 + +#### 로컬 캐시 + 비동기 동기화 + +Redis 왕복을 줄여 지연 시간을 최소화하는 전략. + +``` +요청 → Rate Limiter + │ + ▼ + [로컬 인메모리 카운터] ← 빠름 (네트워크 없음) + │ + │ 비동기 주기적 동기화 (예: 100ms마다) + ▼ + [중앙 Redis Cluster] + +장점: 지연 시간 대폭 감소 +단점: 동기화 지연 동안 임계치를 약간 초과할 수 있음 + → Soft Limit 허용 가능한 API에만 적용 +``` + +#### 멀티 데이터센터 + +``` +사용자 (서울) 사용자 (뉴욕) + │ │ + ▼ ▼ + Seoul Edge Node NY Edge Node + (로컬 Redis + 캐시) (로컬 Redis + 캐시) + │ │ + └────────────┬─────────────┘ + ▼ + [글로벌 Redis Cluster] + (최종 일관성 모델) +``` + +- 사용자와 가까운 에지 서버에서 처리율 제한 → 지연 최소화 +- 에지 서버 간 데이터 동기화: **최종 일관성 (Eventual Consistency)** +- 수백 ms의 동기화 지연 동안 임계치를 약간 초과할 수 있음 (허용 범위) +- 보안 민감 API (결제, 인증)는 글로벌 Redis에 직접 접근하여 강한 일관성 보장 + +--- + +### 모니터링 + +**핵심 메트릭** + +| 메트릭 | 설명 | 알람 임계치 | +|--------|------|------------| +| `rate_limited_requests_total` | 429 반환 요청 수 | 급증 시 규칙 재검토 | +| `rate_limit_hit_ratio` | 전체 요청 대비 제한 비율 | 5% 초과 | +| `redis_op_latency_p99` | Redis 작업 99th percentile 지연 | 5ms 초과 | +| `rule_cache_miss_rate` | 규칙 캐시 미스율 | 1% 초과 | + +**운영 시나리오** + +``` +[시나리오 1] 429 비율 갑자기 급증 +→ 정상 트래픽 패턴이 변했나? or 임계치가 너무 낮나? +→ 조치: 규칙 임계치 조정 (YAML 수정 → 워커 자동 갱신) + +[시나리오 2] 특정 IP에 429 집중 +→ DDoS 가능성 +→ 조치: IP 차단 규칙 추가, WAF 연동 + +[시나리오 3] 알고리즘 교체 후 +→ burst 패턴 허용 여부가 바뀌므로 429 비율 변동 가능 +→ 교체 전후 메트릭 비교 필수 +``` + +--- + +## 마무리 및 추가 논의 + +### 경성 vs. 연성 처리율 제한 + +| 구분 | 정의 | 사용 사례 | +|------|------|-----------| +| 경성 (Hard) | 임계치를 절대 초과할 수 없음 | 결제 API, 인증 API | +| 연성 (Soft) | 짧은 시간 동안 임계치를 약간 초과 허용 | 일반 콘텐츠 조회 | + +### 처리율 제한 장치의 고가용성 + +처리율 제한 장치 자체가 다운되면 어떻게 할 것인가? + +``` +[Fail Open] [Fail Closed] +장치 장애 시 모든 요청 허용 장치 장애 시 모든 요청 차단 + +장점: 서비스 가용성 유지 장점: 보안 정책 유지 +단점: 장애 시간 동안 DDoS 취약 단점: 서비스 다운타임 발생 + +→ 대부분의 서비스: Fail Open +→ 금융/보안 서비스: Fail Closed 검토 +``` + +### 내부 서비스 Bypass + +내부 서비스 간 호출, 관리자 API는 처리율 제한을 우회할 수 있어야 한다. + +```yaml +bypass_rules: + - key: service_name + value: internal-payment-service + action: bypass + + - key: user_role + value: admin + action: bypass +``` + +### 요청 비용 차등 적용 (Weighted Rate Limiting) + +요청마다 소비하는 토큰 수를 다르게 설정. + +``` +일반 GET 요청 → 토큰 1개 소비 +대용량 파일 업로드 → 토큰 10개 소비 +AI 추론 API 호출 → 토큰 50개 소비 + +→ 토큰 버킷에서 자연스럽게 표현 가능 +→ API 비용과 부하를 공정하게 제어 +``` + +### OSI 계층별 처리율 제한 + +``` +Layer 7 (Application) ─ HTTP URL, 헤더, 사용자 ID 기반 ← 본 설계 +Layer 4 (Transport) ─ IP + TCP 포트 기반 +Layer 3 (Network) ─ IP 주소 기반 (iptables 등) +``` + +### 클라이언트 설계 가이드 (429 대응) + +``` +✓ 캐시 활용 → API 호출 횟수 자체를 줄이기 +✓ 요청을 짧은 시간에 집중하지 않기 (분산) +✓ 429 수신 시 Exponential Backoff 적용 + + 1차 실패 → 1초 대기 + 2차 실패 → 2초 대기 + 3차 실패 → 4초 대기 + n차 실패 → min(2^n, 최대대기시간)초 대기 + +✓ X-RateLimit-Remaining 헤더를 사전에 모니터링 + → 0에 가까워지면 요청 속도를 줄이는 로직 구현 +``` + +--- + +## 최종 아키텍처 + +``` + ┌──────────────────┐ + │ 규칙 파일 │ + │ (YAML / Disk) │ + └────────┬─────────┘ + │ 주기적 로드 (Worker Thread) + ▼ +Client → [Load Balancer] → [Rate Limiter Middleware] + │ + ┌──────────┼──────────┐ + │ │ │ + [Rule Cache] [Redis ] [Message] + (메모리) [Cluster ] [Queue ] + │ + [한도 초과?] + ┌─────┴─────┐ + YES NO + │ │ + 429 반환 API Server + + 응답 헤더 +``` + +**설계 요약** + +| 결정 항목 | 선택 | 이유 | +|-----------|------|------| +| 위치 | API Gateway | 중앙화, 언어 무관 | +| 알고리즘 | 토큰 버킷 | burst 허용, 단순, 업계 표준 | +| 저장소 | Redis Cluster | 인메모리 속도, TTL 지원, HA | +| 원자성 | Lua 스크립트 | 완전한 원자적 실행 | +| 규칙 관리 | YAML + Worker 캐시 | 핫리로드, I/O 분리 | +| 동기화 | 중앙 집중형 Redis | 일관성 보장, 수평 확장 | +| 다중 데이터센터 | 최종 일관성 | 지연 최소화, 실용적 | From 4b1c0f237796523046d490b91d696998b52bbb6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=9D=B8=EC=B2=A0?= Date: Sat, 4 Jul 2026 00:54:44 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[5=EC=9E=A5]=20=EC=95=88=EC=A0=95=20?= =?UTF-8?q?=ED=95=B4=EC=8B=9C=20=EC=84=A4=EA=B3=84=201=EC=B0=A8=20?= =?UTF-8?q?=EC=9E=90=EB=A3=8C=20=EC=B6=94=EA=B0=80=20=E2=80=94=20=EC=A0=95?= =?UTF-8?q?=EC=9D=B8=EC=B2=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\354\204\244\352\263\204_1\354\260\250.md" | 366 ++++++++++++++++++ 1 file changed, 366 insertions(+) create mode 100644 "05-\354\225\210\354\240\225 \355\225\264\354\213\234/\354\240\225\354\235\270\354\262\240/01_\354\225\210\354\240\225_\355\225\264\354\213\234_\354\204\244\352\263\204_1\354\260\250.md" diff --git "a/05-\354\225\210\354\240\225 \355\225\264\354\213\234/\354\240\225\354\235\270\354\262\240/01_\354\225\210\354\240\225_\355\225\264\354\213\234_\354\204\244\352\263\204_1\354\260\250.md" "b/05-\354\225\210\354\240\225 \355\225\264\354\213\234/\354\240\225\354\235\270\354\262\240/01_\354\225\210\354\240\225_\355\225\264\354\213\234_\354\204\244\352\263\204_1\354\260\250.md" new file mode 100644 index 0000000..e8a0dc7 --- /dev/null +++ "b/05-\354\225\210\354\240\225 \355\225\264\354\213\234/\354\240\225\354\235\270\354\262\240/01_\354\225\210\354\240\225_\355\225\264\354\213\234_\354\204\244\352\263\204_1\354\260\250.md" @@ -0,0 +1,366 @@ +# 5장. 안정 해시 설계 (Consistent Hashing) + +--- + +## 문제 이해 및 설계 범위 확정하기 + +수평적 확장을 위해서는 요청 또는 데이터를 서버들에 균등하게 나누는 것이 중요하다. +이 장의 목표는 다음 두 가지를 동시에 만족하는 분배 방법을 찾는 것이다. + +**요구사항** + +- 데이터(키)를 서버들에 균등하게 분배 +- 서버가 추가/삭제될 때 **재배치되는 키를 최소화** +- 대규모 분산 캐시/스토리지 환경에서 동작 +- 특정 서버로 키가 쏠리는 핫스팟 방지 + +--- + +## 해시 키 재배치 문제 + +### 나머지 연산 방식 (hash % N) + +N개의 서버에 부하를 나누는 가장 단순한 방법. + +``` +인덱스 = hash(key) % N // N = 서버의 개수 +``` + +서버 풀의 크기가 고정되어 있고 데이터 분포가 균등하면 잘 동작한다. +문제는 **서버가 추가되거나 삭제되는 순간**이다. + +### 치명적 단점: 대규모 재배치 + +``` +서버 4대 → 3대로 축소되는 경우 + + key0: hash=18 → 18 % 4 = 2 (서버2) 18 % 3 = 0 (서버0) ← 이동 + key1: hash=26 → 26 % 4 = 2 (서버2) 26 % 3 = 2 (서버2) ← 유지 + key2: hash=35 → 35 % 4 = 3 (서버3) 35 % 3 = 2 (서버2) ← 이동 + key3: hash=50 → 50 % 4 = 2 (서버2) 50 % 3 = 2 (서버2) ← 유지 + ... + + → % 4와 % 3의 결과가 일치할 확률은 약 25% + → 즉 약 75%의 키가 재배치됨 +``` + +**캐시 환경에서의 장애 시나리오** + +``` +서버 1대 다운 + │ + ▼ +키의 대부분이 다른 서버로 재매핑 + │ + ▼ +대량 캐시 미스 발생 + │ + ▼ +요청이 전부 DB(원본 저장소)로 쏟아짐 + │ + ▼ +cache stampede → DB 과부하 → 연쇄 장애 +``` + +> 서버 1대가 죽었을 뿐인데 DB까지 함께 죽는 구조. +> 4장의 처리율 제한 장치가 "요청 폭주"를 막는 장치였다면, +> 이 장은 "노드 변경으로 인한 내부 폭주"를 막는 문제의식이다. + +--- + +## 안정 해시 (Consistent Hashing) + +해시 테이블의 크기가 조정될 때 평균적으로 **k/n개의 키만 재배치**하는 해시 기법. + +- k: 키의 개수 +- n: 슬롯의 개수 (실질적으로는 노드/서버 수로 이해) + +### 해시 링 (Hash Ring) + +해시 공간(0 ~ 2^160 - 1)의 양 끝을 구부려 맞닿게 하면 해시 링이 만들어진다. +링 위에 **서버의 위치**와 **데이터의 해시 키**를 함께 배치한다. + +``` + 0 (= 2^160) + │ + ┌───────┴───────┐ + │ │ + k3 ○ │ │ ● S0 + │ │ + S3 ● │ Hash │ ○ k0 + │ Ring │ + │ │ ● S1 + k2 ○ │ │ + └───────┬───────┘ ○ k1 + │ + S2 ● + + ● = 서버 노드, ○ = 데이터 키 +``` + +### 키 → 서버 조회 + +각 키는 자신의 위치에서 **시계 방향으로 탐색하여 처음 만나는 서버**에 저장된다. + +``` + k0 → 시계 방향 → S1 담당 + k1 → 시계 방향 → S2 담당 + k2 → 시계 방향 → S3 담당 + k3 → 시계 방향 → S0 담당 +``` + +### 서버 추가 + +``` +S0과 S1 사이에 S4 추가 + + 변경 전: ── S0 ──── k0 ──── S1 ── (k0은 S1 담당) + 변경 후: ── S0 ─ k0 ─ S4 ─── S1 ── (k0만 S4로 이동) + + → 새 서버 위치에서 반시계 방향으로 이전 서버(S0)를 만나기 전까지의 + 키들만 새 서버(S4)로 재배치 + → 나머지 키는 전부 그대로 +``` + +### 서버 제거 + +``` +S1 제거 + + 변경 전: ── S0 ── k0 ── S1 ── S2 ── (k0은 S1 담당) + 변경 후: ── S0 ── k0 ──────── S2 ── (k0만 S2로 이동) + + → 제거된 서버가 담당하던 키들만 시계 방향의 다음 서버로 재배치 +``` + +**핵심**: 영향 범위가 **인접 구간으로 국한**된다. % N처럼 전체가 뒤섞이지 않는다. + +--- + +### 기본 안정 해시의 두 가지 문제점 + +#### 1. 파티션 크기의 불균등 + +서버 추가/삭제가 반복되면 각 서버가 담당하는 링 구간(파티션)의 크기가 균등하게 유지되지 않는다. + +``` + S1이 제거되는 경우: + + ── S0 ──[ 작은 구간 ]── S1 ──[ 작은 구간 ]── S2 ── + │ + ▼ S1 제거 + ── S0 ──[ 매우 큰 구간 ]── S2 ── + + → S2의 담당 구간이 다른 서버 대비 2배로 커짐 +``` + +#### 2. 키의 불균등 분포 (쏠림) + +서버가 링 위에 몰려 배치되면 특정 서버로 키가 집중된다. + +``` + S0 ● ● S1 ● S2 ← 서버 3대가 한쪽에 몰림 + ┌──────────────┐ + │ │ + │ 나머지 │ ← 이 넓은 구간의 키 전부가 + │ 절반의 링 │ 시계 방향 첫 서버 하나에 쏠림 + │ │ + └──────────────┘ + + 서버 3대면 링 위의 점이 3개뿐 + → 운이 나쁘면 한 서버가 링의 절반을 담당 +``` + +--- + +## 가상 노드 (Virtual Node / Replica) + +위 두 문제를 해결하는 기법. 하나의 물리 서버를 링 위에 **여러 개의 가상 노드**로 배치한다. + +``` + 물리 서버 2대 (S0, S1), 서버당 가상 노드 3개 + + ── S0_vn0 ── S1_vn0 ── S0_vn1 ── S1_vn1 ── S0_vn2 ── S1_vn2 ── + + → 각 서버의 담당 구간이 링 전체에 잘게 분산됨 + → 조회 방식은 동일: 시계 방향 탐색, 처음 만난 가상 노드의 실제 서버에 저장 +``` + +### 왜 균등해지는가 + +가상 노드 수가 많아질수록 각 서버가 담당하는 구간 합이 통계적으로 평균에 수렴한다 (**대수의 법칙**, 표준편차 감소). + +``` + 가상 노드 수 키 분포의 표준편차 + ──────────── ───────────────── + 100개 평균 대비 약 10% + 200개 평균 대비 약 5% + + → 가상 노드 ↑ = 표준편차 ↓ = 분포 균등 +``` + +### Trade-off + +| 항목 | 내용 | +|------|------| +| 균등성 | 가상 노드 수 ↑ → 분포 균등 ↑ | +| 메모리 | 가상 노드 V개 저장 필요 → O(V) | +| 조회 비용 | 정렬 자료구조에서 이진 탐색 → O(log V) | +| 노드 증감 비용 | 서버 1대 추가/제거 시 V개의 점을 넣고 빼야 함 | +| 실무 타협점 | 서버당 **100~200개**가 일반적 | + +### 서버 제거 시 데이터는? + +링은 **라우팅 규칙**일 뿐, 데이터 자체를 보존하지 않는다. + +``` + 캐시 용도 → 캐시 미스 후 원본에서 재적재 (자연 회복) + 스토리지 용도 → 별도 복제(replication) 없으면 데이터 유실 + (Dynamo: 시계 방향 다음 N개 노드에 복제) +``` + +--- + +## 구현 (Java + TreeMap) + +핵심은 `ceilingEntry()` 두 줄이다. + +```java +public class ConsistentHashRouter { + + // 링 위의 위치(해시값) -> 실제 서버. 정렬된 맵이라 시계 방향 탐색 가능 + private final TreeMap ring = new TreeMap<>(); + private final int virtualNodeCount; // 서버당 가상 노드 개수 + + public ConsistentHashRouter(List servers, int virtualNodeCount) { + this.virtualNodeCount = virtualNodeCount; + servers.forEach(this::addServer); + } + + public void addServer(String server) { + // 물리 서버 하나를 가상 노드 여러 개로 링 위에 배치 + for (int i = 0; i < virtualNodeCount; i++) { + ring.put(hash(server + "#VN" + i), server); + } + } + + public void removeServer(String server) { + for (int i = 0; i < virtualNodeCount; i++) { + ring.remove(hash(server + "#VN" + i)); + } + } + + public String route(String key) { + long h = hash(key); + // ceilingEntry = 시계 방향으로 처음 만나는 가상 노드 + Map.Entry entry = ring.ceilingEntry(h); + // 없으면 링을 한 바퀴 돌아 처음(firstEntry)으로 + return (entry != null ? entry : ring.firstEntry()).getValue(); + } + + private long hash(String input) { + // String.hashCode()는 분포가 나쁨 → MurmurHash 등 균등 분포 해시 사용 + return Hashing.murmur3_128() + .hashString(input, StandardCharsets.UTF_8).asLong(); + } +} +``` + +**구현 포인트** + +``` +[1] TreeMap.ceilingEntry(h) = "시계 방향 탐색" +[2] null이면 firstEntry() = "링을 한 바퀴 도는" 처리 +[3] 해시 함수는 분포가 균등한 것 사용 (MurmurHash, MD5 등) + → String.hashCode()는 분포가 나빠 부적합 +``` + +--- + +## 방식 비교 + +| 방식 | 노드 변경 시 재배치 | 분포 균등성 | 메모리 | 구현 복잡도 | +|------|--------------------|------------|--------|------------| +| hash % N | ✗ 대부분 재배치 (~75%) | ✓ 균등 | ✓ 없음 | ✓ 매우 단순 | +| 안정 해시 (기본) | ✓ 평균 k/n | ✗ 쏠림 가능 | ✓ 노드 수만큼 | 보통 | +| 안정 해시 + 가상 노드 | ✓ 평균 k/n | ✓ 통계적 균등 | △ O(V) | 보통 | +| 고정 해시 슬롯 (Redis) | ✓ 슬롯 매핑만 이동 | ✓ 균등 | ✓ 슬롯 테이블 | 보통 | + +**선택 트리** + +``` +노드가 동적으로 추가/제거되는가? + NO → hash % N으로 충분 + YES → 노드 수를 미리 고정할 수 있는가? + YES → 고정 해시 슬롯 (Redis Cluster 방식) + NO → 안정 해시 + 가상 노드 +``` + +--- + +## 실무 적용 사례 + +| 시스템 | 적용 방식 | +|--------|----------| +| Amazon DynamoDB | 파티션 키 분산에 안정 해시 계열 사용 (Dynamo 논문이 원조) | +| Apache Cassandra | 클러스터 내 데이터 파티셔닝 | +| Nginx | `hash ... consistent` (ketama 알고리즘) | +| Envoy | Ring Hash / Maglev 로드 밸런싱 | +| Discord, Akamai CDN | 트래픽/콘텐츠 분산 | + +### Redis Cluster는 안정 해시가 아니다 + +``` +Redis Cluster의 방식: + + 슬롯 = CRC16(key) % 16384 ← 슬롯 개수를 16384로 "고정" + + ┌─────────────┐ 슬롯 0 ~ 5460 → 노드 A + │ 16384 슬롯 │ 슬롯 5461 ~ 10922 → 노드 B + └─────────────┘ 슬롯 10923 ~ 16383 → 노드 C + + 노드 추가 시: 키 재해싱 없이 "슬롯 → 노드 매핑"만 이동 +``` + +- 슬롯 개수를 고정해서 % 연산의 재배치 문제 자체를 회피한 **다른 접근** +- 안정 해시 대비 운영이 단순 (슬롯 단위 마이그레이션) + +> **4장과의 연결**: 처리율 제한 장치에서 카운터 저장소로 쓴 Redis Cluster가 +> 내부적으로 "카운터 키를 어느 노드에 둘 것인가"를 푸는 방식이 곧 이 장의 문제의식이다. + +--- + +## 마무리 및 추가 논의 + +### 안정 해시의 이점 정리 + +- 서버 추가/삭제 시 재배치되는 키 최소화 (평균 k/n) +- 데이터가 균등 분포되므로 수평적 규모 확장에 유리 +- 핫스팟(hotspot) 문제 완화 — 특정 키가 몰리는 서버 분산 + +### 스터디 논의 주제 + +``` +[논의 1] 가상 노드 개수 산정 +→ 서버당 100~200개라는 수치의 근거는? (균등성 vs 메모리의 실측 기반 타협) + +[논의 2] Redis Cluster의 선택 +→ 안정 해시 대신 고정 슬롯을 택한 이유는? +→ 운영 단순성(슬롯 단위 관리) vs 유연성(임의 노드 수) + +[논의 3] 캐시 vs 스토리지 +→ 스토리지 용도라면 복제 전략까지 필요 (Dynamo의 N-replica) +→ 어디까지가 라우팅의 책임이고 어디부터 복제의 책임인가 +``` + +--- + +## 설계 요약 + +| 결정 항목 | 선택 | 이유 | +|-----------|------|------| +| 분배 방식 | 안정 해시 | 노드 증감 시 재배치 최소화 (평균 k/n) | +| 균등성 보완 | 가상 노드 (서버당 100~200개) | 파티션 불균등·키 쏠림 해소 | +| 자료구조 | TreeMap (정렬 맵) | ceilingEntry로 시계 방향 탐색 O(log V) | +| 해시 함수 | MurmurHash 등 | 균등 분포 (hashCode 부적합) | +| 데이터 보존 | 캐시: 자연 회복 / 스토리지: 복제 필요 | 링은 라우팅 규칙일 뿐 | \ No newline at end of file From 5830278b70dd8f247f25b144413215732b323a9d Mon Sep 17 00:00:00 2001 From: incheol789 <48637681+incheol789@users.noreply.github.com> Date: Thu, 9 Jul 2026 21:37:07 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[5=EC=9E=A5]=20=EC=95=88=EC=A0=95=20?= =?UTF-8?q?=ED=95=B4=EC=8B=9C=20=ED=9A=8C=EC=82=AC=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=20=EC=9E=90=EB=A3=8C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=E2=80=94=20=EC=A0=95=EC=9D=B8=EC=B2=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...1\354\232\251_\353\266\204\354\204\235.md" | 336 ++++++++++++++++++ .../img.png" | Bin 0 -> 24438 bytes .../img_1.png" | Bin 0 -> 35990 bytes .../img_2.png" | Bin 0 -> 26934 bytes 4 files changed, 336 insertions(+) create mode 100644 "05-\354\225\210\354\240\225 \355\225\264\354\213\234/\354\240\225\354\235\270\354\262\240/02_\355\232\214\354\202\254_\354\240\201\354\232\251_\353\266\204\354\204\235.md" create mode 100644 "05-\354\225\210\354\240\225 \355\225\264\354\213\234/\354\240\225\354\235\270\354\262\240/img.png" create mode 100644 "05-\354\225\210\354\240\225 \355\225\264\354\213\234/\354\240\225\354\235\270\354\262\240/img_1.png" create mode 100644 "05-\354\225\210\354\240\225 \355\225\264\354\213\234/\354\240\225\354\235\270\354\262\240/img_2.png" diff --git "a/05-\354\225\210\354\240\225 \355\225\264\354\213\234/\354\240\225\354\235\270\354\262\240/02_\355\232\214\354\202\254_\354\240\201\354\232\251_\353\266\204\354\204\235.md" "b/05-\354\225\210\354\240\225 \355\225\264\354\213\234/\354\240\225\354\235\270\354\262\240/02_\355\232\214\354\202\254_\354\240\201\354\232\251_\353\266\204\354\204\235.md" new file mode 100644 index 0000000..7aed27e --- /dev/null +++ "b/05-\354\225\210\354\240\225 \355\225\264\354\213\234/\354\240\225\354\235\270\354\262\240/02_\355\232\214\354\202\254_\354\240\201\354\232\251_\353\266\204\354\204\235.md" @@ -0,0 +1,336 @@ +# 5장. 안정 해시 — 회사 시스템 적용 분석 + +> 팀 비즈니스 구조를 기반으로 우리 시스템에서 안정 해시가 필요한 지점을 분석. +> 구조: **현황 분석 → 적용 지점 도출 → 확인 체크리스트** + +--- + +## 전제: 우리 팀 시스템 현황 + +| 항목 | 내용 | +|------|------| +| 도메인 | 이동통신사 백오피스 | +| 팀 모듈 | 판매 / 재고 / 정산 / 기준정보(사용자관리) / RM(여신) / IF | +| 담당 파트 | **정산** — 대리점 퇴근 시 일일 정산, 관리자용 백오피스 운영/유지보수 | +| 인프라 | Redis(캐시), RabbitMQ(배치 트리거, 알림 방송, 주문 이벤트) | +| 사용자 | 대리점/상담원/내부 관리자 (일반 대중 트래픽 아님) | +| 순서 보장 요구 | 휴대폰 판매 시 **같은 주문의 이벤트는 순서대로** 처리되어야 함 | + +**트래픽 특성** (백오피스 + 정산 도메인) + + +![img.png](img.png) + +``` +사용자 = 전국 대리점 + 상담원 + 내부 관리자 → 동시 사용자 수천 명 수준 +캐시 조회: 수백 ~ 수천 QPS 수준 + +정산 도메인의 특성: + 평시에는 트래픽 낮음 + 대리점 퇴근/마감 시간대에 전국 대리점의 정산 요청이 "동시에" 몰림 + → 시간대 집중형(peak-concentrated) 트래픽 + +판매 도메인의 특성: + 평시 낮음, 신규 단말 출시일에 주문 이벤트 수십 배 폭증 가능 +``` + +![img_1.png](img_1.png) + +**정산 업무 흐름 (비즈니스 관점)** + +``` +일마감: 대리점 퇴근 시 + 당일 판매 건 대사 → 수납/입금 확인 → 재고 이동 반영 → 일마감 확정 + +월마감: 월말 + 일마감 데이터 집계 → 판매 수수료(리베이트) 정산 → 본사 지급 확정 + → 일마감이 하나라도 밀리면 월마감 전체가 지연되는 종속 구조 + +후속 연계 (IF): + 마감 확정 데이터는 본사 ERP 일배치로 전송 + → 배치 cut-off 시각 전에 마감이 끝나야 함 (사실상의 SLA) +``` + +![img_2.png](img_2.png) + +**피크가 겹치는 최악 시나리오** + +``` +월말 마감 + 신규 단말 출시일 + 프로모션(보조금 정책) 변경일이 겹치는 날 + → 판매 폭증 → 마감 대상 건수 폭증 → 마감 시간대 처리량이 평시의 수십 배 + → 단일 워커 구조의 한계가 드러나는 지점 +``` + +--- + +## 분석 1: Redis 캐시 — 안정 해시가 필요 없는 지점 + +### 분석 + +**Standalone 또는 Sentinel(HA용) 구성으로 판단. 키 분배 자체가 없으므로 안정 해시 미사용.** + +### 근거 + +1차 문서의 선택 트리에 대입: + +``` +노드가 동적으로 추가/제거되는가? + → NO (단일 노드로 충분한 규모) + → hash % N조차 필요 없음. 분배 문제 자체가 없음 +``` + +- 캐시 대상이 요금제, 단말기 정보, 기준정보(코드성 데이터) 등 → 단일 노드 메모리를 넘을 이유가 없음 +- 백오피스 QPS 수준에서 수평 샤딩의 동기가 없음 +- 필요한 것은 분배가 아니라 **가용성** → Sentinel로 해결 + +### 확장 시나리오 (만약 커진다면) + +- 캐시 데이터가 단일 노드 메모리 초과 시 → **Redis Cluster (고정 해시 슬롯)** 선택이 자연스러움 +- 안정 해시를 직접 구현할 일은 없음 (1차 문서: Redis Cluster는 안정 해시가 아니라 CRC16 % 16384 고정 슬롯) + +--- + +## 분석 2: RabbitMQ 배치 트리거 / 알림 방송 — 분배가 아니라 전달 + +### 분석 + +**direct(배치 트리거) + fanout(알림 방송) exchange로 판단. 안정 해시와 무관.** + +### 근거 + +| 용도 | 패턴 | 필요한 것 | +|------|------|----------| +| 배치 트리거 (정산 배치 등) | 지정된 워커에게 전달 | direct exchange | +| 알림 방송 | 구독자 전원에게 전달 | fanout exchange | + +- 메시지를 여러 노드에 "나눠서(partition)" 처리할 필요가 없음 +- "지정된 곳" 또는 "전부"에게 보내는 패턴 → 해시 기반 분배의 문제의식 자체가 없음 + +--- + +## 분석 3: 판매 주문 이벤트 — ★ 안정 해시 도입 포인트 (1) + +### 현재 구조 + +**주문 큐 1개 + 컨슈머 1개 (또는 prefetch=1 단일 처리)로 판단.** + +- 백오피스 평시 트래픽이면 단일 컨슈머로 충분 +- 큐 1개 + 컨슈머 1개 = 전역 순서 보장이 공짜로 됨 → 안정 해시가 필요 없었음 + +### 문제 시나리오: 신규 단말 출시일 + +``` +아이폰 발매일: 평시 주문 대비 수십 배 폭증 + │ + ▼ +컨슈머 1개로 처리 지연 → 개통 대기 적체 + │ + ▼ +컨슈머를 3개로 늘리면? (competing consumers) + + 주문A-접수 → 컨슈머1 + 주문A-개통 → 컨슈머2 ← 접수 처리 전에 개통이 먼저 끝날 수 있음 + 주문A-완료 → 컨슈머3 + + → 같은 주문의 순서 깨짐 ✗ +``` + +**딜레마**: 처리량을 늘리려면 컨슈머를 늘려야 하는데, 늘리는 순간 순서가 깨진다. + +### 해결: Consistent Hash Exchange (`x-consistent-hash`) + +RabbitMQ 공식 플러그인 `rabbitmq-consistent-hash-exchange` 사용. + +``` + x-consistent-hash exchange + (routing key = 주문번호 해싱) + │ + ┌────────────────┼────────────────┐ + ▼ ▼ ▼ + 큐 Q1 큐 Q2 큐 Q3 + (컨슈머1) (컨슈머2) (컨슈머3) + + 주문A의 모든 이벤트 → hash(주문A) → 항상 Q2 → 항상 컨슈머2 + 주문B의 모든 이벤트 → hash(주문B) → 항상 Q1 → 항상 컨슈머1 + + → 주문 간 병렬 처리 ✓ + 주문 내 순서 보장 ✓ +``` + +**1차 문서 개념과의 매핑** + +| 1차 문서 개념 | RabbitMQ에서의 대응 | +|--------------|---------------------| +| 링 위의 서버 노드 | 바인딩된 큐 | +| 데이터 키 | routing key (주문번호) | +| 가상 노드 개수 | 바인딩 키의 숫자 (가중치) | +| 노드 추가/제거 시 k/n만 재배치 | 큐 추가/제거 시 일부 주문번호만 다른 큐로 이동 | + +- 큐를 늘리거나 줄일 때 **일부 라우팅 키만 재배치** → 안정 해시의 핵심 이점 그대로 +- Kafka의 파티션 키(`hash(key) % partitions`)와 동일한 문제의식의 RabbitMQ 버전 + +--- + +## 분석 4: 정산 마감 처리 — ★ 안정 해시 도입 포인트 (2, 담당 파트) + +### 정산 도메인의 순서 보장 요구 + +정산은 대리점 단위로 **선행 처리가 끝나야 다음 단계가 가능한** 흐름이다. + +``` +대리점 X의 마감 흐름 (예시): + + 판매 내역 집계 → 재고 차감 반영 → 여신(RM) 한도 정산 → 정산 마감 확정 + + → 같은 대리점의 정산 이벤트는 순서대로 처리되어야 함 + → 서로 다른 대리점끼리는 순서 무관 (병렬 처리 가능) +``` + +### 순서가 깨지면 생기는 비즈니스 사고 + +| 순서 위반 | 비즈니스 결과 | +|-----------|--------------| +| 판매 집계 전에 마감 확정 | 마감 금액 ≠ 실제 판매액 → **수수료 오지급** → 재정산 + 대리점 클레임 | +| 재고 차감 전에 마감 확정 | 장부 재고 ≠ 실물 재고 → 재고 실사 불일치, 익일 판매 가능 수량 오류 | +| 여신 정산 전에 추가 판매 승인 | 여신 한도 초과 판매 → **미수금 리스크** (RM 통제 무력화) | +| 일마감 지연 → ERP cut-off 초과 | 본사 회계 반영 누락 → 월마감 지연, 재무 보고 차질 | + +> 순서 보장은 기술 요구사항이기 전에 **돈이 걸린 정합성 요구사항**이다. +> 백오피스는 트래픽은 작아도 건당 금액 정확성의 무게가 크다. + +### 문제 시나리오: 마감 시간대 집중 + +``` +전국 대리점이 퇴근 시간대에 동시에 정산 요청 + │ + ▼ +정산 처리 워커 1개 → 마감 시간대에 처리 적체 + │ + ▼ +워커를 늘리면 같은 대리점의 정산 단계가 뒤섞일 위험 + → 판매 주문 이벤트와 완전히 동일한 딜레마 +``` + +### 해결: routing key = 대리점 코드 + +``` + x-consistent-hash exchange + (routing key = 대리점 코드 해싱) + │ + ┌────────────────┼────────────────┐ + ▼ ▼ ▼ + 정산워커1 정산워커2 정산워커3 + + 대리점 X의 모든 정산 이벤트 → 항상 워커2 → 단계 순서 보장 ✓ + 대리점 간에는 병렬 처리 → 마감 시간대 처리량 확보 ✓ +``` + +- 분석 3과 같은 패턴, 키만 다름 (주문번호 → 대리점 코드) +- **마감 시간대에만 워커를 늘리고 이후 줄이는 운영**도 가능 + → 워커 증감 시 일부 대리점만 재배치되는 안정 해시의 이점이 그대로 적용됨 + +### 주의점 (모의면접 대비) + +``` +[1] 재배치 순간의 순서 보장 + 큐 추가 시 이동하는 키(주문번호/대리점 코드)에 in-flight 메시지가 있으면 + 잠깐 순서가 어긋날 수 있음 → 배포 타이밍 조절 or 드레인 후 확장 + +[2] 큐:컨슈머 = 1:1 유지 필요 + 한 큐에 컨슈머 여러 개 붙이면 순서 보장이 다시 깨짐 + +[3] 핫스팟 + 대형 대리점(판매량 많은 지점)의 이벤트가 몰려도 대리점 단위로는 분산 불가 + → 키 단위 순서 보장이 필요한 이상 감수해야 하는 트레이드오프 + → 1차 문서의 "핫스팟 키 문제"가 실무에서 나타나는 형태 +``` + +--- + +## 분석 5: 다른 모듈로의 확장 — 순서 보장 단위와 키 후보 + +같은 패턴(키 단위 순서 보장 + 병렬 처리)이 팀 내 다른 모듈에도 그대로 적용된다. +핵심은 **"무엇 단위로 순서를 보장해야 하는가" = routing key 선정**이다. + +| 모듈 | 순서 보장 단위 | routing key 후보 | 순서 위반 시 사고 | +|------|---------------|-----------------|------------------| +| 판매 | 주문 | 주문번호 | 접수 전 개통 처리 등 상태 꼬임 | +| 정산 | 대리점 | 대리점 코드 | 수수료 오지급, 마감 금액 불일치 | +| 재고 | 단말 1대 | IMEI(시리얼) | 입고 전 판매, 이중 판매, 이동 이력 꼬임 | +| RM(여신) | 대리점 | 대리점 코드 | 한도 차감/복원 순서 꼬임 → 한도 초과 판매 | +| IF | 대상 외부 시스템 | 시스템 코드 (또는 전문 순번) | 외부 시스템에 역순 전문 도달 → 재전송 협의 비용 | + +**키 선정의 원칙** (1차 문서와 연결) + +``` +키 범위가 좁을수록 (IMEI > 주문번호 > 대리점 코드) + → 병렬성 ↑, 핫스팟 위험 ↓ +키 범위가 넓을수록 (대리점 코드) + → 순서 보장 범위 ↑, 대형 대리점 핫스팟 위험 ↑ + +→ "순서를 보장해야 하는 최소 단위"를 키로 잡는 것이 원칙 + (예: 정산은 대리점 간 순서가 무관하므로 대리점 코드면 충분) +``` + +--- + +## 도입 판단 기준 — 언제 도입할 것인가 + +지금 당장 도입하지 않는 이유와, 도입을 트리거할 지표를 함께 정의한다. + +**지금 도입하지 않는 이유 (비용 관점)** + +- 단일 컨슈머로 SLA(ERP cut-off 전 마감 완료)를 지키고 있다면, 플러그인 도입·큐 토폴로지 변경·운영 러닝커브는 순수 비용 +- 순서 보장이 "구조적으로 공짜"인 현재 방식(단일 큐)이 가장 단순하고 사고 확률이 낮음 + +**도입 트리거 지표 (운영 중 모니터링 대상)** + +``` +[1] 마감 완료 시각이 ERP cut-off에 근접 (여유 시간 지속 감소 추세) +[2] 마감 시간대 큐 적체(depth) 증가 추세 / 처리 지연 민원 발생 +[3] 출시일·월말에 마감 지연 이력 발생 +[4] 대리점 수 증가 계획 (신규 채널 확대 등) → 마감 대상 자체가 증가 + +→ [1]~[2]가 관측되는 시점 = x-consistent-hash 도입 검토 시작점 +``` + +**내가 생각한 모의면접 예상 질문..? -> "그 정도면 Kafka로 가야 하지 않나?"** + +``` +답변 포인트: + - Kafka 파티션 키도 동일한 문제의식 (키 단위 순서 + 파티션 병렬) + - 그러나 우리 트래픽은 백오피스 규모 → Kafka 클러스터 신규 운영 비용이 과함 + - 이미 운영 중인 RabbitMQ에 플러그인 하나로 동일 효과 + → "기술 선택은 문제 크기에 비례해야 한다"는 트레이드오프 논리 +``` + +--- + +## 종합: 선택 트리 대입 결과 + +| 시스템 | 분배 필요? | 현재 방식 | 안정 해시 관련성 | +|--------|-----------|----------|-----------------| +| Redis 캐시 | ✗ | Standalone/Sentinel | 불필요 (규모 미달) | +| MQ 배치/방송 | ✗ | direct/fanout | 무관 (분배 아닌 전달) | +| MQ 판매 주문 이벤트 | 현재 ✗ → 확장 시 ✓ | 단일 큐+단일 컨슈머 | 확장 시 x-consistent-hash (키=주문번호) | +| 정산 마감 처리 | 현재 ✗ → 확장 시 ✓ | 단일 워커 | 확장 시 x-consistent-hash (키=대리점 코드) | + +**결론**: 현재 우리 시스템은 안정 해시가 필요 없는 규모다. +단, "키 단위 순서 보장 + 병렬 처리"를 동시에 만족해야 하는 두 지점 — +판매 주문 이벤트(출시일 폭증)와 정산 마감 처리(마감 시간대 집중) — 에서 +컨슈머/워커를 확장하는 순간 안정 해시(x-consistent-hash)가 필요해진다. +그리고 그 확장의 트리거는 기술 지표가 아니라 **비즈니스 지표**(ERP cut-off 여유 감소, +마감 지연 이력, 대리점 수 증가)로 판단한다. + +--- + +## 확인 체크리스트 (실제 시스템에서 무엇을 확인했는지) + +- [ ] `redis-cli cluster info` → cluster_enabled 확인 +- [ ] 앱 설정에서 Redis 연결 방식 확인 (standalone / sentinel / cluster) +- [ ] `rabbitmq-plugins list | grep consistent` → 플러그인 여부 +- [ ] Management UI에서 exchange 타입 전수 확인 +- [ ] 주문 이벤트 큐의 컨슈머 수, prefetch 설정 확인 +- [ ] 정산 배치/이벤트의 처리 구조 확인 (단일 워커인지, 대리점 단위 병렬인지) +- [ ] 출시일/마감 시간대 처리 지연 이력 확인 (있었다면 도입 근거 강화) +- [ ] ERP 일배치 cut-off 시각과 실제 마감 완료 시각의 여유(gap) 확인 → 도입 트리거 지표 [1] +- [ ] 수수료 재정산/마감 정정 건 발생 이력 확인 (순서·정합성 사고의 실측 근거) diff --git "a/05-\354\225\210\354\240\225 \355\225\264\354\213\234/\354\240\225\354\235\270\354\262\240/img.png" "b/05-\354\225\210\354\240\225 \355\225\264\354\213\234/\354\240\225\354\235\270\354\262\240/img.png" new file mode 100644 index 0000000000000000000000000000000000000000..6f00bf0b879fdff751eb86b75f1c4c21dbe1608d GIT binary patch literal 24438 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 literal 0 HcmV?d00001 diff --git "a/05-\354\225\210\354\240\225 \355\225\264\354\213\234/\354\240\225\354\235\270\354\262\240/img_1.png" "b/05-\354\225\210\354\240\225 \355\225\264\354\213\234/\354\240\225\354\235\270\354\262\240/img_1.png" new file mode 100644 index 0000000000000000000000000000000000000000..31ebcf99a2d830201edc9e31fbbc108307fd6411 GIT binary patch literal 35990 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 DI;auC literal 0 HcmV?d00001 diff --git "a/05-\354\225\210\354\240\225 \355\225\264\354\213\234/\354\240\225\354\235\270\354\262\240/img_2.png" "b/05-\354\225\210\354\240\225 \355\225\264\354\213\234/\354\240\225\354\235\270\354\262\240/img_2.png" new file mode 100644 index 0000000000000000000000000000000000000000..f7675b2c727f91a48eebaaa625a53f23754ee7fc GIT binary patch literal 26934 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