본문 바로가기
Language/☕️Java

Java - bucket4j를 통해 트래픽 제한 및 IP 차단으로 Rate Limit 구현

by 발개발자 2022. 12. 21.
반응형

  작업개요 

개인 사이트의 운영을 시작하다보니 갑자기..!! 여러가지 고려사항들이 떠올랐다... what the fork.. 

가장 먼저, ec2 free-tier를 사용하다보니 DoS 공격이라던지, 한 IP에서 대용량의 트래픽을 요청하는 경우를 막는게 먼저라 생각했다.

그래서 요청 IP당 분단위로 트래픽을 제한하고, 그 이상으로 요청올 경우 error를 뱉어주려고 한다.

 

얌우치게 바로 시작해보자.

 

 

 

  Bucket4j

Bucket4j는 토큰 버킷 알고리즘을 기반으로하는 Java 속도 제한 라이브러리이다.

토큰 버킷 알고리즘은 트래픽을 제어하기 위해 커다란 양동이(버킷)을 준비한 후, 그 안에 토큰을 넣어 요청마다 토큰을 소모시킨 후, 고갈이 되었을 때 에러를 내뱉어 준다. 그리고 일정시간 지났을 때, 다시 양동이에 토큰을 넣어주어 속도를 제어하는 방식이다.

 

오호,, 뭔지 알았으니 의존성을 추가하자.

 

dependency 추가

    // https://mvnrepository.com/artifact/com.bucket4j/bucket4j-jcache
    implementation group: 'com.github.vladimir-bukhtoyarov', name: 'bucket4j-core', version: '7.0.0'

 

 

본인은 Filter단에서 요청 IP당 지정해놓은 접근 횟수를 초과하면 error를 뱉어주는 로직으로 진행할 예정이다.

 

 

  Filter 생성

 

IpThrottlingFilter.class

public class IpThrottlingFilter extends GenericFilterBean {

    int capacity = 300;
    int refillTokens = 300;
    Duration refillDuration = Duration.ofMinutes(1);

    private static final Map<String, Bucket> CACHE = new ConcurrentHashMap<>();

    private Bucket createNewBucket() {
        Refill refill = Refill.intervally(refillTokens, refillDuration);
        Bandwidth limit = Bandwidth.classic(capacity, refill);
        return Bucket.builder().addLimit(limit).build();
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
        String ip = httpRequest.getRemoteAddr();
        Bucket bucket = CACHE.computeIfAbsent(ip, k -> createNewBucket());

        // tryConsume returns false immediately if no tokens available with the bucket
        if (bucket.tryConsume(1)) {
            // the limit is not exceeded
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            // limit is exceeded
            HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
            httpResponse.setContentType("text/plain");
            httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            httpResponse.getWriter().append("{\"message\":\"요청이 너무 많습니다. 1분후 다시 시도해주세요.\"}");
        }

    }

}

 

 

 

이제 해당 로직을 보면 뜯어보자.

 

 

1. 최초 토큰 300개, 리필 시간 1분, 리필 토큰 300개를 설정해주었다. 즉, IP당 1분내 최대 요청수는 300이다.

 

2. 요청이 올때마다 요청자의 IP를 추출한다.

 

3. Thread safe하면서도 성능을 보장해야하기 때문에, ConcurrentHashMap으로 Cache를 지정한다.

 

4. IP별로 Cache의 Key을 넣어주고 Value값으로 버킷을 넣어준다.

 

5. 버킷에서 토큰을 하나 사용할 수 있으면 pass 시켜주고 토큰이 부족한 경우 error를 뱉어준다.

 

 

  테스트

대망의 테스트다. 사실 이쯤왔으면 본인이 요리조리 테스트를 다 마쳐놓은 상태이다 ㅎㅎ..

 

 

Jmeter를 통해 테스트를 진행해보자.

 

시나리오

  • 1명의 유저가 310개의 요청을 보낸다.
  • 300개는 제대로 응답을 받고, 10개에선 error 메시지를 받게된다.
  • Easy...!

아주 간단 명료한 시나리오다. 이 상태로 실천에 옮겨보자.

 

 

 

Jmeter를 통해 마지막 10개의 에러가 발생하는 것을 확인할 수 있다. 

에러 메시지도 Filter에서 지정한 "요청이 너무 많습니다." 라는 걸 확인할 수 있다.

 

이렇게 Bucket4j와 Filter를 통해 어느정도 트래픽을 제어할 수 있었다. 

이 외에도 개인사이트를 운영하며 고려해야할 사항들을 공부하며 하나씩 블로그에 경험치를 쌓아보고자 한다.

오늘은 그럼 여기서 마무리..!!

반응형

댓글