Skip to content
Open
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
174 changes: 174 additions & 0 deletions ch04-처리율-제한/윤유탁.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,177 @@
2. **fail-open의 타이밍이 하필.** Redis가 DoS 공격 한복판에서 죽으면? fail-open이라 다 통과시키는데 — 보호가 가장 필요한 순간에 보호가 꺼짐. 로컬 폴백 카운터가 있긴 한데 그게 완벽히 메워주나? 평상시엔 가용성 위해 fail-open이 맞는데, 공격 중에는 그 선택이 정확히 독이 되는 역설. 상태에 따라 fail 정책을 바꿔야 하나?

3. **로컬 폴백 카운터의 함정.** Redis 죽어서 게이트웨이마다 자기 로컬 카운터로 제한하면, 게이트웨이가 10대일 때 "분당 5회" 제한이 사용자한텐 실제로 분당 50회로 보임(각 게이트웨이가 자기 것만 세니까). 즉 폴백이 "대략적 보호 유지"라지만 실효 한도가 게이트웨이 수만큼 뻥튀기됨. 부정확한 정도가 생각보다 큼.

---
---

# 2차 보완 (책 4장 학습 후)

> 1차 설계를 책 해설과 대조하고 보완. 핵심 발견: 이번엔 "내가 막힌 데 = 책이 깊게 판 데"가 아니라 **그 반대**였음. 1차에서 찜찜하다고 남긴 토론거리들이 정작 책은 안 푼 지점이었음.

## 한 줄 요약

1차는 책의 **이동 윈도 카운터** 노선을 거의 그대로 재발명했고(공식·헤더 이름까지 일치), 1차에서 "자신 없음/토론하고 싶음"으로 남긴 3가지(Redis 핫 키 / fail-open 역설 / 폴백 뻥튀기)는 **책이 다루지 않은, 1차가 책보다 깊었던 지점**이었음.

---

## 1. 1차가 맞춘 것 (책 용어로 매핑 — 모르고 재발명한 것 포함)

거의 다 맞췄음. 책 용어로 매핑하면:

| 1차에서 내가 한 것 | 책 용어 / 위치 | 비고 |
|---|---|---|
| 슬라이딩 윈도우, 추정식 `현재 + 직전×겹침비율` | **이동 윈도 카운터(sliding window counter)** | 공식이 글자 그대로 일치. 모르고 재발명 |
| "정확히 슬라이딩하려면 타임스탬프 다 들고 있어야 함 → 메모리 폭발" | **이동 윈도 로그(sliding window log)의 단점** | 책도 이 알고리즘을 거쳐 카운터로 넘어감. 전개 순서까지 똑같음 |
| 균등분포 가정 오차 감수 | 책: "직전 시간대 균등분포 가정 → 다소 느슨" | 책은 + Cloudflare 40억 요청 중 0.003%만 오차라는 실측 제시 |
| 읽기-검사-증가를 "한 덩어리 스크립트로 통째 실행" | **Lua script** (락 대안) | 이름만 몰랐지 개념 정확. 책은 + sorted set도 제시 |
| 카운터 읽기-증가 원자성 = INCR 류 | 책: Redis **INCR / EXPIRE** | TTL 자동 만료도 1차에서 맞춤 |
| sticky session "확장성·유연성 나빠 비추" | 책: 거의 같은 문장 | 중앙 Redis로 동기화도 동일 |
| `429` + `X-Ratelimit-Limit/Remaining/Retry-After` | 책: 헤더 3개 이름까지 일치 | 큐 보관 옵션(주문 예시)도 동일 |
| 규칙 원본 + 게이트웨이 로컬 캐시 + 워커 주기 갱신 | 책: "규칙은 디스크 설정파일, 워커가 수시로 읽어 캐시" | 거의 동일. **차이는 아래 2번 참고** |

특히 **이동 윈도 카운터 공식을 책 안 보고 도출한 것**, **읽기-검사-증가를 스크립트로 통째 보낸 게 Lua script였던 것**이 가장 정확하게 맞은 지점. 그리고 **버킷 개수를 튜닝 다이얼로 일반화한 건 책보다 추상화가 한 단계 위**였음 — 책은 이동 윈도 카운터를 2버킷 고정으로만 설명함.

규칙 저장의 사소한 차이: 나는 원본을 **Redis**에 뒀는데, 책은 원본을 **디스크의 설정 파일(YAML, Lyft ratelimit 예시)** 에 둠. 둘 다 "원본 따로 + 로컬 캐시 + 워커 갱신" 구조는 같음. 어차피 규칙은 읽기 폭발·쓰기 드묾이라 원본이 디스크든 Redis든 핫 패스엔 영향 없음.

