流量制御とは
アプリケーションで一度に実行できる処理の最大値を設定し、同時にリクエストされる回数を制限することです。サーバの負荷が一定以上になるのを防ぐことで、安定的にアプリケーションを稼働させることができます。APIを外部に公開する場合など、リクエストの回数がものすごく多くなる懸念があるときに使用すると有効でしょう。
JavaではResilience4j
というライブラリにRateLimiter
という仕組みがあるので、それを用いて実現することができます。
リクエストパラメータごとに流量制御を適応する
通常流量制御はコントローラーに設定したURLごとに適応することが一般的でしょう。このような使い方に関しては、以下のサイトでわかりやすく解説されていました。
本記事ではURLのエンドポイントに加え、リクエストパラメータごとでも制御する回数を分ける方法を紹介します。
具体的にはリクエストパラメータにuserId
という値を設定し、GETでの/sample
へのアクセスをuserId
ごとに一定時間にリクエストできる回数を制限しています。
実装
それでは具体的な実装に入っていきます。流量制御で用いる値は主に以下の3つです。
-
limitForPeriod
:一定時間にリクエストできる回数 -
limitRefreshPeriod
:制限をリフレッシュする時間 -
timeoutDuration
:制限されたときの再度実行するまでの待機時間
limitRefreshPeriod
の時間の中でlimitForPeriod
回までメソッドの呼び出しができ、上限に達したら制限する。
呼び出し時に制限されていた場合timeoutDuration
で指定した時間待機し、再度呼び出しを行う。このときも制限されている場合は処理が落ちる。
このような振る舞いをします。
ディレクトリ構成
今回作成するサンプルは以下のような構成になっています。
sample-project
├── ︙
├── build.gradle
└── src
├── ︙
└── main
├── java
│ └── com
│ └── example
│ ├── SampleProjectApplication.java
│ ├── config
│ │ └── resilience4j
│ │ └── SampleRateLimiterConfig.java
│ ├── infrastructure
│ │ └── resilience4j
│ │ └── SampleRateLimiterResistry.java
│ └── controller
│ └── SampleController.java
└── resources
├── application.yml
└── ︙
(使用するファイルのみ記述しています。)
以下で作成するファイルのパスはこちらを参照してください。
依存関係(Gradle)
/build.gradle
のdependencies
に以下のように記述します。
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'io.github.resilience4j:resilience4j-ratelimiter:2.0.1'
compileOnly 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'
}
これでspringの機能、Resilience4jのRateLimiter、lombokをしようできるようになります。
(lombokはGetterやSetterを簡単に記述できるので使用しています。)
【注意】
Resilience4jのバージョンの2.0.*
ではJavaのバージョンが17以降でないとbuildに失敗します。Javaの8や11を使っている場合は、Resilience4jのバージョンを1.7.1
などにすると利用できます。
値を設定をする
上で説明した3つの値を設定していきます。以下の2つのファイル作成します。
-
application.yml
:データを定義するためのYAML形式のファイル -
SampleRateLimiterConfig.java
:application.yml
の値を読み取るためのファイル
application.yml
を以下のように記述します。
resilience4j:
ratelimiter:
limitRefreshPeriod: 1000
limitForPeriod: 10
timeoutDuration: 5000
次にSampleRateLimiterConfig.java
で上で設定した値を読み取れるようにします。
package com.example.config.resilience4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import lombok.Getter;
import lombok.Setter;
@Component
@Getter
@Setter
@ConfigurationProperties(prefix = "resilience4j.ratelimiter")
public class SampleRateLimiterConfig {
int limitRefreshPeriod;
int limitForPeriod;
int timeoutDuration;
}
application.yml
に設定した値を読み込む方法はこちらの記事が参考になりました。
流量制御の管理クラス
設定した値で流量制御を行えるようにしたインスタンスの管理などを行うSampleRateLimiterResistry.java
を作成します。
package com.example.infrastructure.resilience4j;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Component;
import com.example.demo.config.resilience4j.SampleRateLimiterConfig;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
/**
* 流量制御を管理するクラス.
*/
@Component
public class SampleRateLimiterResistry {
// 流量制御の値の設定
private final RateLimiterConfig config;
// リクエストパラメータのuserIdごとにRegistryを管理するためのmap
private Map<Integer, RateLimiterRegistry> registryMap = new HashMap<>();
/**
* コンストラクタ.
*
* @param sampleRateLimiterConfig application.ymlの値を読み込んでいるクラス
*/
public SampleRateLimiterResistry(SampleRateLimiterConfig sampleRateLimiterConfig) {
// 流量制御で用いる各種値を設定
this.config = RateLimiterConfig.custom()
.limitRefreshPeriod(Duration.ofMillis(sampleRateLimiterConfig.getLimitRefreshPeriod()))
.limitForPeriod(sampleRateLimiterConfig.getLimitForPeriod())
.timeoutDuration(Duration.ofMillis(sampleRateLimiterConfig.getTimeoutDuration()))
.build();
}
/**
* ユーザIDごとのRateLimiterRegistryを返す.
*
* @param userId ユーザID
* @return ユーザIDごとのRateLimiterRegistry
*/
public RateLimiterRegistry getRegistry(Integer userId) {
RateLimiterRegistry result = registryMap.get(userId);
if(result == null) {
// コンストラクタで作成した設定値を適応した流量制御を行うRegistryを作成
result = RateLimiterRegistry.of(config);
registryMap.put(userId, result);
}
return result;
}
}
これで1秒間に10回のリクエストまで許容し、制限された場合は5秒待機するような設定となります。
リクエストの受け取りと制限
SampleController.java
にて流量制御を適応します。DDDの観点から言うとinfrastructure
配下の物をコントローラーで使用するのは良くないと思いますが、今回はサンプルということで許容させてください。
package com.example.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.infrastructure.resilience4j.SampleRateLimiterResistry;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import io.vavr.control.Try;
@RestController
@RequestMapping("/api/sample")
public class SampleController {
private SampleRateLimiterResistry sampleRateLimiterResistry;
TaskController(SampleRateLimiterResistry sampleRateLimiterResistry) {
this.sampleRateLimiterResistry = sampleRateLimiterResistry;
}
/**
* 流量制御用サンプルエンドポイント.
*
* @param userId ユーザID
*/
@GetMapping("/ratelimiter")
public String rateLimiter(@RequestParam("userId") Integer userId) {
RateLimiterRegistry registry = sampleRateLimiterResistry.getRegistry(userId);
Try.ofSupplier(RateLimiter.decorateSupplier(registry.rateLimiter("default"), () -> {
System.out.println("処理の実行");
return null;
})).get();
return userId.toString();
}
}
コントローラーの処理自体は受け取ったuserId
をそのまま返しているだけです。
これでリクエストパラメータのuserId
ごとに流量制御を行うことができます。
なお、registry.rateLimiter
の第2引数のラムダ式の部分をSampleService::sampleMethod
のようにすれば、SampleSevice.java
のsampelMethod
という名前のメソッドを呼び出すことができます。
localhost
でサーバを起動すれば次のようにcurl
コマンドで呼び出すことができます。
$curl 'localhost:8080/api/sample/ratelimiter?userId=1'
連続でリクエストを送ると処理を待機したり失敗することがわかると思います。
application.yml
に設定した数値を変えて、動作を確認してみてください。