일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 살아남았다.
- 커넥션 풀
- JWT
- AOP
- circuitbreaker
- oauth
- 오어스
- Kotlin
- Spring cloud gateway
- 우아한테크코스
- Spring Batch
- DispatcherServlet
- redis
- 우테코 5기
- 트랜잭션
- 우아한 테크 코스
- tomcat
- Transactio
- resilience4j
- HikariCP
- Thread
- 톰캣
- spirng
- 최종 합격
- MDC
- 테스트코드
- 동시성문제
- Elk
- 우테코
- Gateway
- Today
- Total
코딩은 내일부터
Bucket4j를 사용해 유량제어 하기 본문
곽두철 프로젝트를 하면서 Riot API를 사용해야 하는데 1초의 20번, 2분에 100번 요청 제한이 있다.
이번 글에서는 어떤 방식으로 그리고 어떤 라이브러리를 사용했는지 알아보겠다.
대표적으로 다음과 같은 라이브러리가 존재한다.
Guava: 다양한 핵심 Java 라이브러리를 제공하지만, 단순히 Rate Limiting 기능만을 위해 사용하기에는 다소 무거운 느낌이다.
Resilience4j: 서킷 브레이커를 제공하는 라이브러리로 Rate Limiter 기능도 포함되어 있지만, 마찬가지로 단순히 Rate Limiting 기능만을 위해 사용하기에는 다소 무거운 느낌이다.
RateLimitj: 슬라이딩 윈도우 알고리즘을 기반으로 하는 Rate Limiter 구현체이다. 하지만 더 이상 개발을 하지 않고 RateLimitj에서도 Bucket4j를 대체제로 권장하고 있다.
Bucket4j: 토큰 버킷 알고리즘을 기반으로 하며, lock-free한 구현으로 멀티 스레딩 환경에서의 확장성이 우수하며, Rate Limiting만을 목적으로 제공되는 라이브러리이다.
이렇게 4가지 선택지 중에 Resilience4j와 Bucket4j 두 가지를 고를 수 있는데 Resilience4j로 유량제어를 하면 한 서버의 종속적이다.
다시 말하면 다중서버일대는 제어가 안 된다는 말이다.
따라서 Bucket4j와 Redis를 사용해서 다중 환경일 때도 유량제어를 할 수 있도록 만들어보겠다.
Bucket4j
Bucket4j는 Token bucket 알고리즘을 기반으로 하는 Java 속도 제한 라이브러리이다.. Bucket4j는 독립 실행형 JVM 애플리케이션 또는 클러스터 환경에서 사용할 수 있는 스레드로부터 안전한 라이브러리이다.
Bucket4j는 다음과 같이 크게 3가지 클래스를 사용한다.
- Refill : 일정 시간마다 충전할 Token의 개수 지정
- Bandwidth : Bucket의 총크기를 지정
- Bucket : 실제 트래픽 제어에 사용
곽두철 프로젝트는 해당 API를 제어해야 하기 때문에 Interceptor를 사용했지만 Filter, AOP 등등 상황에 맞게 구현하면 될 거 같다.
그림을 보게 되면 사용자가 요청을 보내면 Interceptor에 로직이 Redis에 저장된 토큰을 하나씩 가져와 처리한다.
만약 토큰을 10개 저장했다면 10번 요청을 처리하고 11번 요청에서는 에러를 반환하고 토큰이 다시 충전될 때까지 해당 API요청은 기다려야 한다.
구현
의존성 주입
implementation 'com.bucket4j:bucket4j-redis:8.7.0'
유량제어 정책
@Getter
@RequiredArgsConstructor
public enum RiotControlPolicy {
TEST("test", Duration.ofMinutes(60)) {
@Override
public Bandwidth getLimit() {
return Bandwidth.builder()
.capacity(10)
.refillIntervallyAligned(10, Duration.ofSeconds(1), Instant.now())
.build();
}
},
PRODUCTION("production", Duration.ofMinutes(60)) {
@Override
public Bandwidth getLimit() {
return Bandwidth.builder()
.capacity(30000)
.refillIntervallyAligned(100, Duration.ofSeconds(1), Instant.now())
.build();
}
},
PERSONAL("personal", Duration.ofMinutes(2)) {
@Override
public Bandwidth getLimit() {
return Bandwidth.builder()
.capacity(100)
.refillIntervallyAligned(20, Duration.ofSeconds(1), Instant.now())
.build();
}
},
;
public abstract Bandwidth getLimit();
private final String planName;
private final Duration refillTime;
public static RiotControlPolicy from(final String targetPlan) {
return Arrays.stream(RiotControlPolicy.values())
.filter(riotControlPolicy -> riotControlPolicy.getPlanName().equals(targetPlan))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("정의되지 않은 정책입니다."));
}
public static Bandwidth resolvePlan(final String targetPlan) {
return Arrays.stream(RiotControlPolicy.values())
.filter(riotControlPolicy -> riotControlPolicy.getPlanName().equals(targetPlan))
.map(RiotControlPolicy::getLimit)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("정의되지 않은 정책입니다."));
}
}
현재 Riot은 PRODUCTION API KEY로 요청을 보낼 때는 1초에 100번, 1시간에 30000번까지 요청을 보낼 수 있고,
PERSONAL API KEY는 1초에 20번 2분에 100번 요청을 보낼 수 있다.
이 때문에 위에 RiotControlPolicy(좋은 이름이 생각이 안 난다,,)는 3가지의 정책을 정의했다.
Config 설정
@Slf4j
@Configuration
public class RateLimiterConfig implements WebMvcConfigurer {
@Value("${spring.data.redis.host}")
private String redisHost;
@Value("${spring.data.redis.port}")
private int redisPort;
@Value("${bucket.plan}")
private String bucketPlan;
@Override
public void addInterceptors(final InterceptorRegistry registry) {
registry.addInterceptor(new APIRateLimiterInterceptor(bucketPlan, bucketConfiguration(), lettuceBasedProxyManager()))
.addPathPatterns("/~~~~라이엇 API");
}
@Bean
public RedisClient redisClient() {
final RedisURI redisUri = RedisURI.create(redisHost, redisPort);
return RedisClient.create(redisUri);
}
@Bean
public LettuceBasedProxyManager lettuceBasedProxyManager() {
return LettuceBasedProxyManager
.builderFor(redisClient())
.withExpirationStrategy(ExpirationAfterWriteStrategy.
basedOnTimeForRefillingBucketUpToMax(RiotControlPolicy.from(bucketPlan).getRefillTime()))
.build();
}
@Bean
public BucketConfiguration bucketConfiguration() {
return BucketConfiguration.builder()
.addLimit(RiotControlPolicy.resolvePlan(bucketPlan))
.build();
}
}
해당 설정은 토큰을 생성하고 정의된 Interceptor를 등록해 주는 코드다.
Interceptor 구현
@RequiredArgsConstructor
public class APIRateLimiterInterceptor implements HandlerInterceptor {
@Value("${bucket.plan}")
private final String bucketPlan;
private final BucketConfiguration bucketConfiguration;
private final LettuceBasedProxyManager lettuceBasedProxyManager;
@Override
public boolean preHandle(final HttpServletRequest request, HttpServletResponse response, Object handler) {
return checkBucketCounter(RiotControlPolicy.from(bucketPlan).getPlanName());
}
private boolean checkBucketCounter(final String key) {
final Bucket bucket = bucket(key);
if (!bucket.tryConsume(1)) {
throw new IllegalArgumentException("잠시후 다시 시도해 주세요.");
}
return true;
}
private Bucket bucket(final String key) {
return lettuceBasedProxyManager.builder().build(key.getBytes(), bucketConfiguration);
}
}
interceptor preHandle에서 토큰을 가져와 요청을 처리하는 구조이다. 만약 토큰이 없으면 에러를 던진다.
이렇게 Riot API를 사용하는 요청을 제어하는 걸 구현해 봤다.
Riot API뿐만 아니라 요금정책, 회원등급에 따른 요청을 제어할 수 있을 것이다.
구현코드
https://github.com/KwakDooChul/doochul-backend/pull/24
[Feat] 라이엇 API에 사용할 API bucket4j 적용 by ingpyo · Pull Request #24 · KwakDooChul/doochul-backend
github.com