---

## 2. 1차에 아예 없던 거 (책이 추가한 것)

- **알고리즘 4종을 안 봄** — 토큰 버킷, 누출 버킷, 고정 윈도 카운터, 이동 윈도 로그. 나는 이동 윈도 카운터로 직행함. 단, **1차에서 "버스트 스무딩 요구 생기면 다른 알고리즘 계열 검토"라고 예고한 게 정확히 토큰/누출 버킷**이었고, 책도 마무리에서 "깜짝 세일로 트래픽 급증 시엔 토큰 버킷이 적합"이라 함 → 예고가 적중. 알고리즘별 핵심:
- **토큰 버킷**: 버킷에 토큰 주기적 충전, 요청마다 1개 소비. 남은 토큰 있으면 버스트도 통과 → **짧게 몰리는 트래픽 처리에 강함.** (아마존·스트라이프)
- **누출 버킷**: FIFO 큐, **고정 처리율로 흘려보냄** → 안정적 출력이 필요할 때. 대신 버스트 때 오래된 요청이 쌓여 최신 요청이 버려짐. (쇼피파이)
- **고정 윈도 카운터**: 1차에서 내가 "윈도 경계 버스트로 2배 통과" 문제로 버린 그 방식. 책도 같은 단점 지적.
- **성능 최적화** — (1) 에지 서버: 여러 데이터센터에서 멀리 떨어진 사용자 지연 줄임. (2) **최종 일관성 모델로 제한 장치 간 동기화** ← 이게 내 토론거리 1번이랑 정면으로 연결됨(4번 참고).
- **모니터링** — 채택한 알고리즘·규칙이 효과적인지 데이터를 모아 확인. 규칙이 빡빡하면 완화, 트래픽 패턴 바뀌면 알고리즘 교체. 1차에 없던 운영 관점.
- **다양한 계층의 제한** — 나는 L7(HTTP)만 봄. 책은 L3(IP, iptables)에서도 제한 가능하다고 언급.
- **제한 회피하는 클라이언트 설계** — 클라 캐시로 호출 줄이기, 백오프 둔 재시도 등. 막는 쪽뿐 아니라 걸리는 쪽 설계도 언급.

---

## 3. 책도 답 안 준, 1차의 날카로웠던 지점

1차 끝에 남긴 토론거리 3개를 책이 어떻게 다루나 봤더니 — **거의 다 책이 안 다룬 지점이었음.** 이게 이번 챕터의 진짜 수확.

1. **Redis 핫 키 병목** ("같은 사용자/IP는 같은 키라 한 노드에 쏠림, 게이트웨이만 늘려선 안 풀림") → **책은 정면으로 안 다룸.** 성능 최적화에서 에지 서버(지연)와 최종 일관성(동기화 부담)만 건드리지, 키 샤딩/핫 키는 침묵. 책보다 내가 깊었음.

2. **fail-open이 DoS 한복판에 보호를 끄는 역설** → 제일 큰 발견. **책은 요구사항에 "높은 결함 감내성"을 적어놓고, 본문에서 Redis가 죽으면 어떻게 되는지를 사실상 안 다룸.** fail-open/closed 판단도, graceful degradation도 본문엔 없음. 책이 결함 감내성을 짚는 건 마무리의 **경성/연성(hard/soft) 제한** 한 줄뿐 — 근데 이게 보완의 열쇠가 됨(아래 5번).

3. **로컬 폴백 카운터 한도 뻥튀기**(게이트웨이 N대 → 실효 한도 N배) → 책 안 다룸. 내 고유 발견.

---

## 4. 보완: Redis 핫 키 — 강한 일관성(내 선택) vs 최종 일관성(책)

토론거리 1번에 대해 책이 던지는 해법은 **최종 일관성(eventual consistency) 모델로 제한 장치 간 동기화**. 이게 내 1차 선택과 정면으로 트레이드오프가 갈림. **내 결정이 틀린 게 아니라, 무엇을 1순위로 보느냐의 축이 다른 것.**

