-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characterspublic 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 실행결과
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersAvailable 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를 추가해 보겠습니다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characterspublic 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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersAvailable 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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersimport 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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters[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를 반환한다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersimport 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에 맞는 버킷을 반환한다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersimport 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 javadocThis file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersimport 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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters[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 반응형'Spring' 카테고리의 다른 글
Spring Batch란? 이해하고 사용하기(예제소스 포함) (14) 2021.08.24 [Spring] Spring Framework란? 기본 개념 핵심 정리 (15) 2018.09.12