LoginSignup
24
16

More than 5 years have passed since last update.

Spring Boot + Resilience4j で CircuitBreaker と RateLimiter を試す

Posted at

Netflix Hystrix を調べてみようかなーと思っていたところ、どうやらメンテナンスモードになったらしく、Netflix のブログに掲載された画像には、Hystrix ではなく Resilience4j の名前が出ていたので、どうせならと思い Resilience4j について調べてみました。

Resilience4j

Resilience4j は以下の機能を提供しています。

  • CircuitBreaker
  • RateLimiter
  • Bulkhead
  • Retry
  • Cache
  • TimeLimiter

Spring Boot と一緒に利用するのであれば、Starter が用意されています。
なお、Spring Boot の 1.x 系と 2.x 系で、artifactId が異なるようなので、注意が必要です。
また、上記に含まれているのは、CircuitBreaker と RateLimiter のみで、他の機能を利用する場合には、別途 dependency を追加する必要があります。
(AutoConfigure も用意されていないので、Bean 定義も自分で行う必要あり。)

今回は、Spring Boot 2.x 系で CircuitBreaker と RateLimiter を利用する方法に関してまとめます。

環境

  • JDK 8
  • Spring Boot 2.1.2.RELEASE
  • Resilience4j 0.13.2

CircuitBreaker

マイクロサービスで一部のサービスに障害が発生した際、一時的に障害の発生したサービスへのアクセスを遮断して、障害が伝播することを防ぐことができます。

CircuitBreaker には Closed、Open、HalfOpen という3つの状態があります。
正常時は Closed で、一定以上処理が失敗した場合には、Open となりアクセスを遮断します。
Open 状態で一定時間経過すると、HalfOpen 状態となります。
HalfOpen 状態で一定以上処理が失敗しなければ、Closed 状態に戻るという流れです。

CircuitBreaker
画像引用元:http://resilience4j.github.io/resilience4j/

Resilience4j では、処理の成功、失敗をリングバッファで管理しており、バッファ内の失敗数が設定した割合を超えた場合に、状態が遷移します。
下図のような感じに、失敗を0、成功を1として保持しているようです。

RingBuffer
画像引用元:http://resilience4j.github.io/resilience4j/

Closed -> Open と HalfOpen -> Closed の判定で使用されるリングバッファは別で、サイズもそれぞれ定義できますが、判定条件(エラーの割合)は同じ値が利用されます。

なお、リングバッファが満たされるまでは、状態遷移を行いません。
例えば、リングバッファが10、50% 以上で状態遷移する設定の場合で、
5回連続でエラーが発生してもリングバッファが満たされていないので、Open に遷移しません。

また、処理の成功、失敗は例外によって判断します。
デフォルトでは、どんな例外でも例外がスローされれば処理失敗とみなしますが、失敗とみなす条件を指定することもできます。

設定

application.ymlで設定することができます。
複数のCircuitBreaker を定義することもできます。

resilience4j:
    circuitbreaker:
        backends:
            circuitA: # CircuitBreaker の名称
                truering-buffer-size-in-closed-state: 5 # Closed 状態で利用するリングバッファのサイズ
                ring-buffer-size-in-half-open-state: 3 # HalfOpen 状態で利用するリングバッファのサイズ
                wait-duration-in-open-state : 5000 # Open 状態で待機する時間(ミリ秒)
                failure-rate-threshold: 50 # Open 状態へ遷移する閾値(%)
                record-failure-predicate: com.example.resilience.RecordFailurePredicate # 失敗の判定を行う Predicate<Throwable> を指定する
                ignore-exceptions: # 失敗とカウントしない例外クラス
                    - com.example.resilience.exception.BusinessException
                record-exceptions: # 失敗とカウントする例外クラス
                    - com.example.resilience.exception.SystemException
            circuitB:
                # 省略・・・

特定の例外のみを失敗とみなしたいときは recordExceptions、逆に特定の例外のみ失敗とみなしたくないときは ignoreExceptions、複雑な判定を行いたい場合は recordFailurePredicate という感じでしょうか。
一応、すべての要素を同時に指定することも可能です。

実装

