bucket4j を用いた Rate-Limiting

https://github.com/vladimir-bukhtoyarov/bucket4j

bucket4j は Java 向けの Rate-Limiting ライブラリとしては著名なものになり、さまざまな OSS、ライブラリでも用いられている。
内部的にはトラフィックシェーピングなどで広く用いられる **Token Bucket** と呼ばれるアルゴリズムを用いている。

https://en.wikipedia.org/wiki/Token_bucket

# Spring Boot Integration

3rd Party 製ではあるが Spring Boot Starter についても提供されている。

https://github.com/MarcGiffing/bucket4j-spring-boot-starter

この記事では bucket4j-spring-boot-starter をベースに簡単なアプリケーションを動作させ、実際にどのように動作するか検証を行った。

## bucket4j-spring-boot-starter-examples
https://github.com/MarcGiffing/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 扱いになっているため。以下の定義を削除。

https://github.com/MarcGiffing/bucket4j-spring-boot-starter-examples/blob/master/bucket4j-spring-boot-starter-example-ehcache/src/main/resources/application.yml#L30-L54

#### 設定パラメータの微調整。

対象となる 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 にリクエストを送り、挙動を確認する。
同一ユーザーで１分間に５回以上リクエストをすると、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 などを用いて簡易的に導入するというのも、一つの選択肢になると思います。