spring-boot で CircuitBreaker を試してみた
CircuitBreaker Patternを導入したくて少し調べたら Netfix の Hystrix というライブラリが良さそうであったのでspring-bootで利用するために少し試したことをまとめておく。
そもそもCircuitBreakerとは、そのままの英語だと、電源回路の遮断機という意味ですが、今回の意味では障害検知・検出のための装置(オブジェクトやソフトウェア的な仕組み)を導入して一部の障害が他の障害を引き起こしシステム全体の障害に広がることを遮断することです。詳しくは、Martin fowlerさんが言っています。
いろいろ調べると、spring-boot でも Netflix の Hystrix を使えるようです。
まずは、簡単に試してみた。
コード変更内容は、CircuitBreakerに関する処理のHookをHystrixCommandExecutionHook
を使って登録して、ログを追加してわかりやすいようにした。
gs-circuit-breaker
のプロジェクトの内容は、以下の図のようになっている。bookstoreのAPIである:8090/recommend
が停止している場合 に、readingのAPIがエラーになるため、bookstoreへの呼び出し部分にCircuitBreakerを適用して、bookstoreが問題になった場合(例えば、ネットワーク的な問題が発生した場合のコネクションエラーやタイムアウトなど)に回避する仕組みを導入することです。
そして、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
の場合は、どんな振る舞いをするのか確認してみるため、bookstore
、reading
の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を回避して応答することになりました。
このように、CircuitBreakerは、外部のサービスと連携するような場合、外部サービスの問題や障害で自分のサービスのスレッドプールなどを使い切ることによりシステム全体の応答が遅延して全体的なサービス障害につながってしまうなどの影響を最小化することに力を発揮できますし、 例えば、Fallback側ではキャッシュを呼び出し一時的に古い情報を提供することなども可能でしょう。
今後、@HystrixCommand
アノテーションの属性@HystrixProperty
を使って幾つかCircuitBreakerの条件を設定できるようなので、今後はプロパティの変更して試してみます。