본문 바로가기
허브 살리기 프로젝트

Redis로 Rate Limit 구현

by jay-choe 2024. 6. 23.

 

Rate Limit

 

특정 API 호출을 제한하고싶거나(계산량이 많거나 병목으로 이어질 수 있는 API) 사용자별로 호출 횟수 제한등 특정 처리량을 일정하게 유지하싶을 때 Rate Limit을 사용한다. 단어 그대로 비율을 제한하는 기능이다.

 

어디에 사용하는지는 이미 많은 사례들이 있으니 이 글에서는 Redis를 사용한 구현 방법 및 사용 이유에 관해서 초점을 맞추려고 한다.

 

Why Redis?

먼저 전제 조건은 어느정도 요청을 받는 어플리케이션이라고 가정을한다.

 

작은 어플리케이션에서는 rate limite을 bucket4j와 같은 프로세스 내부에서 특정 알고리즘을 사용하여 제한하는 방식으로 처리할 수 있다.

 

하지만 프로세스 측면이 아닌 서비스의 관점에서 특정 요청을 제한하고 싶을 때 재배포를 하거나 부하를 많이 받게 되어서 scale out을 하게 된다면 외부의 counter를 별도로 두지 않는다면 프로세스별로 사용하는 메모리를 통해 제어하는 방식은 정확하게 요청량을 제한하기 어려워진다.

 

그래서 서비스 측면에서 외부의 Counter를 통한 관리가 필요하다. Redis는 다양한 자료구조와 연산들을 제공해주기에 Counter로서의 역할이 적절하다고 볼 수 있다.

 

또한,  Redis의 낮은 지연시간과 싱글스레드 기반이므로 여러가지 연산을 Atomic하게 묶을 수 있기 때문에 동기화에 관한 처리가 필요 없어지는 큰 장점이 있다.

 

rate limit 같은 경우에 분단위로 보장을 시켜야 하는 경우도 있기 때문에 낮은 지연시간이 중요한 요구사항이 된다.

 

또한 rate limit같은 경우 휘발성 기록 데이터이기 때문에 디스크에 영구적으로 관리할 필요가 없다. 즉 중요한 데이터가 아니다.

 

이와 같은 이유로 서비스 측면에서 처리량을 제한하는 것은 Redis를 사용하는 것이 상당한 이점이 존재한다.

 

구현

처리량 제한 로직을 Redis를 통해 구현하는 것은 매우 쉽다.

 

공식 문서에 Best Practice에 나온 부분에서 아이디어를 얻어와서 구현을 하게 되면 다음 로직으로 구현을 할 수 있다.

 

1. 키가 존재하지 않으면 접근하는 키는 이름 + 시간으로 설정 및 TTL 설정

2. 키가 존재한다면 해당 키를 통해서 조회 후 limit 설정값을 넘지 않았다면 INCR 커맨드를 통해서 1 증가

3. limit 설정 기준 이상인 경우 에러 

 

다음과 같은 3가지 로직이 Atomic 하지 않으면 동기화가 필요한 상황이 되기 때문에 이 로직을 한 번의 연산으로 Atomic하게 묶어줄 수 있는 것이 필요한데 이는 Lua Script를 통해서 해결하면 편하다.

 

Lua Script

Redis에서는 루아 라는 언어로 작성한 스크립트를 실행시킬 수 있게 지원을 한다.

 

여러개의 연산을 묶어서 Atomic 하게 실행하고 싶거나 배치성 작업을 하고싶은경우 유용하게 쓰일 수 있다.

 

TMI로 배치성 작업의 경우 여러번의 네트워크 왕복이 발생하게 되는데 이게 처리량 측면에서 상당하게 감소되는 것을 확인할 수 있는데 Lua Script로 Redis 서버 내에서 처리하게 한다면, 이 네트워크 왕복을 줄인다는 측면에서 시간대비 처리량이 훨씬 높아지게 된다.

 

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local ttl = tonumber(ARGV[2])

local current = tonumber(redis.call('get', key) or '0')

if current >= limit then
    return redis.error_reply("Value exceeds limit")
else
    redis.call('INCR', key)
    redis.call('EXPIRE', key, ttl)
    return current + 1
end

 

간단하게 이런 스크립트를 작성해주면 된다.

 

처음에 Lua Script는 매번 호출 시 스크립트를 같이 전송하게 되는가 궁금했는데 (그렇다면 엄청 낭비이지 않을까.. 라고 생각했다.) 역시 스크립트를 레디스 내부에서 SHA1으로 해싱하고 이후에는 해당 해시값을 통해서 바로 접근 할 수있도록 처리해두었다. 어플리캐이션 재시작시 레디스가 이 해시 연산을 안 하게 하려면 어플리케이션 내부에서 SHA1으로 스크립트를 해싱해서 해당 해시 값이 존재하는지 먼저 체크하는 방식도 괜찮아보인다.

 

한계점 및 정리

먼저 모든 로직이 Redis에 의존하기 때문에 안정적이다고 할 수 없다.

 

Redis 같은 경우 SWAP 메모리를 사용하게 된다던가, 연산을 처리하는 부분은 싱글 쓰레드로 동작하기 때문에 서비스 내부에서 다른 로직에서 무거운 연산을 사용하게 되는경우 요청 처리가 지연되기 때문에 외부 환경에 의해서도 영향을 받게 된다. 

 

또한 Redis가 SPOF가 되기에 장애시 처리율 제한 때문에 매인 비지니스 로직을 돌리지 못 할 수 있다.

 

추가로 적당한 트래픽 선에서는 레디스로 처리를 할 수 있겠지만, 라인 기술블로그 처럼 엄청난 양의 트래픽을 받게 된다면 Redis쪽 네트워크 대역폭 사용에 이슈가 있을 수도 있다.

 

이러한 한계점은 어느 로직에나 존재할 수 있다고 생각하고, 저 규모의 트래픽을 받지 않는다면 결국 문제 해결 방안이 빠른 Redis로 처리율 제한을 구현을 해보는 것이 나쁘지 않은 선택같아 보인다.

'허브 살리기 프로젝트' 카테고리의 다른 글

TopN 구하기 (3)  (0) 2024.10.23
Feign Client Retry  (0) 2024.08.30
TopN 구하기 (2)  (1) 2024.06.16
TopN 구하기 (1)  (1) 2024.06.09
로컬에서만 Spring Boot에서 HTML Resource 파일이 제공 가능 했던 이유  (1) 2024.05.02