spring-boot

Spring BootでCircuit Breaker(Hystrix)を使ってみる

Cicuit Breakerは、マイクロサービスで一部のサービスが落ちたときに、障害が連鎖しないようアクセスを遮断して切り離すための仕組み。
Circuit Breakerに関して、詳しくはここ

今回はHystrixをSpringから使ってみます。

環境

Java 8
Spring Boot 1.5.7.RELEASE

まずやってみる

サンプルアプリの概要

クライアントからFrontendAPI(localhost:8090/hello)を叩くと現在時刻が返ってくるシンプルなAPIです。
FrontendAPIは内部でさらにBackendAPI(localhost:8080/time)を叩いて現在時刻を取得する仕組みです。
今回はここにHystrixを導入して、BackendAPIに障害が発生したときも、FrontendAPIに障害が伝搬せずに継続できるような仕組みを作ってみます。

image.png

  • Backend API
    • 概要:現在時刻を返すだけ
    • ホスト:localhost:8080
    • エンドポイント:/time
  • Frontend API
    • 概要:アプリケーション1を叩いて返ってきた内容をそのまま返す
      • app1が落ちている or 3秒以上レスポンスが返ってこなかったら、保持しておいた最後に成功したときのレスポンスを返す (ここにHystrixを使う)
    • ホスト:localhost:8090
    • エンドポイント:/hello

作る

まずBackendAPI。
Spring InitializerでWebにチェックを入れてプロジェクト作成。
BackendControllerを下記の内容で作成。

@RestController
@RequestMapping("/time")
public class BackendController {

    @GetMapping
    public String app1() throws InterruptedException {
        String now = LocalDateTime.now().toString();
        return now;
    }
}

次にFrontendAPI。
Spring InitializerでWeb, lombok, Hystrixにチェックを入れてプロジェクト作成。
FrontendServiceを以下の内容で作成。
オプションなどの説明はコメントに書いた。

@Service
public class FrontendService {

    private String lastResponse = "error";

    @HystrixCommand(
            // エラー時、もしくはサーキットがOPEN時に代わりに呼ばれるメソッド
            fallbackMethod = "fallback",
            commandProperties = {
                // サーキットがオープンになるエラー回数。デフォルトは20
                @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "5"),
                // サーキットがオープンになった後、指定したミリ秒待って再度リクエストを受け付ける。再度リクエストして成功すればサーキットがクローズする。デフォルトは5000
                @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000"),
                // 指定した時間レスポンスが返ってこなかったらエラーとして処理する
                @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000"),
                // サーキットがオープンになるエラー率。デフォルトは50%. 
                @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50")
            }
    )
    public String get() {
        System.out.println("call time api.");

        RestTemplate restTemplate = new RestTemplate();
        URI uri = URI.create("http://localhost:8080/time");
        String response = restTemplate.getForObject(uri, String.class);

        // lastResponseフィールドにレスポンスを保持する
        lastResponse = response;

        return response;
    }

    public String fallback() {
        // 最後に成功したときのレスポンスを返す
        return lastResponse;
    }
}

FrontendControllerを下記の内容で作成

@RestController
@RequestMapping("/hello")
@RequiredArgsConstructor
@EnableCircuitBreaker
public class FrontendController {

    private final FrontendService service;

    @GetMapping
    public String hello() {
        return service.get();
    }
}

動かしてみる

両方のAPIを動かした状態で、FrontendAPIを叩く

$ curl "http://localhost:8090/hello"
2017-12-02T15:53:02.345

現在時刻が返ってくる。

アプリケーションのコンソールには下記のログが出ている。

call time api.

では、BackendAPIを落とした状態で10回くらい叩いてみる

$ curl "http://localhost:8090/hello"
2017-12-02T15:53:02.345
$ curl "http://localhost:8090/hello"
2017-12-02T15:53:02.345
$ curl "http://localhost:8090/hello"
2017-12-02T15:53:02.345
$ curl "http://localhost:8090/hello"
2017-12-02T15:53:02.345
$ curl "http://localhost:8090/hello"
2017-12-02T15:53:02.345
$ curl "http://localhost:8090/hello"
2017-12-02T15:53:02.345
$ curl "http://localhost:8090/hello"
2017-12-02T15:53:02.345
$ curl "http://localhost:8090/hello"
2017-12-02T15:53:02.345
$ curl "http://localhost:8090/hello"
2017-12-02T15:53:02.345
$ curl "http://localhost:8090/hello"
2017-12-02T15:53:02.345

10回叩いても同じ時刻が返ってきている。BackendAPIが落ちているので、fallbackメソッドが呼ばれて、最後に成功したときのレスポンスが返っている。
ログを見てみるとBackendAPIを5回叩いた形跡が見られる。

call time api.
fallback.
call time api.
fallback.
call time api.
fallback.
call time api.
fallback.
call time api.
fallback.
fallback.
fallback.
fallback.
fallback.
fallback.

これは@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "5"),で設定したとおり、5回連続でエラーが返ったその時点でサーキットがオープンになり、その後はBackendAPIは叩かれずにfallbackが呼ばれている。

今度はBackendAPIに3秒間のスリープ処理(Thread.sleep(3000))を入れて、わざとタイムアウトするようにしてみます。
また10回ほど叩いてみます。

$ curl "http://localhost:8090/hello"
2017-12-02T15:55:02.385
$ curl "http://localhost:8090/hello"
2017-12-02T15:55:02.385
$ curl "http://localhost:8090/hello"
2017-12-02T15:55:02.385
$ curl "http://localhost:8090/hello"
2017-12-02T15:55:02.745
$ curl "http://localhost:8090/hello"
2017-12-02T15:55:02.745
$ curl "http://localhost:8090/hello"
2017-12-02T15:55:02.745
$ curl "http://localhost:8090/hello"
2017-12-02T15:55:02.745
$ curl "http://localhost:8090/hello"
2017-12-02T15:55:02.745
$ curl "http://localhost:8090/hello"
2017-12-02T15:55:02.745
$ curl "http://localhost:8090/hello"
2017-12-02T15:55:02.745

1~3回目までは最後に成功したときの時間が返っています。
4回目は、ちょうど1回目に送ったリクエストが返ってきて、キャッシュの値が更新されたのか、若干時刻が新しくなっています。
ログを見てみると先程と同様に6回目からBackendAPIを叩かなくなっています。

call time api.
fallback.
call time api.
fallback.
call time api.
fallback.
call time api.
fallback.
call time api.
fallback.
fallback.
fallback.
fallback.
fallback.
fallback.

所感

  • オプション等はここが詳しい
  • Hystrixの設定、application.ymlに書きたい

参考