Spring Cloud Circuit Breaker(Resilience4j)を試す。
はじめに
CircuitBraekrの実装はNetflixのNetfix Hystrixが有名です。Spirngでもいくつかの実装を組み込めるようになっており、Spring Cloud傘下のプロジェクトとして公開されています。Springで使えるCircuit Breakerの実装はいくつかありますが、その中でも、Resilience4jを使ってみたいと思います。
Circuit Braekrとは
Circuit Braekr PatternはRelease it!安定性のパターンとして紹介されたフォールトレラント(障害許容)を高くするためのソフトウェアの手法の一つです。日本語訳すると遮断器と呼ばれ、家庭にある電気のブレーカーを意味します。もちろんWebソフトウェアの文脈では家庭にあるブレーカーを意味するわけではなく、通信の際のブレーカーとしての役割をソフトウェアコンポーネントとしてもたせる機構のことを意味します。通信時に通信相手先からのレスポンスによってブレーカーのようにオンオフされる(厳密にはオンオフの二値では無いのですが、この後説明します)機構を追加し、通信相手に必要以上の負荷をかけることを防ぐことを目的とします。Circuit Braekerは以下のようなステートマシンとして働きます。
一般的なブレーカーとは違い以下の3つの状態があります。
OPEN
:ブレーカーが開いている状態、通信を通さない。CLOSE
:ブレーカーが閉じている状態、通信を通すHALF_OPEN
:ブレーカーが半分開いている状態。試しにリクエストを送れる状態
状態遷移として以下のものがあります。
Close
⇨OPEN
- 失敗率が設定された敷地を超えた場合
Open
⇨HALF_OPEN
- 任意の待ち時間をおいた後
HALF_OPEN
⇨OPEN
- 失敗率が設定された敷地を超えた場合
HALF_OPEN
⇨CLODE
(失敗率がリセットされる)
失敗率が設定された敷地を下回った場合Circuit Braekerを導入すると、エラー時はデフォルトのデータを返すなどの一時的なフォールバックを組み込むことも簡単になります。
Spring Clould Circuit Braekr
Spring Clould CirCuit Braekrの実装は以下の4つがあります。
- Netfix Hystrix
- Resilience4J
- Sentinel
- Spring Retry
この中でNetfix Hystixはすでにメンテナンスモードに入ることが宣言されており、今後の利用は下の3つの中から選ぶことになると考えられます。今回は、その中でもResilience4jを使ってみたいと思います。
Resilience4j Circuit Braeker
Resilience4jはNetflix Hystrixに触発された。フォールトレラントを高くするためのライブラリです。Netflix Hystrixと違うのはJava8やファンクショナルなプログラミングのために設計され作られています。
Resilience4Jでは大きく以下のようなものが提供されています。
- CircuitBreaker
- 上記で説明したCircuitBreaker
- Bulkhead
- 安定性のパターンの一つとして、隔離を実現する
- Retry
- リトライ
- Cache
- インメモリのキャッシュ。キャッシュのヒットなどのイベントに対応するファンクションを実装することができる。
Spring Circuit BraekerではこのResilience4jの実装を用いることができます。Resilience4jのCircuitBraekerでは、一定時間内ないの失敗率を計測する時間ベースでのスライディングウィンドウと一定回数内の失敗率を計測すうるカウントベースのスライディングウィンドウを選択することができます。
CircuitBreakerCinfigについて
Resilience4jでは、CircuitBraekerCinfig
と呼ばれるクラスが提供されており、CircuitBraekrに食わせることで、各種設定を行いことができます。CircuitBraekerCinfig
では以下のようなコンフィグポイントが用意されています。
Config property | 説明 | デフォルト値 |
---|---|---|
failureRateThreshold | 失敗率をパーセンテージで設定します。失敗率が設定値よりも上回った際にCircuitBreakerはClose からOpen の状態に遷移します |
50 |
slowCallRateThreshold | スローコールの率をパーセンテージで設定します。スローコールの回数が設定値よりも上回れば、CircuitBreakerはOPEN 状態に遷移し、短いサイクル(short-circuiting)の呼び出しを行なうようになります。 |
100 |
slowCallDurationThreshold | CircuitBreakerが呼び出しに対して、スローコールであると判断するしきい値です。 | 60000 [ms] |
permittedNumberOfCallsInHalfOpenState | CirCuitBreakerがHALF_OPEN の状態の際に許可される呼び出しの上限です |
10 |
slidingWindowType | スライディングウィンドウの種類を指定します。カウントベースのスライディングウィンドウを利用する場合はCOUNT_BASED`を時間ベースのスライディングウィンドウを利用する場合は TIME_BASED`を指定します。 |
COUNT_BASED |
slidingWindowSize | CircuitBraekerの状態がCLOSE である際の外部呼び出しの数を保存する個数を指定します。 |
100 |
minimumNumberOfCalls | CurcitBraekerがエラーのカウント始めるまでの最低の呼び出し回数を指定します。例えば、10 と指定されていた場合、最初の9階がエラーで落ちていたとしてもCircuitBraekerはOPEN の状態に遷移しません。 |
10 |
waitDurationInOpenState | CircuitBraekerがOPEW からHALF_OPEN へ遷移する際の最低限待つ時間を設定します。 |
60000 [ms] |
automaticTransitionFromOpenToHalfOpenEnabled | tureで設定されていた場合、CircuitBraekrはOPEN の状態からHALF_OPEN の状態に自動的に遷移します。外部からの呼び出しを必要としません。 |
false |
recordExceptions | レコードとして記録されるException の種類です。失敗回数としてカウントされ失敗率を引き上げます。登録されたException にマッチするか、もしくはそのException を継承している場合にカウントの対象となります。もし、ignoreExceptions で明示的にException が指定されている場合は無視されカウントされません。 |
empty |
ignoreExceptions | recordExceptions の逆の設定 |
empty |
recordException | あるエクセプションがカウントされるべきか否かのPredicate を記述します。もし失敗としてカウントされるべきであればPredicate はture を返し、成功としてカウントされるべきであればPredicate はfalse を返します |
throwable -> true(デフォルトではすべてのエクセプションが失敗のカウント対象となります。) |
ignoreException | recordException の逆の設定 |
throwable -> false(デフォルトではすべてのエクセプションが失敗としてカウントされます) |
Spring CircuitBraekrでも、Resilience4jのCirCuitBreakerConfig.java
を用いて細かい設定を行なうことができます。
Spring Circuit Brakerを使ってみる。
Springの公式サンプルを参考に実際にCircuit Braekerを使ってアプリを書いてみたいと思います。
アプリはGETでのリクエストを受付けhttps://httpbin.org
に対して外部接続を行います。この外部接続先はシンプルなJsonのレスポンスを返してくれるサービスです。例えば、https://httpbin.org/delay/4
のようにリクエストを送ると4秒間のウェイトをはさみ下記のようなレスポンスを返してくれます。
{ "args": {}, "data": "", "files": {}, "form": {}, "headers": { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "ja,en-US;q=0.9,en;q=0.8", "Cache-Control": "max-age=0", "Host": "httpbin.org", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36", "X-Amzn-Trace-Id": "Root=1-5e57dbce-b90abaeae327c84251086901" }, "origin": "113.149.133.110", "url": "https://httpbin.org/delay/4" }
今回は上記のサイトに対するプロキシとして動くアプリを書きます。
プロジェクトの作成
Spring Initializrを使ってプロジェクトを作成します。その際Javaは11、Springのバージョンは 2.2.4
を選択し、依存にはweb
とResilience4j
を選択しました。
CircuitBraekerの設定を書く
次にCircuit Braekerの設定とRestTemplateのインスタンスをBeanとして登録するための設定を書きます。RestTemplateは外部接続のクライアントとして利用します。
@Configuration public class CBConfig { @Bean public RestTemplate restTemplate() { return new RestTemplateBuilder().build(); } @Bean public Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() { return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id) .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(5)).build()) .circuitBreakerConfig(CircuitBreakerConfig.ofDefaults()).build() ); } }
1つめの@Bean
では単にRestTemplateのインスタンスを登録しています。
2つめの@Bean
ではResilience4JCircuitBreakerFactory
をCustomizer
にくるんでBeanとして登録しています。ファクトリーのtimeLimiterConfig
に外部接続の際のタイムアウトを設定しています。また、circuitBreakerConfig
にはResilience4j
のCircuitBreakerConfig
を登録できるようです。まずは特に設定はいじらないでデフォルト値を利用します。
Controllerを書く
以下のようにコントローラーを記述します。
@RestController public class CBController { Logger LOG = LoggerFactory.getLogger(CBController.class); private RestTemplate restTemplate; private CircuitBreakerFactory circuitBreakerFactory; public CBController(RestTemplate restTemplate, CircuitBreakerFactory circuitBreakerFactory) { this.restTemplate = restTemplate; this.circuitBreakerFactory = circuitBreakerFactory; } @GetMapping("/delay/{second}") public Map greeting(@PathVariable int second) { return circuitBreakerFactory .create("delay") .run(() -> restTemplate.getForObject("https://httpbin.org/delay/" + second, Map.class), t -> { LOG.warn("delay call failed error", t); Map<String, String> fallback = new HashMap<>(); fallback.put("hello", "world"); return fallback; }); } }
コントローラーではGETリクエストを/delay/{second}
で受付け、DIされたCircuitBraekerFactory
を用いて外部リクエストの処理を記述しています。
run
メソッドの第一引数のところに実際の外部接続を記述し、第二引数に接続に失敗した際のフォールバック処理を記述っできるようです。
読めばわかるかもしれませんが、このアプリは、リクエストに成功した場合は外部接続から帰ってきた結果をそのままプロキシし、失敗した際にはログに接続失敗のメッセージを出力し、{"hello":"world"}
クライアントに返却します。
作ったアプリにリクエストを送ってみる
作成したアプリを動かして、cURL
を使ってリクエストを送ってみます。設定のところで外部接続のタイムアウトを5秒に設定したのでまずは成功するリクエストを送ってみます。
$ curl localhost:8081/delay/2 {"args":{},"data":"","files":{},"form":{},"headers":{"Accept":"application/json, application/*+json","Host":"httpbin.org","User-Agent":"Java/11.0.6","X-Amzn-Trace-Id":"Root=1-5e57e57a-e6556f3120f942323218a0dd"},"origin":"113.149.133.110","url":"https://httpbin.org/delay/2"}
想定通り外部接続先から帰ってきた情報がそのままプロキシされて帰ってきています。次に失敗するリクエストを送ってみます。
$ curl localhost:8081/delay/7 {"hello":"world"}
フォールバック処理に記述したレスポンスが帰ってきていますね。またログにも以下のような出力が出ていました。
log2020-02-28 00:53:08.176 WARN 18204 --- [nio-8081-exec-2] d.h.c.r.s.r.CBController : delay call failed error
CircuitBraekerのステートを変化させてみる
先程はデフォルトの状態の設定のCircuit Braekrを使いましたがデフォルトの状態ではCircuit Braekerのステートマシンを変化させるのが少しめんどくさいです。なので、少し設定を変更したいと思います。先程のJavaConfigを以下のように書き換えます。
@Configuration public class CBConfig { @Bean public RestTemplate restTemplate() { return new RestTemplateBuilder().build(); } @Bean public Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() { return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id) .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(5)).build()) .circuitBreakerConfig( CircuitBreakerConfig.custom() .failureRateThreshold(90) .slidingWindowSize(5) .minimumNumberOfCalls(1) .waitDurationInOpenState(Duration.ofSeconds(30)) .build() ).build() ); } }
以下のような設定を追加しました。
failureRateThreshold(30)
⇨ OPEN
の状態にすぐ遷移するように失敗率のしきい値を30%に設定
slidingWindowSize
⇨ デフォルトだとサイズが100で大きすぎるため5に設定
minimumNumberOfCalls
⇨ 特にウェイトの時間などは必要ないので、最小値である1を設定
waitDurationInOpenState
⇨ OPENの状態でいろいろ試してみたいので30秒を設定
それではリクエストを投げてみたいと思います。まずはスライディングウィンドウのキューをすべて成功で埋めるため、以下のリクエストを5回送ります。
$ curl localhost:8081/delay/2
キューに成功が5回溜まったところで、Circuit Braekerの状態をOPEN
にしてみたいと思います。失敗率が30%を上回れば良いので、2回失敗するリクエストを投げ、+1回で成功するはずのリクエストを投げてみます。
$ time curl localhost:8081/delay/6 {"hello":"world"} real 0m5.039s user 0m0.006s sys 0m0.007s $ time curl localhost:8081/delay/6 {"hello":"world"} real 0m5.043s user 0m0.019s sys 0m0.006s $ time curl localhost:8081/delay/2 {"hello":"world"} real 0m0.038s user 0m0.021s sys 0m0.005s
通信等や諸々のオーバヘットがありますが、最初の2回の呼び出しは、タイムアウトとして設定した5秒でフォールバックのレスポンスが返ってきています。そして、3回目の成功するリクエストcurl localhost:8081/delay/2
もフォールバックのレスポンスが即時で返ってきています。CircuitBraekerの状態がOPEN
になり、外部接続への通信が行われずにレスポンスが返されていることがわかります。
また、成功するリクエストに対してはログには以下のように表示されていました。
io.github.resilience4j.circuitbreaker.CallNotPermittedException: CircuitBreaker 'delay' is OPEN and does not permit further calls
今度はOPEN
の状態で30秒待って、HALF_OPEN
の状態にしてから、リクエストを投げてみたいと思います。
まずは、HALF_OPEN
の状態で失敗するリクエストを送ってみます。
$ time curl localhost:8081/delay/6 {"hello":"world"} real 0m0.017s user 0m0.005s sys 0m0.005s # OPENの状態なのでフォールバックのレスポンスが即時で返ってきています。 # この状態で30秒待ちます。 $ time curl localhost:8081/delay/6 {"hello":"world"} real 0m5.022s user 0m0.011s sys 0m0.000s $ time curl localhost:8081/delay/6 {"hello":"world"} real 0m0.016s user 0m0.005s sys 0m0.005s
30秒待った後のリクエストはタイムアウトで設定した5秒たった後にリクエストがフォールバックの処理が返ってきていることがわかります。
その後のリクエストはまた即時返ってきています。Circuit BraekerがHALF_OPEN
の状態で、外部接続のリクエストを一度投げてみて、失敗したので、OPEN
の状態に遷移したことが見て取れます。
ちなみに、HALF_OPEN
の状態を示すようなログは特になかったです。
今度は、HALF_OPEN
の状態で成功するリクエストを送ってみたいと思います。
$ time curl localhost:8081/delay/6 {"hello":"world"} real 0m0.016s user 0m0.005s sys 0m0.005s # 30秒待つ $ time curl localhost:8081/delay/2 {"args":{},"data":"","files":{},"form":{},"headers":{"Accept":"application/json, application/*+json","Host":"httpbin.org","User-Agent":"Java/11.0.6","X-Amzn-Trace-Id":"Root=1-5e59e4ef-d05e3cd9d33af739fb8c8c58"},"origin":"103.5.140.152","url":"https://httpbin.org/delay/2"} real 0m2.810s user 0m0.015s sys 0m0.011s $ time curl localhost:8081/delay/6 {"hello":"world"} real 0m5.024s user 0m0.005s sys 0m0.009s
30秒待った後のリクエストが成功しています。そして、その後の失敗するリクエストはタイムアウトの時間を置いてからレスポンスが返ってきています。これはCircuit Braekerの状態がHALF_OPEN
からCLOSE
に遷移したためです。これで、一通りの状態と状態遷移は試せたと思います。
感想
今回は、Spring Circuit BraekerのResilience4jの実装を試してみました。Circuit Braekerはリトライなどの機構と合わせて使うと更に効果を発揮します。Spring RetryでもCircuitBrakerを使うことが可能なようなので、また別の機会に試してみたいと思います。