はじめに
Spring Bootは超便利だが、標準ではRateLimiter(流量制御)の機能は搭載されていない。
Spring Cloud Gatewayを使えばお手軽に実装することも可能だが、少しレイヤが違うので、API Gatewayを使わずに流量制御したい場合にはちょっと過剰な構成となってしまう。
Resilience4jはお手軽に流量制御を実装することができ、Spring Bootとの親和性も高いので、導入の仕方を確認してみよう。
依存関係定義
Mavenの場合
Mavenを使う場合は、<dependencies>
内に以下の依存関係を追加する。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-core</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
<version>1.7.1</version>
</dependency>
Javaの実装
RateLimiterの実装
RateLimiterの実装をするには、流量制御を入れたいところに@RateLimiter
のアノテーションを入れるだけ。
超お手軽。
たとえば、GetUserを流量制御の対象にする場合は以下のように記述する。
name属性の内容は後で説明する。
fallbackMethodは、流量制御中に応答する情報を定義するためのメソッドを記述する。
デフォルトでは500応答を返してしまうので、今回は以下のようにして503応答を返すようにしてみた。
package com.example;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.beans.factory.annotation.Autowired;
@RestController
public class WebController {
@Autowired
private WebService WebService;
@RequestMapping(value = "/user", method = RequestMethod.GET)
public User response(@RequestParam(name = "id", required = false) String id) {
return WebService.GetUser(id);
}
}
package com.example;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.ResponseStatus;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
(中略)
@Service
public class WebService {
@RateLimiter(name = "example", fallbackMethod = "WebServiceFallback")
public User GetUser(String id) {
if (id == null) {
throw new BadRequestException();
}
(以下、通常の処理を書いていく)
}
@SuppressWarnings("unused")
private User WebServiceFallback(String id, RequestNotPermitted rnp) {
throw new ServiceUnavailableException();
}
@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
private class ServiceUnavailableException extends RuntimeException {
}
}
Configurationによるイベントの取得
Configurationを設定することにより、流量制御にエラーになった際の動作を定義することが可能だ。
今回は、流量制御時のみログを出力するようにしてみよう。
package com.example;
import javax.annotation.PostConstruct;
import org.springframework.context.annotation.Configuration;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
@Configuration
public class RateLimiterConfiguration {
private final RateLimiterRegistry rateLimiterRegistry;
public RateLimiterConfiguration(RateLimiterRegistry rateLimiterRegistry) {
this.rateLimiterRegistry = rateLimiterRegistry;
}
@PostConstruct
public void eventSetting() {
rateLimiterRegistry.getAllRateLimiters().forEach(rateLimiter -> {
rateLimiter.getEventPublisher()
.onFailure(e -> System.out.println("ERROR!!!!!!!"));
});
}
}
プロパティの設定
さて、プロパティでは、上記のname属性に紐付く流量制御の具体的な設定を行う。
application.propertiesでもapplication.ymlでもどちらでも記述可能だ。
後者の方が直感的で分かりやすいのでオススメ。というか、基本的にマニュアルとかもapplication.ymlしか載っていない。
既存のアプリケーションをまだapplication.ymlに移行していなくてどうしてもapplication.propertiesしか使いたくない場合に使おう。
resilience4j.ratelimiter.instances.example.registerHealthIndicator=true
resilience4j.ratelimiter.instances.example.limitForPeriod=${LIMIT_FOR_PERIOD:5}
resilience4j.ratelimiter.instances.example.limitRefreshPeriod=10s
resilience4j.ratelimiter.instances.example.timeoutDuration=0
resilience4j.ratelimiter.instances.example.eventConsumerBufferSize=10
resilience4j.ratelimiter.instances.example.subscribeForEvents=true
resilience4j.ratelimiter:
instances:
example:
registerHealthIndicator: true
limitForPeriod: ${LIMIT_FOR_PERIOD:5}
limitRefreshPeriod: 10s
timeoutDuration: 0
eventConsumerBufferSize: 10
subscribeForEvents: true
第4階層がnameに紐付く。ここを変えてクライアントのID単位で定義してあげて、認証基盤やヘッダから取得したキーを使うことでより細かい単位での流量制御も可能になるだろう。
動かしてみる
これでJavaを起動して、curlをlocalhost向けに何度か実行すると、limitForPeriod
で定義した回数以降は以下のような結果を得られる。
$ curl http://localhost/user?id=00001 | jq
{
"timestamp": "2022-03-06T12:58:26.324+0000",
"status": 503,
"error": "Service Unavailable",
"message": "No message available",
"path": "/user"
}
この時、サーバ側のログにはしっかりと
java-container | ERROR!!!!!!!
と記録されているのが分かる。
実際には、流量制御が発動するくらいトラフィックが流れている状況でログを出してしまうと大量になってしまうため、こういう使い方はしないと思われるが、イベントを拾うためには重要な機能である。
これで、Javaでお手軽に流量制御を実装できるようになった。
次回は、実践的に流量制御が発動したことを監視する方法を考察する。