Spring AOP を利用する方法と、Functional に書く方法があります。
どちらで実装しても、Circuit が Open 状態の場合には、CircuitBreakerOpenExceptionが発生するようになります。

以下の実装例では、簡略化のためマイクロサービスにはなっていません。
本来は Service クラスが RestTemplateなどを利用して他サービスの API を呼び出すような処理になると思います。

Spring AOP

クラス or メソッドに @CircuitBreaker(name = "hogehoge") と指定することで、CircuitBreaker が有効になります。
クラスに指定した場合には、すべての public メソッドで CircuitBreaker が有効になります。
name には appliction.ymlで設定した CircuitBreaker の名称を指定します。

@Service
@CircuitBreaker(name = "circuitB")
public class CircuitBreakerService {
    public String aop(String str) {
        if (str == null) {
            throw new RuntimeException();
        }
        return "success!!";
    }
}

呼び出す側は特に何も考えず、メソッドを実行するだけでOKです。

@RestController
@RequestMapping("/circuit")
public class CircuitBreakerController {
    private final CircuitBreakerService service;

    public CircuitBreakerController(CircuitBreakerService service) {
        this.service = service;
    }

    @GetMapping("/aop")
    public String aop(@RequestParam(required = false) String str) {
        return service.aop(str);
    }
}

Functional に書く方法

呼び出される側は特になにも考えずに実装します。

@Service
public class CircuitBreakerService {
    public String func(String str) {
        if (str == null) {
            throw new RuntimeException();
        }
        return "success!!";
    }
}

呼び出す側は CircuitBreaker の decorate〜メソッドを利用して、呼び出すメソッドをデコレートします。

@RestController
@RequestMapping("/circuit")
public class CircuitBreakerController {
    private final CircuitBreaker circuitBreaker;
    private final CircuitBreakerService service;

    public CircuitBreakerController(CircuitBreakerRegistry registry, CircuitBreakerService service) {
        this.circuitBreaker = registry.circuitBreaker("circuitA");
        this.service = service;
    }

    @GetMapping("/func")
    public String func(@RequestParam(required = false) String str) {
        return CircuitBreaker.decorateSupplier(circuitBreaker, () -> service.func(str)).get();
    }
}

フォールバック処理

続いて、障害が発生した場合、フォールバック処理を実行したいと思います。
Hystrix の場合、@HystrixCommand("hogeMethod")のように指定することで、フォールバック処理として hogeMethodを呼び出すことができたらしいのですが、Resilience4j にはそのような機能は提供されていないため、自身で実装する必要があります。

公式サイトのサンプルでは、Vavr の Try モナドを利用したフォールバック処理を行っていたため、これを用いて実装したいと思います。

@RestController
@RequestMapping("/circuit")
public class CircuitBreakerController {
    private final CircuitBreaker circuitBreaker;
    private final CircuitBreakerService service;

    public CircuitBreakerController(CircuitBreakerRegistry registry, CircuitBreakerService service) {
        this.circuitBreaker = registry.circuitBreaker("circuitA");
        this.service = service;
    }

    @GetMapping("/func")
    public String func(@RequestParam(required = false) String str) {
        return Try.ofSupplier(CircuitBreaker.decorateSupplier(circuitBreaker, () -> service.func(str)))
                .recover(CircuitBreakerOpenException.class, "Circuit is Open!!")
                .recover(RuntimeException.class, "fallback!!").get();
    }
}

もちろん普通に try-catch で処理しても問題ありません。

動作確認

ソースコードの全容は以下に配置しています。
https://github.com/d-yosh/spring-boot-resilience4j-example

リングバッファサイズが5、半分失敗した段階で Open 状態になるように設定し、Open 状態は10秒間継続します。
実装はこれまで掲載した処理と同様です。

以下のように、処理が成功する呼び出しを2回実行後、3回失敗させるようにすると、Circuit が Open 状態になります。
ここで、成功するはずの呼び出しを実行してみると、失敗してフォールバック処理が実行されていることがわかります。

