가상 면접 사례로 배우는 대규모 시스템 설계 기초 (알렉스 쉬 저)를 읽고 정리합니다.
처리율 제한 장치 (rate limiter)
💡 클라이언트 또는 서비스가 보내는 트래픽 처리율을 제어하기 위한 장치
0️⃣ 왜 필요한가?
- Dos 공격에 의한 자원 고갈 (resource starvation)을 방지할 수 있다.
- 처리를 제한함으로써 서버를 많지 않게 두거나, 우선 순위가 높은 API에 더 높은 자원을 할당하는 식으로 비용을 절감할 수 있다.
- 잘못된 이용 패턴으로 유발된 트래픽을 막는 등 서버 과부하를 방지할 수 있다.
- 예시
- HTTP : 특정 기간 내 전송되는 클라이언트 요청 횟수를 제한
- 요청 횟수가 threshold를 넘어서면 추가로 요청된 호출은 모두 block
- 사용자는 초당 2회 이상 새 글을 올릴 수 없다
- 같은 IP 주소로는 하루에 10개 이상의 계정을 생성할 수 없다
1️⃣ 문제 이해 및 설계 범위 확정 :: 필요한 알고리즘 특정
- 요구에 따라 알고리즘의 장단점을 따지며 뭘 쓸지 고민해보자
- 이런 질문을 해보자
- 어떤 종류의 처리율 제한 장치? 서버? 클라이언트?
- 어떤 기준? IP 주소? 사용자 ID?
- 시스템 규모? 스타트업? 대규모 요청?
- 환경? 분산 환경? 모놀리틱?
- 독립적? 코드에 종속?
- 사용자 요청이 걸러진 경우 그 사실을 알려야 하는가?
2️⃣ 개략적인 설계안 제시 및 동의 구하기
- 처리율 제한 장치를 어디에 둘건가?
- 클라이언트측 : 위변조가 쉽고, 모든 구현을 통제하기 어렵기에 일반적으로 안정적이지 못하다.
- 미들웨어측 : 처리율 제한 미들웨어를 만들어 거기서 API 서버로의 요청을 통제하게 만들자.
- API gateway가 이를 지원해준다.
- 만약 마이크로서비스이고, API 게이트웨이를 이미 설계에 포함했거나, 또는 처리율 제한 장치 구현 인력이 부족하다면 API 게이트웨이를 이용하는게 바람직하다.
- 서버측 : 프로그래밍 언어 등이 서버측 구현을 지원할만큼 효율이 높은지 검토해보자. 만약 서버에서 직접 구현할 경우 알고리즘은 자유롭게 선택 가능
-
처리율 제한 알고리즘
토큰 버킷
: 간단하고, 세간의 이해도도 높아 폭넓게 사용되는 중
- 버킷이 꽉 찼다면 추가로 공급된 토큰은 추가되지 않고 버려진다 (overflow)
- 요청은 처리할 때마다 토큰을 사용한다. 토큰이 있으면 꺼내고 요청을 전달하고, 없으면 요청을 버리는 방식
- 장점
- 구현쉽고, 메모리 효율적이고, 짧은 시간에 집중되는 (burst of traffic)도 커버 가능하고, 버킷에 토큰 남아 있으면 요청은 무조건 시스템에 전달된다.
- 단점
- 버킷 크기와 토큰 공급률을 정하기가 까다롭다.
- 보통 API endpoint마다 버킷을 둔다.
- 만약 IP주소마다 처리율을 제한해야 한다면 IP주소마다 버킷을 둬야 하고, 초당 요청으로 제한하고 싶다면 모든 요청이 하나의 버킷을 쓰게 해야 한다.
누출 버킷
: 요청 처리율 고정되어 있는 토큰 버킷 알고리즘, FIFO 큐로 구현
- 동작 원리
- 큐에 빈 자리가 있는지 확인
- 가득 찼으면 새 요청을 버리고, 빈 자리가 있으면 큐에 요청 추가
- 지정된 시간마다 큐에서 요청 꺼내서 처리
- 장점
- 큐 크기 제한적이라 메모리 효율적, 고정된 처리율이라 안정적 출력(stable outflow rate)이 필요할 때 적합
- 단점
- 단시간에 많은 트래픽 몰리면 큐에 오래된 요청이 쌓여지고, 그 요청 제때 처리 못하면 새 요청이 버려짐
- 버킷 크기 (= 큐 사이즈), 처리율(= 정해진 시간에 몇 개 처리?) 인자를 관리하기 힘듬
고정 윈도 카운터
- 동작 원리
- 타임라인을 고정 간격의 윈도로 나누고, 윈도마다 카운터 붙인다
- 요청 접수된 카운터 += 1
- 카운터 값이 사전 정의된 임계에 도달하면 새 요청은 새 윈도 열릴 때까지 버려진다
- 장점
- 메모리 효율 좋고, 이해하기 쉽고, 특정 트래픽 패턴 처리에 적합
- 단점
- 윈도 경계 부근에서 일시적인 트래픽이 몰려들 때, 기대했던 처리 한도보다 많은 양의 요청을 처리하게 됨
이동 윈도 로깅
: 고정 윈도의 윈도 부근 트래픽 문제를 해결
- 동작 방식
- 요청 타임스탬프 추적 (보통 레디스 등 캐시에 보관)
- 새 요청이 오면 만료된 타임스탬프(= 윈도 시작 시점보다 오래된 것) 제거
- 새 요청 타임스탬프를 로그에 추가
- 로그 크기가 허용치보다 같거나 작으면 요청 전달, 아니면 처리 거부
- 장점
- 어느 순간의 윈도도 허용 요청 개수는 시스템 처리 한도를 넘지 않음
- 단점
- 거부된 요청의 타임스탬프도 보관 ⇒ 메모리 많이 잡아먹음
이동 윈도 카운터
: 고정 윈도 카운터 + 이동 윈도 로깅
- 장점
- 이전 시간대 평균 처리율에 따라 윈도 상태 계산 → 짧은 시간 트래픽 대응 O
- 메모리 효율 굿
- 단점
- 직전 시간대 요청이 균등히 분포되어 있다고 가정 → 느슨
-
개략적인 아키텍처
- 얼마나 많은 요청이 접수되었는지 추적할 수 있는 카운터를 추적 대상별로 생성 → 카운터 값이 한도를 넘어서면 요청 거부
- 카운터를 어디에 보관할건지?
- DB는 디스크 접근이라 느리니까 안됨
- 캐시가 적절 : 빠르고 시간에 기반한 만료 정책 쓰기 때문
- redis : 클라이언트가 처리율 제한 미들웨어에게 요청을 보내면, 미들웨어는 레디스 지정 버킷에서 카운터를 가져와 한도 도달 여부를 확인한다.
3️⃣ 상세 설계
처리율 제한 규칙
은 어떻게 만들어지고 어디에 저장되나?
- 보통 오픈소스 등을 활용할 수 있고, 설정 파일 형태로 디스크에 저장된다.
처리가 제한된 요청
들은 어떻게 처리하나? (= 처리율 한도 초과 트래픽 처리 = throttle)
- HTTP 429 응답만 보내거나
- HTTP 응답 헤더로 확인할 수도 있다.
- X-Ratelimit-Remaining
- X-Ratelimit-Limit
- X-Ratelimit-Retry0After
- 또는 나중에 처리하기 위해 큐에 보관한다.
분산 환경
에서의 처리율 제한 장치의 구현
- 여러대의 서버와 병렬 스레드 지원하도록 시스템 확장하기.
- 경쟁 조건 (race condition)
- 요청을 처리하는 스레드가 병렬로 레디스에서 값을 읽었다면, 그리고 둘 중 어느쪽도 아직 변경 값을 저장하지 않았다면 카운터가 이상하게 작동한다.
- 락을 걸어 해결할 수 있지만, 성능 이슈가 있다.
- 이 경우 루아 스크립트 또는 정렬 집합 자료구조를 이용해 해결할 수 있다.
- 동기화 (synchronization)
- 처리율 제한 장치 서버가 여러 대인 경우 동기화가 필요하다.
- 고정 세션 (sticky session)을 활용해 같은 클라이언트 요청을 같은 장치로 보내게 만들 수 있다.
- 레디스같은 중앙 집중형 데이터 저장소를 쓸 수 있다.
성능 최적화
- 데이터 센터 지원
- 에지 서버를 이용해 트래픽 전달 지연 시간을 줄인다.
- 제한 장치 간 데이터 동기화에 최종 일관성 모델 사용 (eventual consistency model)
모니터링
- 아래를 모니터링을 통해 확인한다
- 채택된 처리율 제한 알고리즘이 효과적인지
- 정의한 처리율 제한 규칙이 효과적인지
4️⃣ 마무리 :: 설계에 대한 후속 질문 및 추가 논의
- 경성 (hard) 또는 연성 (soft) 처리율 제한
- 경성 처리율 제한 : 요청 개수는 임계치를 절대 못 넘는다
- 연성 처리율 제한 : 요청 개수는 잠시 동안은 임계치를 넘을 수 있다
- 다양한 계층에서의 처리율 제한
- OSI 계층 어디에서나 처리율 제한이 가능하다
- 처리율 제한을 회피하는 방법
- 클라이언트를 어떻게 설계할 것인가?
- 캐시 사용으로 API 호출 횟수 줄이기
- 짧은 시간에 너무 많은 메시지 보내지 말기
- 예외나 에러 처리 코드를 도입해 클라이언트가 예외 상황에서 복구될 수 있게 하기
- 재시도 로직 구현에는 충분한 백오프 시간을 두기