bucket4j は Java 向けの Rate-Limiting ライブラリとしては著名なものになり、さまざまな OSS、ライブラリでも用いられている。
内部的にはトラフィックシェーピングなどで広く用いられる Token Bucket と呼ばれるアルゴリズムを用いている。
Spring Boot Integration
3rd Party 製ではあるが Spring Boot Starter についても提供されている。
この記事では bucket4j-spring-boot-starter をベースに簡単なアプリケーションを動作させ、実際にどのように動作するか検証を行った。
bucket4j-spring-boot-starter-examples
bucket4j-spring-boot-starter の作者が、動作確認用の examples プロジェクトをいくつか公開しているため、この記事ではこちらをベースに動作を試みる。
いくつかある中で、 jCache(EHCache)を状態管理に用いた bucket4j-spring-boot-starter-example-ehcache が最も単体で動作をさせやすいので、こちらを用いる。
Configuration
上記の examples ファイルを checkout し、いくつか設定を変更する。
なお、今回は Java 11 上で動作をさせることを前提とする。
pom.xml
オリジナルの bucket4j-spring-boot-starter から以下について手を加えた
bucket4j-spring-boot-starter 最新版の使用
<dependency>
<groupId>com.giffing.bucket4j.spring.boot.starter</groupId>
<artifactId>bucket4j-spring-boot-starter</artifactId>
<version>0.2.0</version>
</dependency>
JAXB 対応
Java9以降では標準対応をしていないのでライブラリを追加
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
javax.activation 対応
Java9以降ではjavax.activationが標準対応していないのでライブラリを追加
<dependency>
<groupId>com.sun.activation</groupId>
<artifactId>javax.activation</artifactId>
<version>1.2.0</version>
</dependency>
application.yml
filter-key-type の定義を削除
最新版では deprecated 扱いになっているため。以下の定義を削除。
設定パラメータの微調整。
対象となる URL などを変更した。
その結果、設定ファイルは以下のような形となる。
management:
endpoints:
web:
exposure:
include: "*"
prometheus:
enabled: true
spring:
cache:
jcache:
config: classpath:ehcache.xml
bucket4j:
enabled: true
filters:
- cache-name: buckets
filter-method: servlet
filter-order: -10
url: /login
metrics:
tags:
- key: USERNAME
expression: "@securityService.username() != null ? @securityService.username() : 'anonym'"
- key: URL
expression: getRequestURI()
rate-limits:
- bandwidths:
- capacity: 5
time: 1
unit: minutes
- Rete-Limiting : 有効
- 対象URL:/login
- tags : username, url (tagの単位で rate-limiting の計算を行う)
- 制限 : username x url ごとに1分あたり5回のリクエストを許可
- stateの管理 : jcache(ehcache)
Running
実際に curl コマンドで Rate-Limiting 対象の URL Endpoint にリクエストを送り、挙動を確認する。
同一ユーザーで1分間に5回以上リクエストをすると、429 が返却される。
通常時(200)
< HTTP/1.1 200
< X-Rate-Limit-Remaining: 0
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
(snip.)
Rate-Limit 時 (429)
< HTTP/1.1 429
< X-Rate-Limit-Retry-After-Seconds: 60
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cacheo
< Expires: 0-
< X-Frame-Options: DENY
(snip.)
Monitoring
Spring Actuator 経由で設定状況、ならびに Rate-Limiting 処理のステータスについて確認をすることができる。
/actuator/bucket4j
{
"servlet": [
{
"cacheName": "buckets",
"filterMethod": "SERVLET",
"strategy": "FIRST",
"url": "/login",
"filterOrder": -10,
"rateLimits": [
{
"filterKeyType": null,
"executeCondition": null,
"skipCondition": null,
"expression": "1",
"bandwidths": [
{
"capacity": 5,
"time": 1,
"unit": "MINUTES",
"fixedRefillInterval": 0,
"fixedRefillIntervalUnit": "MINUTES"
}
]
}
],
"metrics": {
"enabled": true,
"types": [
"CONSUMED_COUNTER",
"REJECTED_COUNTER"
],
"tags": [
{
"key": "USERNAME",
"expression": "@securityService.username() != null ? @securityService.username() : 'anonym'",
"types": [
"CONSUMED_COUNTER",
"REJECTED_COUNTER"
]
},
{
"key": "URL",
"expression": "getRequestURI()",
"types": [
"CONSUMED_COUNTER",
"REJECTED_COUNTER"
]
}
]
},
"httpResponseBody": "{ \"message\": \"Too many requests!\" }"
}
]
}
/actuator/prometheus
# HELP bucket4j_summary_consumed_total
# TYPE bucket4j_summary_consumed_total counter
bucket4j_summary_consumed_total{URL="/login",USERNAME="anonym",name="buckets",} 87.0
bucket4j_summary_consumed_total{URL="/login",USERNAME="admin",name="buckets",} 1.0
cache_removals{cache="buckets",cacheManager="cacheManager",name="buckets",} 0.0
cache_puts_total{cache="buckets",cacheManager="cacheManager",name="buckets",} 88.0
cache_evictions_total{cache="buckets",cacheManager="cacheManager",name="buckets",} 0.0
# HELP bucket4j_summary_rejected_total
# TYPE bucket4j_summary_rejected_total counter
bucket4j_summary_rejected_total{URL="/login",USERNAME="anonym",name="buckets",} 796.0
bucket4j_summary_rejected_total{URL="/login",USERNAME="admin",name="buckets",} 7.0
cache_gets_total{cache="buckets",cacheManager="cacheManager",name="buckets",result="miss",} 2.0
cache_gets_total{cache="buckets",cacheManager="cacheManager",name="buckets",result="hit",} 890.0
http_server_requests_seconds_count{exception="None",method="GET",status="200",uri="/actuator/bucket4j",} 1.0
http_server_requests_seconds_sum{exception="None",method="GET",status="200",uri="/actuator/bucket4j",} 0.049276574
http_server_requests_seconds_max{exception="None",method="GET",status="200",uri="/actuator/bucket4j",} 0.049276574
Conclusion
bucket4j ならびに bucket4j-spring-boot-starter を用いると、かなり簡単に既存の Spring Boot アプリケーションに Rate-Limiting の機能を追加することができる。
本格的に Rate-Limiting の処理をシステム全体に適用する場合は、Ambassador Pattern や Side-Car Pattern を用いて API Gateway のミドルウェアを導入した方が望ましいと思います。
特定のアプリケーションの処理に個別に適用する程度であれば、bucket4j などを用いて簡易的に導入するというのも、一つの選択肢になると思います。