$ curl http://localhost:8080/circuit/func?str=hoge
success!!
$ curl http://localhost:8080/circuit/func?str=hoge
success!!
$ curl http://localhost:8080/circuit/func?str=hoge
fallback!!
$ curl http://localhost:8080/circuit/func
fallback!!
$ curl http://localhost:8080/circuit/func
fallback!!
$ curl http://localhost:8080/circuit/func?str=hoge
Circuit is Open!!

RateLimiter

単位時間あたりの実行数を制限することができます。

単位時間を1サイクルとし、1サイクルで実行できる数を制限しています。
1サイクルで実行できる上限を超えた場合は、次に実行可能なサイクルを計算し、それまで待機させます。
待機する時間がタイムアウト時間を超えた場合はタイムアウトし、RequestNotPermittedが発生します。

なお、次に実行可能なサイクルを計算した時点で、タイムアウト時間を超えるかどうかは判定できていますが、すぐに例外をスローするのではなく、タイムアウト時間が経過するまでは待たせるような動きをするようです。

設定

application.ymlで設定できます。
複数の RateLimiter を定義することもできます。

resilience4j:
    ratelimiter:
        limiters:
            limiterA: # RateLimiter の名称
                limit-for-period: 1 # 単位時間あたりに実行可能な処理数
                limit-refresh-period-in-millis: 10000 # 単位時間(ミリ秒)
                timeout-in-millis: 10000 # タイムアウト時間(ミリ秒)
            limiterB:
                # 省略・・・

実装

CircuitBreaker と同じで、Spring AOP を利用する方法と、Functional に書く方法があります。
実装方法も CircuitBreaker が RateLimiter に置き換わった感じです。

Spring AOP

クラス or メソッドに @ReteLimiter(name = "hogehoge") と指定することで、RateLimiter が有効になります。
クラスに指定した場合には、すべての public メソッドで RateLimiter が有効になります。
name には appliction.ymlで設定した RateLimiter の名称を指定します。

@Service
@RateLimiter(name = "limiterB")
public class RateLimiterService {
    public String func() {
        return LocalDateTime.now().toString();
    }
}

呼び出す側は特に何も考えず、メソッドを実行するだけでOKです。

@RestController
@RequestMapping("/ratelimiter")
public class RateLimiterController {
    private final RateLimiterService service;

    public RateLimiterController(RateLimiterService service) {
        this.service = service;
    }

    @GetMapping("aop")
    public String aop() {
        return service.aop();
    }
}

Functional に書く方法

呼び出される側は特になにも考えずに実装します。

@Service
public class RateLimiterService {
    public String func() {
        return LocalDateTime.now().toString();
    }
}

呼び出す側は RateLimiter の decorate〜メソッドを利用して、呼び出すメソッドをデコレートします。

@RestController
@RequestMapping("/ratelimiter")
public class RateLimiterController {
    private final RateLimiter rateLimiter;
    private final RateLimiterService service;

    public RateLimiterController(RateLimiterRegistry registry, RateLimiterService service) {
        this.rateLimiter = registry.rateLimiter("limiterA");
        this.service = service;
    }

    @GetMapping("func")
    public String func() {
        return Try.ofSupplier(RateLimiter.decorateSupplier(rateLimiter, service::func))
                .recover(RequestNotPermitted.class, "Request Not Permitted!!").get();
    }
}

フォールバック処理

CircuitBreaker のときと同様、自動でフォールバック処理を実行する仕組みはないため、自身で実装する必要があります。
Vavr の Try モナドを利用した方法や、普通の try-catch など、お好みの方法で処理すればOKです。

動作確認

ソースコードの全容は以下に配置しています。
https://github.com/d-yosh/spring-boot-resilience4j-example

単位時間を5秒、タイムアウト時間を1秒、単位時間あたりの実行数を1としています。
複数のリクエストを同時に送付すれば、失敗するリクエストがあります。
(3つ同時にリクエストすれば、少なくとも1つは必ず失敗します。)

$ curl http://localhost:8080/ratelimiter/func
2019-01-22T23:09:35.612
$ curl http://localhost:8080/ratelimiter/func
Request Not Permitted!!

所感

Hystrix は使ったことがないのでそちらとの比較はできませんが、Resilience4j は簡単な設定、実装で実現できるなーという印象です。
他の機能も試してみたい。

24
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
16