728x90
API 스로틀링(Throttling)은 API 요청에 속도와 횟수를 제한하는 것을 말한다.
예를 들어, 한 사용자가 1초에 100번 API를 호출하려고 한다면, 서버에 과부하가 걸릴 수 있다. 이런 상황을 방지하기 위해 "초당 10번까지만 허용" 같은 식으로 제한을 두는 것이 API 스로틀링이다.
보통 속도 제한(Rate Limit)과 유사하지만, 세부적으로는 이렇게 동작한다.
- Rate Limit: "10분에 100번 호출 가능" → 초과 시 오류(429 Too Many Requests) 반환.
- Throttle: 요청이 초과되면 아예 거부하지 않고 지연시켜서 처리하거나 일정 간격으로만 실행.
즉, Rate Limit은 "벽"이고, Throttle은 "밸브"에 가깝다.
그렇다면, 왜 필요할까?
- 서버 보호: 갑작스러운 트래픽 폭주(스팸, DDoS 등)로 인한 장애를 막음.
- 공정한 자원 분배: 한 사용자가 자원을 독점하지 못하게 하고, 모든 사용자에게 균등한 서비스 제공.
- 비용 절감: 클라우드 API나 외부 서비스는 호출 횟수에 따라 비용이 발생하기 때문에, 불필요한 과도 호출을 줄여줌.
그럼 어떻게 구현할 수 있을까?
- Token Bucket 알고리즘: 일정량의 토큰을 지급, API 호출 시 토큰을 사용. 토큰이 없으면 대기.
- Leaky Bucket 알고리즘: 요청을 버킷에 담아 일정 속도로만 흘려보냄.
- Fixed Window / Sliding Window: 시간 단위별 호출 횟수 제한.
Node.js Express 예시
const rateLimit = require("express-rate-limit");
const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1분
max: 10, // 1분에 최대 10회
message: "Too many requests, please try again later."
});
app.use("/api/", apiLimiter);
이 경우 /api/ 경로로는 1분에 최대 10회까지만 호출 가능하게 제한된다.
더 나아가기
Redis랑 조합하면 쓰로틀/레이트리밋을 분산 환경(여러 인스턴스)에서도 정확하고 빠르게 구현할 수 있는데, 핵심은 "요청당 Redis에 아주 짧은 원자 연산을 한 번" 하는 것이다.
어떻게 조합할 수 있을까?
1) Fixed Window (가장 간단)
- 키: rate:{userId}:{route}:{YYYYMMDDHHMM}
- 연산: INCR 후 처음이면 EXPIRE로 TTL 설정
- 장점: 구현/운영이 매우 간단
- 단점: 윈도우 경계에서 버스트 가능
const Redis = require("ioredis");
const redis = new Redis();
async function allowRequest(userId, route, limit = 100, windowSec = 60) {
const nowBucket = Math.floor(Date.now() / 1000 / windowSec);
const key = `rate:${userId}:${route}:${nowBucket}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, windowSec);
return count <= limit; // false면 429
}
2) Sliding Window (정확한 제한)
- 자료구조: Sorted Set (ZSET)
- 원리: 현재시간 - 윈도우 크기 이전의 타임스탬프를 ZREMRANGEBYSCORE로 제거하고 남은 개수 확인
- 장점: 경계 버스트 완화, 더 “부드러운” 제한
- 단점: Fixed보다 약간 무거움
async function allowRequest(userId, route, limit = 100, windowMs = 60000) {
const key = `swr:${userId}:${route}`;
const now = Date.now();
const min = now - windowMs;
const pipeline = redis.multi()
.zremrangebyscore(key, 0, min)
.zadd(key, now, `${now}-${Math.random()}`) // 멤버 유니크 보장
.zcard(key)
.pexpire(key, windowMs);
const [, , count] = await pipeline.exec();
return count[1] <= limit;
}
3) Token Bucket (가장 일반적)
- 일정 속도로 토큰을 “보충”, 요청마다 토큰 1개 소비
- Lua 스크립트로 보충/차감/판정을 원자적으로 처리 → 레이스 컨디션 방지
-- token_bucket.lua
-- KEYS[1] = bucket key
-- ARGV[1]= capacity, ARGV[2]= refill_per_sec, ARGV[3]= now(ms), ARGV[4]= cost
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rps = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])
local data = redis.call("HMGET", key, "tokens", "ts")
local tokens = tonumber(data[1]) or capacity
local ts = tonumber(data[2]) or now
-- refill
local delta = math.max(0, now - ts) / 1000.0
tokens = math.min(capacity, tokens + delta * rps)
local allowed = tokens >= cost
if allowed then
tokens = tokens - cost
end
redis.call("HMSET", key, "tokens", tokens, "ts", now)
redis.call("PEXPIRE", key, math.ceil((capacity / rps) * 1000)) -- 유휴시 정리
return allowed and 1 or 0
// Node (ioredis) - Token Bucket 호출
const fs = require("fs");
const script = fs.readFileSync("./token_bucket.lua","utf8");
async function allowRequest(userId, route, capacity=60, refillPerSec=1, cost=1) {
const key = `tb:${userId}:${route}`;
const now = Date.now();
const res = await redis.eval(script, 1, key, capacity, refillPerSec, now, cost);
return res === 1;
}
Redis 모듈 redis-cell(토큰버킷 전용)이나 Redis Stack 을 사용하면 유사 기능을 더 쉽게 사용할 수 있다.
운영 팁 (실무 체크리스트)
- 원자성: INCR+EXPIRE는 첫 호출에서만 EXPIRE가 빠지지 않도록 파이프라인/멀티 or Lua 사용을 고려.
- 헤더 표준화: 클라이언트 친화적으로
429 Too Many Requests Retry-After: <seconds> X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset - 키 설계: userId, IP, API 키, 엔드포인트 등 조합으로 공정성 확보. 필요 시 가중치(비싼 엔드포인트는 cost↑).
- 클러스터 고려: Redis Cluster면 키 해시태그({userId})로 같은 샤드에 묶어 원자성/성능 유지
rate:{userId}:... → rate:{u123}:... - 성능/비용: 핫키(대용량 트래픽 사용자) 분산, TTL로 자동 청소, 메모리 정책(volatile-*) 정합성 확인.
- 장애 모드: Redis 장애 시 Fail-Open/Fail-Closed 정책을 사전 결정.
- 관측: 쓰로틀 이벤트 카운팅/샘플링 로깅, 대시보드화(경보: 폭증 탐지).
- 멀티 리전: 리전별 독립 제한(기본) vs 글로벌 공유 제한(복제/스트림/외부 카운터) 정책 선택.
References
728x90
'Node.js' 카테고리의 다른 글
| [Javascript] 이벤트 루프 (0) | 2024.06.25 |
|---|---|
| [Node.js] 노드의 주요 특징 (1) | 2024.01.16 |
| [Node.js] ECS Fargate Datadog APM 심기 (0) | 2023.04.01 |
| [Node.js] OpenSearch Node.js 클라이언트 (0) | 2022.12.20 |
| [Node.js] 문자열 표기법 - Camel Case, Pascal Case, Kebab Case, Snake Case (0) | 2022.07.21 |
