ABOUT ME

Today
Yesterday
Total
  • Bucket4j란? Bucket4j를 사용한 Spring 트래픽 제한하기
    Spring 2022. 1. 4. 14:05
    반응형

     

    들어가기 앞서..

    Spring REST API를 개발하며 특정 API들에 대해 일정시간 혹은 사용자마다 요청한도(트래픽)을 제한하고 싶은 경우가 있습니다. 해당 기능을 개발하기 위한 유용한 라이브러리인 Bucket4j를 소개하며 해당 라이브러리를 통한 유량제어에 대해 실습을 해보려고 합니다. (요청한도(트래픽) 제한을 하기 위한 라이브러리론 해당 라이브러리 외에 RateLimiter, RateLimitJ 등도 있으니 함께 알아보며 공부하면 더 좋을 것 같습니다.)

    Bucket4J란?

    Bucket4j는 Token bucket 알고리즘을 기반으로 하는 Java 속도 제한 라이브러리 입니다. Bucket4j는 독립 실행형 JVM 애플리케이션 또는 클러스터 환경에서 사용할 수 있는 스레드로부터 안전한 라이브러리입니다.

     

    Bucket4J 개발하기

    Bucket4J library

    implementation group: 'com.github.vladimir-bukhtoyarov', name: 'bucket4j-core', version: '7.0.0'
    

    https://mvnrepository.com/artifact/com.github.vladimir-bukhtoyarov/bucket4j-core

     

    Bucket4J Class

    Refill

    해당 Class는 일정시간마다 몇개의 Token을 충전할지 지정하는 Class입니다

    Bandwidth 

    해당 Class는 Bucket의 총 크기를 지정하는 Class이며 앞서 Token 충전 주기 및 개수를 지정한 Refill Class를 사용하여 만듭니다.

    Bucket  

    해당 Class는 최종적으로 트래픽 제어를 할 Class이며 앞서 만든 Bandwidt Class를 사용하여 Build 합니다.

     

    Bucket4J Example Source 1

    public static void main(String args[]) throws InterruptedException {
    //Refill.intervally toekn = 2, 1회충전시 2개의 토큰을 충전
    //Duration.ofSeconds = 1, 1초마다 토큰을 충전
    Refill refill = Refill.intervally(2, Duration.ofSeconds(1));
    //Bandwidth capacity = Bucket의 총 크기는 3
    Bandwidth limit = Bandwidth.classic(3,refill);
    //총크기가 3이며 1초마다 2개의 Token을 충전하는 Bucket 생성
    Bucket bucket = Bucket.builder()
    .addLimit(limit)
    .build();
    for (int i = 1; i <= 10; i++) {
    System.out.println("\nAvailable Toekn : " + bucket.getAvailableTokens());
    //tryConsume = 1, 한번에 소모할 Token을 지정
    if(bucket.tryConsume(1)){
    System.out.println("index : " + i);
    }else{
    System.out.println("Token is Empty!");
    //Toekn 충전
    sleep(1000);
    }
    }
    }

    Example Source 1 실행결과

    Available Toekn : 3
    index : 1
    Available Toekn : 2
    index : 2
    Available Toekn : 1
    index : 3
    Available Toekn : 0
    Token is Empty!
    Available Toekn : 2
    index : 5
    Available Toekn : 1
    index : 6
    Available Toekn : 0
    Token is Empty!
    Available Toekn : 2
    index : 8
    Available Toekn : 1
    index : 9
    Available Toekn : 0
    Token is Empty!

    Bucket4J Example Source 2

    분당 5개 트래픽 제한이 있다고 가정합니다. 동시에, 10초 안에 모든 토큰을 소진시키는 트래픽을 피하고 싶을 수도 있습니다 . Bucket4j를 사용 하면 동일한 버킷에 여러 Limit를 설정할 수 있습니다. 10초 시간 창에 2개의 요청만 허용하는 또 다른 Limit를 추가해 보겠습니다.

    public static void main(String args[]) throws InterruptedException {
    //1분에 5개의 요청을 할 수 있는 분당 Limit 설정
    Bandwidth minLimit = Bandwidth.classic(5,Refill.intervally(5, Duration.ofMinutes(1)));
    //10초에 2개의 요청을 할 수 있는 초당 Limit 설정
    Bandwidth secLimit = Bandwidth.classic(2,Refill.intervally(2, Duration.ofSeconds(10)));
    //2개의 Limit로 Bucket 생성
    Bucket bucket = Bucket.builder()
    .addLimit(minLimit)
    .addLimit(secLimit)
    .build();
    for (int i = 1; i <= 13; i++) {
    System.out.println("\nAvailable Toekn : " + bucket.getAvailableTokens());
    //tryConsume = 1, 한번에 소모할 Token을 지정
    if(bucket.tryConsume(1)){
    System.out.println("index : " + i);
    }else{
    System.out.println("Token is Empty!");
    //Toekn 충전
    sleep(10000);
    }
    }
    }

    Example Source 2 Result

    Available Toekn : 2
    index : 1
    Available Toekn : 1
    index : 2
    Available Toekn : 0
    Token is Empty!
    Available Toekn : 2
    index : 4
    Available Toekn : 1
    index : 5
    Available Toekn : 0
    Token is Empty!
    Available Toekn : 1
    index : 7
    Available Toekn : 0
    Token is Empty!
    Available Toekn : 0
    Token is Empty!
    Available Toekn : 0
    Token is Empty!
    Available Toekn : 0
    Token is Empty!
    Available Toekn : 2
    index : 12
    Available Toekn : 1
    index : 13

     

    Bucket4J로 Spring 트래픽 제한 개발하기

    Bucket4J Spring Example Source 1

    import com.crypto.domian.BucketTest;
    import io.github.bucket4j.Bandwidth;
    import io.github.bucket4j.Bucket;
    import io.github.bucket4j.Refill;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import java.time.Duration;
    @Slf4j
    @RestController
    @RequestMapping(value = "/v1/khj93")
    public class BucketController {
    private final Bucket bucket;
    public BucketController() {
    //10분에 10개의 요청을 처리할 수 있는 Bucket 생성
    Bandwidth limit = Bandwidth.classic(10, Refill.intervally(10, Duration.ofMinutes(10)));
    this.bucket = Bucket.builder()
    .addLimit(limit)
    .build();
    }
    @PostMapping("/bucketTest")
    public ResponseEntity<BucketTest.Response> bucketTest(@RequestBody BucketTest.Request request) {
    if (bucket.tryConsume(1)) {
    log.info("Success");
    return ResponseEntity.status(HttpStatus.OK).build();
    }
    log.info("TOO MANY REQUEST");
    return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
    }
    }

    Bucket4J Spring Example Source 1 Result

    [17:33:14][ INFO][BucketController.java:36 ][][][http-nio-8080-exec-1] Success
    [17:33:16][ INFO][BucketController.java:36 ][][][http-nio-8080-exec-2] Success
    [17:33:18][ INFO][BucketController.java:36 ][][][http-nio-8080-exec-3] Success
    [17:33:19][ INFO][BucketController.java:36 ][][][http-nio-8080-exec-5] Success
    [17:33:27][ INFO][BucketController.java:36 ][][][http-nio-8080-exec-6] Success
    [17:33:31][ INFO][BucketController.java:36 ][][][http-nio-8080-exec-4] Success
    [17:33:34][ INFO][BucketController.java:36 ][][][http-nio-8080-exec-8] Success
    [17:33:40][ INFO][BucketController.java:36 ][][][http-nio-8080-exec-7] Success
    [17:33:50][ INFO][BucketController.java:36 ][][][http-nio-8080-exec-9] Success
    [17:33:54][ INFO][BucketController.java:36 ][][][http-nio-8080-exec-10] Success
    [17:33:58][ INFO][BucketController.java:40 ][][][http-nio-8080-exec-1] TOO MANY REQUEST
    [17:34:00][ INFO][BucketController.java:40 ][][][http-nio-8080-exec-2] TOO MANY REQUEST
    [17:34:02][ INFO][BucketController.java:40 ][][][http-nio-8080-exec-3] TOO MANY REQUEST

     

    Bucket4J로 API 클라이언트 요금제 개발하기

    Bucket4J Spring Example Source 2 (API 클라이언트 요금제 개발하기)

    API 클라이언트 요금제 Enum 

    클라이언트 요금제를 Enum에 저장하여 해당 apiKey에 맞는 Bucket를 반환한다.

     

    import io.github.bucket4j.Bandwidth;
    import io.github.bucket4j.Refill;
    import java.time.Duration;
    public enum PricingPlan {
    //1시간에 3번 사용가능한 무제한 요금제
    FREE {
    public Bandwidth getLimit() {
    return Bandwidth.classic(3, Refill.intervally(3, Duration.ofHours(1)));
    }
    },
    //1시간에 5 사용가능한 Basic 요금제
    BASIC {
    public Bandwidth getLimit() {
    return Bandwidth.classic(5, Refill.intervally(5, Duration.ofHours(1)));
    }
    },
    //1시간에 10번 사용가능한 Professional 요금제
    PROFESSIONAL {
    public Bandwidth getLimit() {
    return Bandwidth.classic(10, Refill.intervally(10, Duration.ofHours(1)));
    }
    };
    public abstract Bandwidth getLimit();
    public static PricingPlan resolvePlanFromApiKey(String apiKey) {
    if (apiKey == null || apiKey.isEmpty()) {
    return FREE;
    } else if (apiKey.startsWith("BA-")) {
    return BASIC;
    } else if (apiKey.startsWith("PX-")) {
    return PROFESSIONAL;
    }
    return FREE;
    }
    }

    API 클라이언트 요금제 Service

    apiKey별로 Bucket를 저장하고 apiKey에 맞는 버킷을 반환한다.

     

    import com.bucket.code.PricingPlan;
    import io.github.bucket4j.Bucket;
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Service;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    @Service
    @RequiredArgsConstructor
    public class PricingPlanService {
    private final Map<String, Bucket> cache = new ConcurrentHashMap<>();
    public Bucket resolveBucket(String apiKey) {
    return cache.computeIfAbsent(apiKey, this::newBucket);
    }
    private Bucket newBucket(String apiKey) {
    PricingPlan pricingPlan = PricingPlan.resolvePlanFromApiKey(apiKey);
    return Bucket.builder()
    .addLimit(pricingPlan.getLimit())
    .build();
    }
    }

    API 클라이언트 요금제 Controller

    apiKey별로 시간마다 정해진 횟수만큼 사용 가능한 Controller입니다.

    ConsumptionProbe Class내 Method 이용하여 해당 bucket의 남은 Token 및 재충전 시간등을 조회 가능합니다. 

    해당 Method를 사용하여 고객 편의 양질의 서비스를 만들 수 있습니다.
    ConsumptionProbe javadoc

     

     

    import com.bucket.domian.BucketTest;
    import com.bucket.service.PricingPlanService;
    import io.github.bucket4j.Bucket;
    import io.github.bucket4j.ConsumptionProbe;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    @Slf4j
    @RestController
    @RequiredArgsConstructor
    @RequestMapping(value = "/v1/khj93")
    public class BucketController {
    private final PricingPlanService pricingPlanService;
    @PostMapping("/bucketTest")
    public ResponseEntity<BucketTest.Response> bucketTest(@RequestBody BucketTest.Request request) {
    log.info("REQUEST : {} ", request.toString());
    Bucket bucket = pricingPlanService.resolveBucket(request.getApiKey());
    ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);
    long saveToekn = probe.getRemainingTokens();
    if (probe.isConsumed()) {
    log.info("Success");
    log.info("Available Toekn : {} ", saveToekn);
    return ResponseEntity.status(HttpStatus.OK).build();
    }
    long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;
    log.info("TOO MANY REQUEST");
    log.info("Available Toekn : {} ", saveToekn);
    log.info("Wait Time {} Second ", waitForRefill);
    return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
    }
    }

    Bucket4J Spring Example Source 2 Result

     

    [18:41:34][ INFO][BucketController.java:28 ][][][http-nio-8080-exec-1] REQUEST : BucketTest.Request(apiKey=BA-001)
    [18:41:34][ INFO][BucketController.java:35 ][][][http-nio-8080-exec-1] Success
    [18:41:34][ INFO][BucketController.java:36 ][][][http-nio-8080-exec-1] Available Toekn : 4
    [18:41:40][ INFO][BucketController.java:28 ][][][http-nio-8080-exec-2] REQUEST : BucketTest.Request(apiKey=BA-001)
    [18:41:40][ INFO][BucketController.java:35 ][][][http-nio-8080-exec-2] Success
    [18:41:40][ INFO][BucketController.java:36 ][][][http-nio-8080-exec-2] Available Toekn : 3
    [18:41:41][ INFO][BucketController.java:28 ][][][http-nio-8080-exec-3] REQUEST : BucketTest.Request(apiKey=BA-001)
    [18:41:41][ INFO][BucketController.java:35 ][][][http-nio-8080-exec-3] Success
    [18:41:41][ INFO][BucketController.java:36 ][][][http-nio-8080-exec-3] Available Toekn : 2
    [18:41:45][ INFO][BucketController.java:28 ][][][http-nio-8080-exec-4] REQUEST : BucketTest.Request(apiKey=BA-005)
    [18:41:45][ INFO][BucketController.java:35 ][][][http-nio-8080-exec-4] Success
    [18:41:45][ INFO][BucketController.java:36 ][][][http-nio-8080-exec-4] Available Toekn : 4
    [18:41:56][ INFO][BucketController.java:28 ][][][http-nio-8080-exec-5] REQUEST : BucketTest.Request(apiKey=FR-001)
    [18:41:56][ INFO][BucketController.java:35 ][][][http-nio-8080-exec-5] Success
    [18:41:56][ INFO][BucketController.java:36 ][][][http-nio-8080-exec-5] Available Toekn : 2
    [18:42:02][ INFO][BucketController.java:28 ][][][http-nio-8080-exec-6] REQUEST : BucketTest.Request(apiKey=PX-001)
    [18:42:02][ INFO][BucketController.java:35 ][][][http-nio-8080-exec-6] Success
    [18:42:02][ INFO][BucketController.java:36 ][][][http-nio-8080-exec-6] Available Toekn : 9
    [18:42:06][ INFO][BucketController.java:28 ][][][http-nio-8080-exec-7] REQUEST : BucketTest.Request(apiKey=BA-001)
    [18:42:06][ INFO][BucketController.java:35 ][][][http-nio-8080-exec-7] Success
    [18:42:06][ INFO][BucketController.java:36 ][][][http-nio-8080-exec-7] Available Toekn : 1
    [18:42:07][ INFO][BucketController.java:28 ][][][http-nio-8080-exec-8] REQUEST : BucketTest.Request(apiKey=BA-001)
    [18:42:07][ INFO][BucketController.java:35 ][][][http-nio-8080-exec-8] Success
    [18:42:07][ INFO][BucketController.java:36 ][][][http-nio-8080-exec-8] Available Toekn : 0
    [18:42:09][ INFO][BucketController.java:28 ][][][http-nio-8080-exec-9] REQUEST : BucketTest.Request(apiKey=BA-001)
    [18:42:09][ INFO][BucketController.java:42 ][][][http-nio-8080-exec-9] TOO MANY REQUEST
    [18:42:09][ INFO][BucketController.java:43 ][][][http-nio-8080-exec-9] Available Toekn : 0
    [18:42:09][ INFO][BucketController.java:44 ][][][http-nio-8080-exec-9] Wait Time 3565 Second
    [18:42:13][ INFO][BucketController.java:28 ][][][http-nio-8080-exec-10] REQUEST : BucketTest.Request(apiKey=BA-001)
    [18:42:13][ INFO][BucketController.java:42 ][][][http-nio-8080-exec-10] TOO MANY REQUEST
    [18:42:13][ INFO][BucketController.java:43 ][][][http-nio-8080-exec-10] Available Toekn : 0
    [18:42:13][ INFO][BucketController.java:44 ][][][http-nio-8080-exec-10] Wait Time 3561 Second
    반응형
Designed by Tistory.