horiga blog

とあるエンジニアのメモ

spring-boot で CircuitBreaker を試してみた

CircuitBreaker Patternを導入したくて少し調べたら Netfix の Hystrix というライブラリが良さそうであったのでspring-bootで利用するために少し試したことをまとめておく。

そもそもCircuitBreakerとは、そのままの英語だと、電源回路の遮断機という意味ですが、今回の意味では障害検知・検出のための装置(オブジェクトやソフトウェア的な仕組み)を導入して一部の障害が他の障害を引き起こしシステム全体の障害に広がることを遮断することです。詳しくは、Martin fowlerさんが言っています。

martinfowler.com

いろいろ調べると、spring-boot でも Netflix の Hystrix を使えるようです。

github.com

Spring Cloud Netflix

まずは、簡単に試してみた。

github.com

コード変更内容は、CircuitBreakerに関する処理のHookをHystrixCommandExecutionHookを使って登録して、ログを追加してわかりやすいようにした。

gs-circuit-breakerのプロジェクトの内容は、以下の図のようになっている。bookstoreのAPIである:8090/recommendが停止している場合 に、readingのAPIがエラーになるため、bookstoreへの呼び出し部分にCircuitBreakerを適用して、bookstoreが問題になった場合(例えば、ネットワーク的な問題が発生した場合のコネクションエラーやタイムアウトなど)に回避する仕組みを導入することです。

f:id:horiga:20160424234853p:plain

そして、CircuitBreakerの重要なコードは以下のようになります。

@Service
@Slf4j
public class BookService {

    @HystrixCommand(fallbackMethod = "reliable")
    public String readingList() {
        try {
            RestTemplate restTemplate = new RestTemplate();
            URI uri = URI.create("http://localhost:8090/recommended");
            final String reading = restTemplate.getForObject(uri, String.class);
            log.info("(Success in reading list)");
            return reading;
        } catch (Throwable e) {
            log.error("<<Failed>> Reading lists in Book service. cause:{}", e.getMessage());
            throw e;
        }
    }

    public String reliable() {
        log.error("(In fallback method)");
        return "Cloud Native Java (O'Reilly)";
    }

}

まずは正常な場合、つまりCircuitBreakerがCLOSEの場合は、どんな振る舞いをするのか確認してみるため、bookstorereadingのAPPを起動し、curlを使ってreadingのAPIを呼び出します。 結果は以下のようになりました。

