분산 시스템에서 고가용성, 안정성, 자원 보호를 위해서 사용하는 방법들입니다.
✅ 1. 각 개념 설명
🔹 Rate Limiting (속도 제한)
- 정의: 단위 시간 동안 허용되는 최대 요청 수를 제한
- 목적: API 남용 방지, 공정한 자원 사용 보장
- 예시: “사용자는 1분에 100건 요청 가능, 초과 시 429 오류 반환”
🔹 Throttling (스로틀링, 속도 조절)
- 정의: 사용량이 많을수록 요청을 일시적으로 지연하거나 속도를 제한
- 목적: 시스템 과부하 방지, 리소스 보호
- 예시: “요청이 몰리면 응답을 500ms 지연하거나, 초당 처리 수 제한”
🔹 Circuit Breaker (서킷 브레이커)
- 정의: 외부 시스템 호출 실패율이 높으면 해당 호출을 **일시적으로 차단(Open)**하고, 일정 시간 후 회복 상태를 **테스트(Half-Open)**하여 정상화되면 다시 연결(Closed)
- 목적: 장애 전파 방지, 전체 시스템 안정성 확보
- 예시: “DB 응답 실패율이 50% 이상이면 1분간 DB 호출을 차단”
✅ 2. 비교 분석 표
항목 | Rate Limiting | Throttling | Circuit Breaker |
🔧 핵심 기능 | 요청 수 제한 | 속도 조절, 지연 | 장애 서비스 호출 차단 |
🎯 주요 목적 | 남용 방지, 자원 보호 | 과부하 방지 | 장애 확산 방지 |
🧠 적용 방식 | 고정 제한값 (예: 100req/min) | 실시간 리소스 기반 동적 조절 | 상태 기반 차단(Open/Closed) |
⚠️ 오류 응답 | 429 Too Many Requests | 없음 or 지연된 응답 | fallback 응답 or 즉시 실패 |
🧪 상태 관리 | 없음 | 없음 | 있음 (Closed/Open/Half-Open) |
🧩 적용 대상 | API 요청, 사용자별 트래픽 | 네트워크, DB, API 요청 | 외부 시스템 호출 (DB, API) |
👥 사용자 경험 | 갑작스러운 실패 가능 | 지연되지만 성공 가능 | 빠른 실패 or 대체 응답 |
📦 기술 예시 | NGINX, AWS API Gateway | Istio, Envoy, Redis 기반 큐 | Resilience4j, Hystrix |
✅ 3. 비유로 이해하기
개념 | 비유 |
Rate Limiting | 놀이공원에서 “1시간에 100명만 입장 가능” 규칙 |
Throttling | 입장 대기줄이 길면 천천히 입장시키는 것 |
Circuit Breaker | 놀이기구가 고장 났을 때 일시적으로 폐쇄하고 점검 후 재개장 |
✅ 4. 함께 사용하는 경우
복잡한 시스템에서는 아래처럼 조합하여 사용하는 것이 일반적입니다:
- Rate Limiting으로 API 남용을 방지
- Throttling으로 시스템 부하를 조절
- Circuit Breaker로 불안정한 외부 서비스 차단
예시 시나리오:
“사용자 요청이 초당 100건 넘으면 Throttle.
1분에 500건 넘으면 429 응답(Rate Limit).
외부 DB 오류율 60% 넘으면 1분간 호출 차단(Circuit Breaker).”
✅ 5. 결론 요약
항목 | Rate Limiting | Throttling | Circuit Breaker |
✅ 핵심 역할 | 요청 수 제한 | 처리 속도 조절 | 장애 확산 방지 |
🧠 똑똑한 동작 | ❌ 정적 | ✅ 동적 | ✅ 상태 기반 |
🧩 사용 시기 | 사용량 제어 | 자원 보호 | 외부 시스템 불안정 시 |
🤝 보완 관계 | 함께 사용 가능 | 함께 사용 가능 | 함께 사용 권장 |
✅ Redis 성능 기준
연산 종류 | 처리 속도 |
단순 GET/SET | 약 10만~30만 QPS (싱글 인스턴스 기준) |
INCR / EXPIRE | 약 8만~15만 QPS |
Lua Script 실행 | 복잡도에 따라 수천~수십만 QPS 가능 |
List/ZSet 삽입/삭제 | 수만 QPS 이상 가능 (데이터 길이에 따라 다름) |
⚠ Redis는 싱글 스레드 기반의 인메모리 DB로서, 단순 연산에 매우 빨라, 수만 ~ 수십만 대기열 관리용도로 충분합니다.
Throttling / Queueing / Rate Limiting 를 사용한 분산 환경에서의 사용자 대기열 시스템 예제
✅ 목표 재정의
기능 | 목적 |
컨테이너 간 접속 상태 공유 | 수평 확장 대응 |
사용자 요청을 대기열 방식으로 제어 | 동시 접속자 제한 |
TTL 사용 | 중복 삽입 방지, 유효 시간 관리 (단 너무 짧게 설정 ❌) |
대기 중 사용자에게 정보 제공 | 대기 순번, 예상 시간 |
초당 허용량 제한 | 급격한 요청 폭주 방어 |
사용자 대기 지속 시간 보장 | TTL 만료 시점이 지나도 처리가 보장되도록 함 |
✅ Redis 설계
Redis 키 | 타입 | 설명 |
entry:zqueue | ZSet | 대기열 (score: 입장 timestamp 또는 증가값) |
entry:user:{userId} | String (TTL 5~10분) | 중복 삽입 방지용 |
rate:tokens | String + TTL | 초당 요청 제한 토큰 관리 |
entry:metadata:{userId} | Hash or String | 사용자별 요청 정보/상태 저장 (선택적) |
✅ 전체 로직 흐름 (사용자 1명 기준)
[1] 클라이언트 요청 → Gateway 진입
└─ 헤더 또는 세션에서 userId 추출
[2] Redis에 이미 entry:user:{userId} TTL 키가 있다면:
└─ 이미 대기열에 있음 → 순번 및 예상 시간 응답
[3] 없다면:
├─ ZADD entry:zqueue userId (score = timestamp 또는 counter)
├─ SETEX entry:user:{userId} "1" TTL 10분
└─ 대기 metadata 저장 가능 (선택)
[4] ZRANK entry:zqueue userId → 대기 순번 확인
├─ 순번 < 허용 동시 처리 수 (ex. 100명): 통과
└─ 순번 ≥ 허용치: 대기 메시지 응답
[5] 초당 제한: Redis Token Bucket(Lua script) → 실패 시 429
[6] 처리 후:
├─ ZREM entry:zqueue userId
├─ DEL entry:user:{userId}
└─ metadata 정리
✅ TTL 전략
TTL 항목 | 적정 값 | 이유 |
entry:user:{userId} | 5~10분 | 대기 중 재삽입 방지, 유실 방지 |
Token Bucket | 1~2초 TTL or sliding window | Redis 내 자동 갱신 |
entry:metadata | 사용자 처리 완료 후 즉시 삭제 또는 10분 |
✅ UX 개선 예시 응답 메시지
{
"status": 429,
"message": "현재 대기열 위치: 152 / 예상 대기 시간: 약 30.4초",
"retry_after_ms": 30400,
"userId": "abc123",
"entry_time": "2025-06-30T11:30:21.000Z"
}
✅ 초당 허용량 제한 (Token Bucket)
- Redis Lua script 기반 구현 (replenishRate, burstCapacity)
- 전역 키 or 사용자/경로별 키 사용
- Redis TTL로 부하 완화
📦 전체 구성요소 요약
요소 | 설명 |
QueueGatewayFilter | Redis ZSet 기반 대기열 필터 |
RequestRateLimiter | Spring Cloud Gateway 기본 제공 QPS 제한 |
Redis | TTL 키, ZSet, Token 관리 |
application.yml | Gateway + Redis 설정 포함 |
✅ application.yml
spring:
redis:
host: localhost
port: 6379
timeout: 2000
lettuce:
pool:
max-active: 50
max-idle: 10
min-idle: 5
cloud:
gateway:
routes:
- id: entry-api
uri: http://localhost:8081
predicates:
- Path=/entry
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 5 # 초당 5건 허용
redis-rate-limiter.burstCapacity: 10 # 버스트 처리 허용
key-resolver: "#{@userKeyResolver}"
- name: RedisQueueFilter
✅ KeyResolver (userId 기준 제한)
@Component("userKeyResolver")
public class UserKeyResolver implements KeyResolver {
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.justOrEmpty(exchange.getRequest().getHeaders().getFirst("X-User-Id"));
}
}
✅ 3. GatewayFilter (RedisQueueFilter)
@Component("RedisQueueFilter")
public class RedisQueueFilter implements GatewayFilter {
private static final String ZQUEUE = "entry:zqueue";
private static final String PREFIX = "entry:user:";
private static final int MAX_ACTIVE = 100;
private static final int TTL_SECONDS = 600;
private static final int AVERAGE_PROCESS_MS = 200;
private final StringRedisTemplate redis;
public RedisQueueFilter(StringRedisTemplate redis) {
this.redis = redis;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String userId = extractUserId(exchange);
if (userId == null) return reject(exchange, "User ID 누락");
String userKey = PREFIX + userId;
return Mono.fromCallable(() -> {
// TTL로 중복 삽입 방지
Boolean alreadyQueued = redis.hasKey(userKey);
if (Boolean.FALSE.equals(alreadyQueued)) {
// 사용자 삽입
long score = System.currentTimeMillis();
redis.opsForZSet().add(ZQUEUE, userId, score);
redis.opsForValue().set(userKey, "1", Duration.ofSeconds(TTL_SECONDS));
}
return true;
}).flatMap(_ -> {
Long position = redis.opsForZSet().rank(ZQUEUE, userId);
if (position == null) return reject(exchange, "대기열 순위 조회 실패");
if (position >= MAX_ACTIVE) {
long waitMs = (position - MAX_ACTIVE + 1) * AVERAGE_PROCESS_MS;
return reject(exchange, String.format("대기 위치: %d / 예상 시간: %.1f초", position + 1, waitMs / 1000.0));
}
return chain.filter(exchange).doFinally(signal -> {
redis.opsForZSet().remove(ZQUEUE, userId);
redis.delete(userKey);
});
});
}
private String extractUserId(ServerWebExchange exchange) {
return exchange.getRequest().getHeaders().getFirst("X-User-Id");
}
private Mono<Void> reject(ServerWebExchange exchange, String message) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
DataBuffer buffer = response.bufferFactory().wrap(message.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
}
'Dev > Article' 카테고리의 다른 글
데이터 거래소 개념 (0) | 2025.02.06 |
---|---|
[용어/개념] 시계열 데이터베이스(TSDB, Time Series Database) 란 (1) | 2023.12.11 |
[용어/개념] Thundering herd problem (0) | 2023.12.07 |
강타입 언어/약타입 언어/정적타입/동적타입 (0) | 2023.10.06 |
애플리케이션 서비스 아키텍처 대안 (0) | 2023.08.23 |