| | 내 1차 (강한 일관성) | 책 (최종 일관성) |
|---|---|---|
| 방식 | 매 요청 Redis 원자 INCR, 한 곳에서 강하게 셈 | 노드가 로컬에서 세고 비동기로 느슨하게 맞춤 |
| **1순위로 보는 것** | **정확성** (요구사항 1번) | **규모 / 지연** |
| 한도 | 정확히 지켜짐 | 살짝 샐 수 있음 |
| 대가 | 모든 요청이 Redis를 때림 → **핫 키 병목** | 정확성 일부 포기 (어차피 오차 0.003%급) |

**판단: 책의 교환을 받아들임.** 대부분 도메인에서 한도가 1~2개 새는 건 무해하고, 그 대가로 핫 키 병목이 풀림. 단 이건 알고리즘 2번에서 "정확도 vs 메모리를 도메인이 고르게"라고 한 거랑 같은 결: **정확성이 진짜 치명적인 도메인(과금·결제 한도)이면 강한 일관성을 유지하고 핫 키는 키 샤딩으로 따로 푼다.** = 일관성 강도를 도메인이 고르는 다이얼.

(주의: 이 "느슨하게 센다"는 내 토론거리 3번 폴백 뻥튀기랑 표면이 비슷하지만 다름. 폴백은 Redis가 *죽었을 때* 어쩔 수 없이 노드별로 세는 장애 상황이고, 여기 최종 일관성은 *평상시* 의도적으로 동기화를 느슨하게 해 부하를 줄이는 정상 운영 전략임. 전자는 N배 뻥튀기가 사고, 후자는 동기화 주기를 짧게 잡아 오차를 통제함.)

---

## 5. 보완: fail-open 역설 — 경성/연성(hard/soft) 제한으로 일반화

토론거리 2번. 책의 **경성/연성 제한**이 내 fail 정책 고민과 사실 같은 한 축이었음:

- **경성(hard)**: 임계치를 절대 못 넘음 ≈ **fail-closed**(장치 죽으면 다 막아 임계치 사수)
- **연성(soft)**: 잠시는 넘어도 됨 ≈ **fail-open**(장치 죽으면 통과시켜 잠시 한도 초과 허용)

그럼 "평상시 vs DoS 중" 역설이 이렇게 번역됨:

> **fail 정책 = 경성/연성을 도메인(또는 상태)별로 고르는 다이얼.**
> - 가용성이 우선인 일반 API → 연성 = fail-open (장치 장애가 본체를 죽이면 안 됨, 요구사항 6번)
> - 정확성이 치명적인 도메인(결제 한도·인증 시도) → 경성 = fail-closed
> - "DoS 한복판" 같은 상태 변화 → 상태를 감지해 일시적으로 경성으로 전환하는 것도 가능

알고리즘 2번의 버킷 개수, 4번의 일관성 강도와 **완전히 같은 패턴**: 핵심 트레이드오프를 하나의 다이얼로 빼고 기본값만 안전하게 두는 것. 1차 땐 fail-open/closed를 "둘 중 뭐가 맞나"로만 봤는데, 책의 hard/soft 어휘를 얻고 나니 **둘은 한 스펙트럼의 양 끝이고 도메인이 고르는 것**으로 정리됨.

---

## 최종 구조

책 그림 4-13(상세 설계)을 기반으로, 내 2차 보완(최종 일관성 동기화 · Redis 클러스터 · fail 정책 다이얼 · 로컬 폴백)까지 얹은 구조. **굵은 화살표 = 핫 패스(매 요청), 점선 = 백그라운드/장애 경로.**

```mermaid
flowchart TD
C1["클라이언트 1"]
C2["클라이언트 2"]

subgraph GW["처리율 제한 게이트웨이 (여러 대 · 무상태)"]
M1["제한 미들웨어 1"]
M2["제한 미들웨어 2"]
end

C1 ==> M1
C2 ==> M2

subgraph RULE["제한 규칙 (읽기 폭발 · 쓰기 드묾)"]
RS[("규칙 원본<br/>YAML 설정파일 / Redis")]
W["워커: 주기적 갱신"]
LC["게이트웨이 로컬 캐시"]
RS -. 수시로 읽음 .-> W
W -. 갱신 .-> LC
end

M1 -. "규칙 조회 (왕복 0)" .-> LC
M2 -. "규칙 조회 (왕복 0)" .-> LC

subgraph REDIS["Redis 클러스터 (카운터 원본 · SPOF 완화)"]
R[("카운터<br/>INCR / EXPIRE<br/>Lua 원자 연산")]
end

M1 == "원자 카운트" ==> R
M2 == "원자 카운트" ==> R
M1 <-. "최종 일관성 동기화" .-> M2

M1 ==> D{"한도 초과?"}
D == "아니오 · 성공" ==> API["API 서버"]
D == "예 · 스로틀링" ==> RESP["429 Too Many Requests<br/>+ X-Ratelimit-Limit/Remaining/Retry-After"]
RESP --> O1["옵션 1: 요청 버림"]
RESP --> O2["옵션 2: 메시지 큐 보관"]

R -. "Redis 장애 시" .-> FAIL{"fail 정책 다이얼"}
FAIL -. "연성=fail-open · 일반 API" .-> FB["로컬 폴백 카운터<br/>(대략 보호, 한도 N배 주의)"]
FAIL -. "경성=fail-closed · 결제·인증" .-> BLK["전부 차단"]
```

