개요
이 장은 처리율 제한 장치(Rate Limiter)에 대한 기술 면접을 대비하기 위한 장입니다. 이 장은 아래와 같은 내용을 다룹니다.
- 처리율 제한 장치가 무엇이고, 왜 사용하는지
- 기술 면접에서 어떤 점을 면접관과 소통해야 하는지
- 처리율 제한 장치에서 사용될 알고리즘과 각각의 장단점
해당 내용을 잘 익히시고, 만약에 해당 내용을 기술 면접에서 물어볼 때 잘 대답하시고, 그렇지 않더라도 프로젝트에서 적용할 수 있길 바랍니다.
처리율 제한 장치란?
처리율 제한 장치는 말 그대로 특정 시간 동안 서버로 오는 클라이언트의 요청의 수를 제한하는 장치를 말합니다.
보통은 대규모 트래픽을 처리할 수 있는 능력을 물어볼 텐데, 오히려 처리율을 제한한다니 왜 그럴까요?
결국에 성능 개선을 한다고 하더라도, 기업에서 현재 가지고 있는 서버가 처리할 수 있는 총트래픽이 정해져 있거나, 특정 정책 상의 이유로 제한을 걸 수 있겠죠.
책에서는 다음과 같은 장점이 있다고 합니다.
- DoS 공격에 의한 자원 고갈 방지
- 비용 절감
- 서버 과부하 방지
1번과 3번은 서버에 많은 요청을 사전에 방지하니까 이해가 되겠고, 2번은 외부 API를 사용하는 경우에 그 비용을 줄일 수 있을 겁니다.
아래 사진처럼 클라우드를 사용하는 경우도 비용이 많이 나갈 수 있겠죠?
면접 체크리스트
그럼 이제 처리율 제한 장치를 설계하는 과정에서 어떤 내용들을 면접관에게 확인해야 할까요?
책에서 정리된 질문 목록은 아래와 같습니다.
- 호출 제한 위치가 클라이언트 측인지 서버 측인지
- 호출 제한 기준은 어떤 것인가? IP or 유저 ID?
- 처리할 시스템의 규모는 어느 정도인가?
- 분산 환경에서 동작하는가?
- 처리율 제한 장치가 독립적인 서비스인지, APP 코드에 속한 것인가?
- 처리율 제한에 걸렸을 때, 사용자가 그 결과를 알아야 하는가?
그리고 책의 지원자는 다음과 같은 사실을 정리했습니다.
- 처리율을 정확하게 제한해야 한다.
- 처리하는 과정에서 응답 시간이 낮아야 한다.
- 가능한 적은 메모리를 사용해야 한다.
- 분산 처리율 제한 장치여야 한다. (하나의 처리 제한 장치가 여러 서버에서 사용 가능)
- 예외가 발생한다면 사용자에게 그 사실을 알려야 한다.
- 제한 장치에 장애가 생기더라도 전체 시스템에 영향을 주면 안 된다.
제한 장치 위치 정하기
제한 장치를 둘 수 있는 곳은 총 3가지가 있다.
클라이언트, 서버, 미들웨어를 두는 것이다.
클라이언트에 제한 장치를 위치하면 서버의 자원을 사용하지는 않겠지만 클라이언트에서 오는 요청이 위변조 될 가능성이 있고, 모든 클라이언트의 장치에 일괄적인 설정을 진행하기 어렵습니다.
그렇기 때문에, API 서버에서 처리율 제한 기능을 만들어서 제한을 두거나, 처리율 제한 미들웨어를 만든 후, 미들웨어가 API 서버에게 요청을 보내는 방식으로 사용하게 됩니다.
어떤 방법을 선택하는 것이 옳은지는 정답은 따로 없다고 합니다. 현재 개발하는 환경에 맞춰서 방법을 선택하는 것이 좋고, Rate Limiter를 개발하고 운영하기 어렵다면 사용 API Gateway 서비스를 이용하는 것이나 오픈소스 라이브러리를 사용하는 방법도 있다고 합니다.
처리율 제한 알고리즘
제한 위치를 정하게 된다면, 마찬가지로 어떤 알고리즘을 통해서 처리율을 제한할 지에 대해서 생각해봐야 합니다. 각각의 알고리즘은 저마다의 장단점이 존재하기 때문에, 그 특징을 익힌 후 상황에 맞게 사용하는 것이 중요합니다.
책에서 다루는 알고리즘은 총 5개가 있습니다.
- 토큰 버킷 알고리즘
- 누출 버킷 알고리즘
- 고정 윈도 카운터 알고리즘
- 이동 윈도우 로그 알고리즘
- 이동 윈도우 카운터 알고리즘
이들이 어떻게 동작하고, 어떤 특징이 있는지 가볍게 알아봅시다.
토큰 버킷 알고리즘
토큰 버킷 알고리즘은 버킷에 일정 시간마다 토큰이 추가가 되고, 하나의 요청을 처리할 때마다 토큰을 사용하게 됩니다. 토큰이 없다면 요청은 거부가 되고, 버킷의 한계만큼만 토큰을 저장할 수 있습니다.
이 알고리즘에서 가장 중요한 설정 부분은 버킷의 최대 크기와, 1초에 몇 개의 토큰을 리필해 주는지 설정하는 부분입니다.
토큰 버킷 알고리즘을 사용할 때엔 여러 규칙을 적용할 수 있습니다.
API 마다 호출 회수를 지정해 그에 알맞은 토큰을 설정해 준다거나, IP에 대한 최대 요청 수를 설정한다거나, 서버에 대한 모든 요청 수를 제한할 수 있습니다.
책에서는 통상적으로는 API마다 별도의 버킷을 둔다고 합니다.
토큰 버킷 알고리즘의 장단점은 아래와 같습니다.
장점
- 구현이 쉽다. (버킷설정 + 토큰 추가 + 토큰 사용)
- 메모리가 효율적이다. (토큰 자체가 많은 메모리를 사용하지 않음)
- Burst 상황에서 대처가 가능하다. (특정 기간의 끝 ~ 리필된 후 직후에 몰려도 토큰이 빵빵하면 대처 가능)
단점
- 버킷의 수, 토큰 공급률(초당 토큰 리필양)에 대한 적절한 조절이 어렵다.
Java의 경우 Bucket4J라는 라이브러리를 통해서 쉽게 처리율 제한이 가능하다고 합니다. API 서버의 기능으로도 두기 편하고, Spring Cloud API Gateway에 사용해서 미들웨어의 역할을 해도 좋아 보입니다.
Bucket4J Github
누출 버킷 알고리즘
누출 버킷 알고리즘은 큐 형태로 구현되어 있고, 큐에 쌓인 요청을 일정 시간마다 FIFO 알고리즘에 따라서 처리합니다. 그리고 큐의 사이즈가 지정되어 있어서 큐가 꽉 찰 경우엔 새로운 요청은 거부됩니다.
동작은 정리하면 다음과 같습니다.
- 큐의 사이즈가 꽉 찼나? 찼으면 버리고 아니면 추가한다.
- 일정 시간마다 큐에서 작업을 처리해 준다.
참.. 간단하죠?
누출 버킷 알고리즘에서는 토큰 버킷과 마찬가지로 2개의 설정값이 존재합니다.
바로 큐의 최대 사이즈, 그리고 지정된 시간당 몇 개의 항목을 처리할지입니다.
책의 내용에 따르면 Shopify라는 전자상거래 기업이 해당 알고리즘을 사용한다고 합니다.
찾아보면 Nginx
를 활용해서도 Rate Limiter를 구현할 수 있는데, 이때 누출 버킷 알고리즘을 사용한다고 합니다.
Nginx 관련 내용은 아래 블로그들을 참고해보십셔.
will.log - [Nginx RateLimit] Nginx에 RateLimit 적용해보기
내용이 간단해서 Java의 BlockingQueue를 이용해서 내용을 구현해 봤습니다.
누출 버킷 알고리즘의 장단점은 아래와 같다고 합니다.
장점
- 큐의 사이즈가 고정되어 메모리 사용량 측면에서 효율적이다.
- 고정된 처리율을 갖고 있기 때문에, 안정적 출력이 필요한 경우 적합하다.
단점
- 단시간에 많은 트래픽이 몰리게 되면, 오래된 요청만 큐에 쌓여서 최신 요청들을 처리하지 못하는 경우가 발생한다.
- 토큰 버킷과 마찬가지로 두 인자를 튜닝하는 것이 어렵다.
고정 윈도 카운터 알고리즘
고정 윈도 카운터 알고리즘은 특정한 타임라인을 나누고 window로 나누고, 해당 window에서 들어온 요청의 수를 카운팅을 해줍니다. 정해진 요청의 수를 넘기면 거절하는 방식으로 동작합니다.
고정 윈도 카운터 알고리즘의 경우 window의 경계 부분에서 트래픽이 몰리면, 예상한 양보다 많은 부하가 발생할 수 있다는 점이 존재하지만 개발자가 이해하기 쉽고, 메모리 효율이 좋다는 특징이 있습니다.
이동 윈도 로깅 알고리즘
이동 윈도 로깅 알고리즘은 고정 윈도 카운터 알고리즘에서 발생하는 window의 경계에서 트래픽이 몰리는 경우를 해결하기 위한 알고리즘이다.
이름에서 알 수 있겠지만, window를 이동하면서 해당 윈도에서 걸쳐있는 요청의 개수를 체크하며 요청을 처리해 준다. 그렇기 때문에 모든 영역에서 원하는 양의 트래픽을 처리할 수 있다는 장점이 존재한다. 하지만 이 과정에서 각 요청들이 언제 들어왔는지 타임스탬프를 저장해야 하기 때문에, 메모리 사용양이 많다는 단점 역시 발생하게 된다.
타임스탬프를 저장할 때엔 Redis의 정렬 집합과 같은 캐시에 저장을 하고, window의 범위에 벗어난 과거 요청들은 타임스탬프를 삭제하는 식으로 관리를 해준다.
이동 윈도 카운터 알고리즘
마지막으로 이동 윈도 카운터 알고리즘이다. 앞선 두 윈도 알고리즘의 특징을 결합한 알고리즘으로, 고정된 사이즈에서 각각 요청에 대한 카운팅을 해주고, 윈도우가 이동하면서, 걸쳐있는 2개의 윈도우의 비율 * 요청수로 전체 요청의 수를 계산해 받을지, 거절할 지를 계산한다.
예를 들면 이전 구간 A에는 10개의 요청을 처리했고, 새로운 구간 B에서는 3개의 요청을 처리한 상황에서 현재 윈도우가 A구간에 70%, B구간에 30% 걸쳐있다면 현재 윈도우에서 처리한 요청은 대략 8개로 계산하여 진행하는 것이다.
이 알고리즘의 장점은 이동 윈도우 알고리즘이기 때문에, 경계에서 트래픽이 몰리더라도, 해결할 수 있으며 타임스탬프를 저장하지 않아서 메모리 효율이 좋다.
하지만 앞선 예시처럼 대략이라는 말이 나올 수밖에 없는 느슨한 알고리즘이라는 것이 단점으로 작용할 수 있다. 하지만 클라우드 플레어에 따르면 큰 문제는 아니라고 한다.
고려해야 할 점
어떤 알고리즘을 사용해서까지 제작하는 방법을 파악할 수 있었다. 그럼 그 외엔 어떤 내용을 고려해야 할까?
바로 '처리율 제한 규칙을 어떻게 관리하느냐'와 '한도가 초과된 트래픽을 어떻게 관리하냐'이다.
책의 예시에서는 사용하는 오픈소스가 설정 파일로 그 규칙을 관리하기 때문에, 설정 파일로 관리한다고 나와있다.
앞서서 알고리즘을 소개할 때 말한 Java의 Bucket4J의 경우 Application 코드와 yml 설정파일로 규칙을 관리할 수 있고, Nginx의 경우는 설정 파일로 관리할 수 있다.
한도 초과 트래픽 관리는 주로 HTTP 헤더를 사용한다. 책에서는 'X-Ratelimit-Remaining' 이라는 식으로 작성한다고 나왔는데, 결국 Http 헤더에서 'X' 가 붙은 헤더들은 커스텀이라는 것을 나타내기 때문에, 개발팀의 컨벤션을 따르면 될 것 같다.
한도 초과가 발생한 경우 429번 Too Many Request 상태 코드를 작성해서 에러 메세지를 리턴할 수도 있다.
추가적으로 서비스가 분산 환경에서 동작한다면, 동시성 이슈와 동기화 문제, 성능 최적화를 고려해야 한다.
동시성 이슈의 경우 주로 Redis의 Sorted Set을 이용하고, 동기화 문제의 경우 Sticky Session이나 Redis와 같은 공용 저장소를 이용해 해결한다.
성능 최적화의 경우 1장에서 살펴본 것처럼 가까운 데이터 센터를 사용하는 것을 통해 최적화가 가능하다.
마지막으로는 모니터링을 통해 우리가 채택한 알고리즘과 파라미터 값이 적절한지 판단하고, 더 좋은 방법을 찾을 수 있도록 고민하면 될 것이다.