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

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

今回は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](https://qiita-image-store.s3.amazonaws.com/0/48525/1c2e8b91-ecfa-de20-b725-49e5059a6e4d.png)


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

### 作る

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

```java
@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を以下の内容で作成。
オプションなどの説明はコメントに書いた。

```java
@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を下記の内容で作成

```java
@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回叩いた形跡が見られる。

```bash
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を叩かなくなっています。

```bash
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.
```

## 所感
- オプション等は[ここ](https://steeltoe.io/docs/steeltoe-circuitbreaker/)が詳しい
- Hystrixの設定、application.ymlに書きたい

## 参考
- [Spring Cloud Netflix](http://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/2.0.0.M5/single/spring-cloud-netflix.html#_circuit_breaker_hystrix_clients)
- https://steeltoe.io/docs/steeltoe-circuitbreaker/
- [Spring BootでCircuit Breaker(Spring Cloud Netflix Hystrix)を試す](http://pppurple.hatenablog.com/entry/2017/01/11/235814)
- [How to configure Hystrix annotations from property file?](https://stackoverflow.com/questions/35085617/how-to-configure-hystrix-annotations-from-property-file)