// bookstoreとreadingでgradle bootRunを実行
$ gradle bootRun
// reading 側のログ
[nio-8080-exec-3] hello.ReadingApplication                 : [start] /to-read
[nio-8080-exec-3] hello.ReadingApplication                 : [onStart] --- ①
[x-BookService-2] hello.ReadingApplication                 : [onThreadStart] --- ② 
[x-BookService-2] hello.ReadingApplication                 : [onExecutionStart] --- ③
[x-BookService-2] hello.BookService                        : (Success in reading list)
[x-BookService-2] hello.ReadingApplication                 : [onExecutionEmit] value:Spring in Action (Manning), Cloud Native Java (O'Reilly), Learning Spring Boot (Packt)  --- ④
[x-BookService-2] hello.ReadingApplication                 : [onEmit] value:Spring in Action (Manning), Cloud Native Java (O'Reilly), Learning Spring Boot (Packt)  --- ⑤
[x-BookService-2] hello.ReadingApplication                 : [onExecutionSuccess]  --- ⑥
[x-BookService-2] hello.ReadingApplication                 : [onThreadComplete]  --- ⑦
[x-BookService-2] hello.ReadingApplication                 : [onSuccess]  --- ⑧
[nio-8080-exec-3] hello.ReadingApplication                 : [end] /to-read

上記のログをみるとBookSercice#readingListを呼び出す時点(①、②)、スレッドが切り替わりHystrixが管理するスレッドに切り替わっていることがわかる。 当然今回は、bookstoreのAPIは正常にアクセスできるため、Fallbackが発生しない。

今度は、Fallbackを発生させるため、bookstoreのプロセスを停止してもう一度curlでreadingのAPIを呼び出す。

[nio-8080-exec-5] hello.ReadingApplication                 : [start] /to-read
[nio-8080-exec-5] hello.ReadingApplication                 : [onStart]
[x-BookService-3] hello.ReadingApplication                 : [onThreadStart]
[x-BookService-3] hello.ReadingApplication                 : [onExecutionStart]
[x-BookService-3] hello.BookService                        : <<Failed>> Reading lists in Book service. cause:I/O error on GET request for "http://localhost:8090/recommended":Connection refused; nested exception is java.net.ConnectException: Connection refused  --- ①
[x-BookService-3] hello.ReadingApplication                 : [onExecutionError] message:I/O error on GET request for "http://localhost:8090/recommended":Connection refused; nested exception is java.net.ConnectException: Connection refused --- ②
[x-BookService-3] hello.ReadingApplication                 : [onFallbackStart] --- ③
[x-BookService-3] hello.BookService                        : (In fallback method) --- ④
[x-BookService-3] hello.ReadingApplication                 : [onFallbackEmit] value:Cloud Native Java (O'Reilly) --- ⑤
[x-BookService-3] hello.ReadingApplication                 : [onEmit] value:Cloud Native Java (O'Reilly) --- ⑦
[x-BookService-3] hello.ReadingApplication                 : [onFallbackSuccess] --- ⑧
[x-BookService-3] hello.ReadingApplication                 : [onThreadComplete] --- ⑨
[x-BookService-3] hello.ReadingApplication                 : [onSuccess] --- ⑩
[nio-8080-exec-5] hello.ReadingApplication                 : [end] /to-read

上記の通り、BookService#readingListで例外が発生しHystrixで検知され(①、②)、Fallbackを指定したBookService#reliableが自動的に実行されていること(③〜⑧)が確認できます。結局コントローラ側へは成功として応答されていることが確認できました。どうやら想定されている動作がされているようです。

そして、CircuitBreakerの重要な点として、エラーが発生する処理が何度も実行されないように遮断することがあります。ここでbookstoreを停止したまま、多くのアクセスを実行してみました。

// 100回アクセスする
ab -n 100 -c 1 http://localhost:8080/to-read

すると、最初のうちは先程の結果と同じく、BookService#readingListが実行されエラー検出後にFallbackが呼び出される処理がされますが、しばらくすると以下のログのような振る舞いになることが確認できました。

[nio-8080-exec-5] hello.ReadingApplication                 : [start] /to-read
[nio-8080-exec-5] hello.ReadingApplication                 : [onStart]
[nio-8080-exec-5] hello.ReadingApplication                 : [onFallbackStart]
[nio-8080-exec-5] hello.BookService                        : (In fallback method)
[nio-8080-exec-5] hello.ReadingApplication                 : [onFallbackEmit] value:Cloud Native Java (O'Reilly)
[nio-8080-exec-5] hello.ReadingApplication                 : [onEmit] value:Cloud Native Java (O'Reilly)
[nio-8080-exec-5] hello.ReadingApplication                 : [onFallbackSuccess]
[nio-8080-exec-5] hello.ReadingApplication                 : [onSuccess]
[nio-8080-exec-5] hello.ReadingApplication                 : [end] /to-read
[nio-8080-exec-6] hello.ReadingApplication                 : [start] /to-read
[nio-8080-exec-6] hello.ReadingApplication                 : [onStart]
[nio-8080-exec-6] hello.ReadingApplication                 : [onFallbackStart]
[nio-8080-exec-6] hello.BookService                        : (In fallback method)
[nio-8080-exec-6] hello.ReadingApplication                 : [onFallbackEmit] value:Cloud Native Java (O'Reilly)
[nio-8080-exec-6] hello.ReadingApplication                 : [onEmit] value:Cloud Native Java (O'Reilly)
[nio-8080-exec-6] hello.ReadingApplication                 : [onFallbackSuccess]
[nio-8080-exec-6] hello.ReadingApplication                 : [onSuccess]
[nio-8080-exec-6] hello.ReadingApplication                 : [end] /to-read

そうです、ログを見るとBookService#readingListは呼びだされていませんし、スレッドの切り替えも発生せずにFallbackが処理されています。下記の図のようになり、これで問題のあるbookstoreを回避して応答することになりました。

f:id:horiga:20160425000208p:plain

このように、CircuitBreakerは、外部のサービスと連携するような場合、外部サービスの問題や障害で自分のサービスのスレッドプールなどを使い切ることによりシステム全体の応答が遅延して全体的なサービス障害につながってしまうなどの影響を最小化することに力を発揮できますし、 例えば、Fallback側ではキャッシュを呼び出し一時的に古い情報を提供することなども可能でしょう。

今後、@HystrixCommandアノテーションの属性@HystrixPropertyを使って幾つかCircuitBreakerの条件を設定できるようなので、今後はプロパティの変更して試してみます。

Configuration · Netflix/Hystrix Wiki · GitHub