Python으로 레질리언스 엔진 구축: LimitPal 내부 구조 (파트 2)
Source: Dev.to
개요
Executor 파이프라인, 시계 추상화, 그리고 circuit‑breaker 아키텍처는 LimitPal의 핵심입니다. 설계는 모든 호출이 동일한 단계들을 고정된 순서대로 통과하는 단일 실행 파이프라인을 따릅니다:
- Circuit breaker
- Rate limiter
- Retry loop
- Result recording
이 순서는 의도된 것으로, 빠르게 실패하도록 하여 상위 서비스가 다운되었을 때 즉시 오류를 반환하고, breaker 실패가 할당량을 소모하는 것을 방지하며, 재시도가 레이트 제한을 준수하도록 보장하고, 버스트 동작을 예측 가능하게 유지합니다.
실행 파이프라인
- 빠른 실패: 회로 차단기가 먼저 실행됩니다. 업스트림 서비스가 사용 불가능하면 호출이 즉시 거부되어 호출자를 보호합니다.
- 속도 제한: 차단기가 실행을 허용한 후에만 속도 제한기가 적용되어 재시도가 할당된 할당량 내에 머물도록 보장합니다.
- 제한기 윈도우 내 재시도: 재시도는 속도 제한 윈도우 내에서 발생하며, 각 재시도를 예산된 작업으로 취급합니다. 이는 스트레스 상황에서도 시스템을 안정적으로 유지합니다.
- 결과 기록: 마지막으로 결과가 관찰 가능성을 위해 기록되고 차단기에 피드백됩니다.
단일 파이프라인이 필요한 이유
개별 데코레이터는 종종 자체 시간 모델, 재시도 로직 및 실패 의미를 가지고 있습니다. 이를 겹쳐 사용하면 예기치 않은 행동이 발생합니다. 실행자는 다음을 강제합니다:
- 공유 시계
- 공유 실패 모델
- 공유 실행 라이프사이클
이는 시스템을 예측 가능하게 하고 이해하기 쉽게 만듭니다.
시계 추상화
시간은 복원력 시스템에서 가장 어려운 의존성입니다. LimitPal은 time.time()에 대한 직접 호출을 플러그인 가능한 시계 인터페이스로 교체합니다:
class Clock(Protocol):
def now(self) -> float: ...
def sleep(self, seconds: float) -> None: ...
async def sleep_async(self, seconds: float) -> None: ...
모든 컴포넌트는 이 시계를 사용하며, 단조(monotonic) 시간을 기반으로 하여 시스템 시계 점프, NTP 조정, 혹은 컨테이너 마이그레이션으로 인한 문제를 방지합니다.
테스트 이점
clock.advance(5.0) # 실제 대기 없이 5초를 빠르게 전진
테스트가 결정론적이고 빠르게 되며, 몇 분에 달하는 재시도 동작을 즉시 시뮬레이션할 수 있습니다.
서킷 브레이커 아키텍처
브레이커는 상태 머신입니다:
CLOSED → OPEN → HALF_OPEN → CLOSED
정상 작동 (CLOSED)
- 실패가 발생하면 카운터가 증가합니다.
- 실패 임계값에 도달하면 브레이커가 OPEN 상태로 전환됩니다.
OPEN 상태
- 모든 호출이 즉시 실패합니다—재시도 없음—빠른 거절을 제공합니다.
HALF_OPEN 상태
- 복구 타임아웃이 지나면 제한된 수의 탐색 호출이 허용됩니다.
- 호출이 성공하면 브레이커가 CLOSED 로 돌아가고, 그렇지 않으면 다시 OPEN 상태가 됩니다.
이 규칙은 복구 후 재시도 폭풍을 방지하고 안정성 조절 장치 역할을 합니다.
지터 (Jitter)와 지수 백오프
지터가 없으면 수천 개의 클라이언트가 동시에 재시도하면서 동기화된 스파이크가 발생해 서비스를 압도할 수 있습니다. 작은 무작위 오프셋을 추가하면 재시도가 시간에 걸쳐 분산됩니다:
- 지터 없음: 모든 재시도가
t = 1 s에 발생 - 지터 있음: 재시도가
[0.9 s, 1.1 s]구간에서 발생
이 무작위성은 큰 안정성 향상을 가져옵니다.
Rate Limiting
Limiters operate per key (e.g., user:123, tenant:acme, ip:10.0.0.1). Each key gets its own bucket, preventing a single bad actor from exhausting the global quota.
제한기는 키별로 작동합니다(예: user:123, tenant:acme, ip:10.0.0.1). 각 키는 자체 버킷을 할당받아 단일 악의적인 사용자가 전체 할당량을 고갈시키는 것을 방지합니다.
Internally this requires:
내부적으로는 다음이 필요합니다:
- Dynamic bucket allocation
- 동적 버킷 할당
- TTL eviction
- TTL 기반 삭제
- Bounded memory usage
- 제한된 메모리 사용
- Optional LRU trimming
- 선택적 LRU 정리
동기와 비동기의 동등성
LimitPal은 동기와 비동기 실행을 모두 지원하는 통합 API를 제공합니다:
executor.run(...) # sync
await executor.run(...) # async
숨겨진 동작 차이가 없으며, 백그라운드 워커, HTTP 서버, CLI 도구 전반에 걸쳐 동일한 사고 모델을 사용할 수 있습니다.
예정 작업
- Observability hooks
- Adaptive rate limiting
- Redis 백엔드 지원
- Bulkhead 패턴 구현
- 인기 프레임워크와의 통합
탄력성은 실행에서 끝나는 것이 아니라; 분산 시스템은 실패하며, 설계된 실패 동작이 필수적이다.
참고 문헌
- 문서:
- 소스 코드:
피드백을 환영합니다, 특히 깊은 인프라 도구에 관심이 있는 분들의 의견을 기다립니다.