> **REDIS를 클러스터로**, **미들웨어 간 최종 일관성 동기화**, **Redis 장애 시 fail 정책 다이얼 + 로컬 폴백**

---

## 6. 다음 설계 때 가져갈 교훈

1. **재발명은 나쁘지 않다.** 이동 윈도 카운터 공식·Lua script를 이름 모르고 도출함 — 요구사항에서 출발해 트레이드오프를 따지면 정설에 수렴함. 용어를 몰라도 추론이 맞으면 됨.
2. **내가 찜찜한 지점이 책보다 깊을 수도 있다.** 1차의 토론거리 3개가 책 미답 영역이었음. 책을 정답지로만 보지 말 것 — 책도 요구사항(결함 감내성)을 적어놓고 본문에서 안 푸는 구멍이 있음.
3. **반복되는 메타 패턴 = "핵심 트레이드오프를 다이얼로 빼고 기본값을 안전하게."** 버킷 개수(메모리↔정확도), 일관성 강도(정확성↔규모), fail 정책=hard/soft(정확성↔가용성) — 전부 같은 모양. 이걸 ch06 일관성 옵션 때부터 봤음. 앞으로 설계할 때 "이 결정도 다이얼로 뺄 수 있나?"를 먼저 물어볼 것.
4. **운영 관점(모니터링)을 1차에 빠뜨림.** 다음엔 "설치 후 효과를 어떻게 측정하나"를 처음부터 넣을 것.

---

## 요구사항 → 기술 최종 매핑표 (2차)

| 요구사항 | 1차 결정 | 2차 보완 |
|---|---|---|
| 1. 처리율 초과 정확히 제한 | 이동 윈도 카운터(2버킷) | 동일. 정확성 진짜 중요하면 버킷 잘게 / 강한 일관성 유지 |
| 2. 낮은 응답시간 | Redis 카운터 + 규칙 로컬 캐시 + 원자 연산 | 동일 + 에지 서버(지역 지연), Lua script로 원자성 |
| 3. 메모리 적게 | 버킷 카운터 + 2버킷 + TTL | 동일 (이동 윈도 로그 대신 카운터 = 책과 동일 선택) |
| 4. 분산형 | Redis 중앙집중 + 원자 연산 | + **최종 일관성으로 동기화(핫 키 병목 완화)** ← 일관성 강도는 도메인 다이얼 |
| 5. 예외 처리 | 429 + X-Ratelimit-* 헤더 + 큐 옵션 | 동일 (책과 헤더까지 일치) |
| 6. 높은 결함 감내성 | fail-open + 로컬 폴백 + Redis 클러스터 | + **fail 정책 = 경성/연성 다이얼**(도메인/상태별), 모니터링으로 효과 측정 |
| 위치 | 서버 앞단 미들웨어/게이트웨이 | 동일 (= 책의 API 게이트웨이) |
| 유연한 규칙 | 규칙 원본 + 로컬 캐시 주기 갱신 | 동일. 책은 원본을 디스크 YAML에, 나는 Redis에 (핫패스 영향 없음) |

---

## 의문점

- **핫 키 본질은 안 풀림.** 최종 일관성으로 동기화 부하는 줄여도, "같은 키는 한 노드"라는 샤딩의 본질적 핫 키는 그대로. 강한 일관성이 필요한 도메인에선 결국 키를 더 잘게 쪼개거나(`user:123:shard:0~9` 후 합산) 별도 처리해야 할 듯. 책은 여기까지 안 감.
- **상태 기반 fail 전환의 트리거.** "DoS 감지 시 경성으로 전환"이 말은 쉬운데, 무엇을 보고 공격이라 판단하나? 그 판단 자체가 또 다른 처리율 측정이라 순환 같음. 이건 더 파볼 거리.