クラウド、マイクロサービスなどのアーキテクチャとともにサーキットブレーカー
というパターンがよく取り上げられます。
詳細は、上記リンク先などを参照していただくのがよいです。
超大雑把にいうと、クラウドのサービスなどを利用する際に、
クライアントから見てリクエスト先のサービスはネットワーク越し(インターネット越し)のリモート呼び出しになっています。
リモート呼び出しはローカルの呼び出しに比べて、色々な理由によりクライアントにエラーが返ってくる可能性が高まります。
クライアントはエラーが返ってくる可能性を織り込んでおく必要があり、リトライ(再試行)は必須となります。
が、リクエスト先のサービスが過負荷の状態に陥っていて、複数クライアントからのリクエストを上手くさばけずに、
エラーを返している状況では、クライアントからの頻繁なリトライは余計に問題を悪化させる可能性があります。
クライアントとクラウドサービスの間にブレーカーを設けるのがサーキットブレーカーパターンです。
リクエスト先から特定のエラーが複数回連続で返された場合は、一旦ブレーカーをおろして、サービスへのリクエストを遮断(クラウドサービスへリクエストを送らずして失敗させる)というような動作をします。
その後一定時間経過後に、クラウドサービスへのリクエスト経路を復活させ、リクエストを送りながら正常になったかどうかを判断して、正常と見なされれば元の状態に戻します。
と書いておいてなんですが、文章だとわかりにくいので、先ほどのリンク先などを見てもらうのが良いです。
サーキットブレーカーパターンは以下の3つのモードから成り立っています
Closed
- クライアントからのリクエスト(要求)は、呼び出し先のサービスへ送られます。
- 直近のエラーの回数が特定の閾値を超えると、
Open
モードに遷移します。
Open
- クライアントからのリクエスト(要求)は、すぐにエラー扱いで返されます。リクエストは呼び出し先のサービスへは送られません。
Open
モードになった時点でタイマーを開始し、タイマーの期限が切れると、Half Open
モードになります
Half Open
- 呼び出し先が復旧したかどうかを確認しながら別のモードに遷移させるためのものです
- 呼び出し先が復旧した(リクエストが一定回数成功した等の)場合に、
Closed
モードに遷移します - 呼び出し先が障害状態である(リクエストに対してサービスからエラーが返された)場合に、
Open
モードに遷移します
これらのモードの組み合わせや遷移により、障害時に呼び出し先サービスへ余計な負荷をかけてしまう状況を避けることができます。
FailSafeが提供するサーキットブレーカー
以前のエントリでFailSafe
というライブラリによるJavaでのリトライ処理(再試行処理)方法を紹介しました。
FailSafe
はリトライ処理だけではなく、サーキットブレーカー用のCircuitBreaker
というクラスを用意してくれています。
今回は、このCircuitBreaker
を簡単に試して見ます。
呼び出される側のサービス
今回のコード例では、実際にリモート呼び出しは行わず、それの代わりに適当なクラス(以下)を用意して、
これを呼び出すことでリモート呼び出しを模倣します。
このクラスは、正常なモード と 障害モード の2パターンを持っており
正常モードでは呼び出しは成功しますが、障害モードでは呼び出しは失敗します。
また、呼び出しが行われたこと自体をロギングしており、ログから呼び出されたかどうかを知ることができます。
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.concurrent.TimeUnit; public final class APIServer { private static final Logger logger = LoggerFactory.getLogger(APIServer.class); private static ExecMode execMode = ExecMode.SUCCESS; public static Result request() throws IOException { logger.info("**** API server processing in progress..........[mode: {}]", execMode); waitTime(1, TimeUnit.SECONDS); if (execMode == ExecMode.FAILED) { throw new IOException("APIServer process failed"); } return Result.OK; } public static void setExecMode(ExecMode mode) { execMode = mode; } public static ExecMode getExecMode() { return execMode; } private static void waitTime(long value, TimeUnit timeUnit) { try { Thread.sleep(timeUnit.toMillis(value)); } catch (InterruptedException e) { } } enum ExecMode { SUCCESS, // 正常なモード FAILED // 障害モード } }
FailSafeのCircuitBreaker
FailSafe
の CircuitBreaker
は以下のように扱います
CircuitBreaker breaker = new CircuitBreaker() .failOn(IOException.class) // エラーとみなす例外 .withFailureThreshold(3) // エラーが何回発生したらOpenモードに遷移するかの閾値 .withSuccessThreshold(2) // 成功が何回発生したらClosedモードに遷移するかの閾値 .onOpen(() -> logger.warn("##### CircuitBreaker on Open")) // Openモードに遷移したことの通知 .onHalfOpen(() -> logger.warn("##### CircuitBreaker on Half Open")) // Half Openモードに遷移したことの通知 .onClose(() -> logger.warn("##### CircuitBreaker on Close")) // Closedモードに遷移したことの通知 .withDelay(5, TimeUnit.SECONDS); // Closedモードに遷移してからHalf Openモードに遷移するまでの時間
このサーキットブレーカーの条件利用して実際の処理呼び出しをするには以下のようになります
Result result = Failsafe.with(breaker) .withFallback(Result.NG) .onSuccess((o, ctx) -> logger.info("onSuccess")) .onFailure((o, th, ctx) -> logger.error("onFailure [{}]", th.getClass().getName())) .get(() -> APIServer.request());
上記のCircuitBreaker
の条件で実行すると以下のようになります
IOException
が返されたらエラーとみなす- エラーカウントが3回に達したら
Open
モードに遷移する Open
モードに遷移したら、5秒間はそのままのモード(その間に呼び出されても即時に失敗を返す)Half Open
モードで、呼び出し先サービスから2回成功が返されたらClosed
モードに遷移する
CircuitBreakerを利用して実行してみる
上記のサーキットブレーカーの条件でいくつか実際に処理を実行してみます。
ClosedモードからOpenモードへの遷移
サンプルコード
import net.jodah.failsafe.CircuitBreaker; import net.jodah.failsafe.Failsafe; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.concurrent.TimeUnit; public class BreakerSample1 { private static final Logger logger = LoggerFactory.getLogger(BreakerSample1.class); public static void main(String[] args) { CircuitBreaker breaker = new CircuitBreaker() .failOn(IOException.class) .withFailureThreshold(3) .withSuccessThreshold(2) .onOpen(() -> logger.warn("##### CircuitBreaker on Open")) .onHalfOpen(() -> logger.warn("##### CircuitBreaker on Half Open")) .onClose(() -> logger.warn("##### CircuitBreaker on Close")) .withDelay(5, TimeUnit.SECONDS); // 呼び出し先のAPIServerは処理が失敗するモード APIServer.setExecMode(APIServer.ExecMode.FAILED); execWithCircuitBreaker(breaker); execWithCircuitBreaker(breaker); execWithCircuitBreaker(breaker); waitTime(1, TimeUnit.SECONDS); execWithCircuitBreaker(breaker); execWithCircuitBreaker(breaker); } private static Result execWithCircuitBreaker(CircuitBreaker breaker) { Result result = Failsafe.with(breaker) .withFallback(Result.NG) .onSuccess((o, ctx) -> logger.info("onSuccess")) .onFailure((o, th, ctx) -> logger.error("onFailure [{}]", th.getClass().getName())) .get(() -> APIServer.request()); logger.info("execWithCircuitBreaker [server-mode: {} -> return: {}]", APIServer.getExecMode(), result); return result; } private static void waitTime(long value, TimeUnit timeUnit) { logger.info("----------------------------------"); logger.info("wait....[{} {}]", value, timeUnit); logger.info("----------------------------------"); try { Thread.sleep(timeUnit.toMillis(value)); } catch (InterruptedException e) { } } }
上記サンプルコードですが、何をやっているかは以下の通りです
- APIServerを障害モードにして必ず
IOException
が返ってくるようにします。 - 3回リクエスト送りますが全てエラー扱いになります
- 3回エラーとなったことで
Open
モードになります - 5秒間は
Open
モードのままなので、その後の2回の呼び出しはAPIServerへのリクエスト無しでエラー扱いとなります
実行結果
21:47:27.771 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED] 21:47:28.780 [main] ERROR net.yyuki.breaker.BreakerSample1 - onFailure [java.io.IOException] 21:47:28.780 [main] INFO net.yyuki.breaker.BreakerSample1 - execWithCircuitBreaker [server-mode: FAILED -> return: NG] 21:47:28.780 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED] 21:47:29.780 [main] ERROR net.yyuki.breaker.BreakerSample1 - onFailure [java.io.IOException] 21:47:29.781 [main] INFO net.yyuki.breaker.BreakerSample1 - execWithCircuitBreaker [server-mode: FAILED -> return: NG] 21:47:29.781 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED] 21:47:30.788 [main] WARN net.yyuki.breaker.BreakerSample1 - ##### CircuitBreaker on Open 21:47:30.788 [main] ERROR net.yyuki.breaker.BreakerSample1 - onFailure [java.io.IOException] 21:47:30.788 [main] INFO net.yyuki.breaker.BreakerSample1 - execWithCircuitBreaker [server-mode: FAILED -> return: NG] 21:47:30.788 [main] INFO net.yyuki.breaker.BreakerSample1 - ---------------------------------- 21:47:30.788 [main] INFO net.yyuki.breaker.BreakerSample1 - wait....[1 SECONDS] 21:47:30.788 [main] INFO net.yyuki.breaker.BreakerSample1 - ---------------------------------- 21:47:31.793 [main] INFO net.yyuki.breaker.BreakerSample1 - execWithCircuitBreaker [server-mode: FAILED -> return: NG] 21:47:31.793 [main] INFO net.yyuki.breaker.BreakerSample1 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]
##### CircuitBreaker on Open
のログがOpen
モードに遷移したことを意味しています。
最後に2行以下のようにログが出ているのは、APIServerへのリクエスト無しでエラー扱いとなっていることを意味しています。(Open
モードなので)
execWithCircuitBreaker [server-mode: FAILED -> return: NG] execWithCircuitBreaker [server-mode: FAILED -> return: NG]
APIServerへのリクエストが行われていれば以下のログが出るはずなので
**** API server processing in progress..........
OpenモードからHalf Openモードへの遷移
サンプルコード
必要な部分以外省略しています。(省略している部分は基本的にBreakerSample1
と同じです)
public class BreakerSample2 { private static final Logger logger = LoggerFactory.getLogger(BreakerSample2.class); public static void main(String[] args) { CircuitBreaker breaker = new CircuitBreaker() .failOn(IOException.class) .withFailureThreshold(3) .withSuccessThreshold(2) .onOpen(() -> logger.warn("##### CircuitBreaker on Open")) .onHalfOpen(() -> logger.warn("##### CircuitBreaker on Half Open")) .onClose(() -> logger.warn("##### CircuitBreaker on Close")) .withDelay(5, TimeUnit.SECONDS); // 呼び出し先のAPIServerは処理が失敗するモード APIServer.setExecMode(APIServer.ExecMode.FAILED); execWithCircuitBreaker(breaker); execWithCircuitBreaker(breaker); execWithCircuitBreaker(breaker); waitTime(1, TimeUnit.SECONDS); execWithCircuitBreaker(breaker); execWithCircuitBreaker(breaker); waitTime(6, TimeUnit.SECONDS); // 6秒待ってHalf Openへ遷移 execWithCircuitBreaker(breaker); execWithCircuitBreaker(breaker); } //(省略) }
先ほどのサンプルとの違いは、6秒待ってHalf Open
モードに遷移させている部分です。
その直後の呼び出しはAPIServerへのリクエストとして扱われますが、APIServerが障害モードのままなのでエラーが返されます。
結局、再度Open
モードに戻ってしまい、さらにそのあとの呼び出しはAPIServerへのリクエストまで行かずに、即座にエラーとして扱われています。
実行結果
22:00:30.208 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED] 22:00:31.215 [main] ERROR net.yyuki.breaker.BreakerSample2 - onFailure [java.io.IOException] 22:00:31.215 [main] INFO net.yyuki.breaker.BreakerSample2 - execWithCircuitBreaker [server-mode: FAILED -> return: NG] 22:00:31.215 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED] 22:00:32.217 [main] ERROR net.yyuki.breaker.BreakerSample2 - onFailure [java.io.IOException] 22:00:32.217 [main] INFO net.yyuki.breaker.BreakerSample2 - execWithCircuitBreaker [server-mode: FAILED -> return: NG] 22:00:32.218 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED] 22:00:33.224 [main] WARN net.yyuki.breaker.BreakerSample2 - ##### CircuitBreaker on Open 22:00:33.224 [main] ERROR net.yyuki.breaker.BreakerSample2 - onFailure [java.io.IOException] 22:00:33.224 [main] INFO net.yyuki.breaker.BreakerSample2 - execWithCircuitBreaker [server-mode: FAILED -> return: NG] 22:00:33.224 [main] INFO net.yyuki.breaker.BreakerSample2 - ---------------------------------- 22:00:33.224 [main] INFO net.yyuki.breaker.BreakerSample2 - wait....[1 SECONDS] 22:00:33.224 [main] INFO net.yyuki.breaker.BreakerSample2 - ---------------------------------- 22:00:34.230 [main] INFO net.yyuki.breaker.BreakerSample2 - execWithCircuitBreaker [server-mode: FAILED -> return: NG] 22:00:34.230 [main] INFO net.yyuki.breaker.BreakerSample2 - execWithCircuitBreaker [server-mode: FAILED -> return: NG] 22:00:34.230 [main] INFO net.yyuki.breaker.BreakerSample2 - ---------------------------------- 22:00:34.230 [main] INFO net.yyuki.breaker.BreakerSample2 - wait....[6 SECONDS] 22:00:34.231 [main] INFO net.yyuki.breaker.BreakerSample2 - ---------------------------------- 22:00:40.234 [main] WARN net.yyuki.breaker.BreakerSample2 - ##### CircuitBreaker on Half Open 22:00:40.234 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED] 22:00:41.236 [main] WARN net.yyuki.breaker.BreakerSample2 - ##### CircuitBreaker on Open 22:00:41.236 [main] ERROR net.yyuki.breaker.BreakerSample2 - onFailure [java.io.IOException] 22:00:41.236 [main] INFO net.yyuki.breaker.BreakerSample2 - execWithCircuitBreaker [server-mode: FAILED -> return: NG] 22:00:41.236 [main] INFO net.yyuki.breaker.BreakerSample2 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]
以下ログが Half Open
モードへ遷移したことを意味しています
##### CircuitBreaker on Half Open
最後の以下のログが、APIServerへリクエストしたがまた失敗してしまったので、Open
モードに戻ってしまったことを意味しています。
22:00:40.234 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED] 22:00:41.236 [main] WARN net.yyuki.breaker.BreakerSample2 - ##### CircuitBreaker on Open 22:00:41.236 [main] ERROR net.yyuki.breaker.BreakerSample2 - onFailure [java.io.IOException] 22:00:41.236 [main] INFO net.yyuki.breaker.BreakerSample2 - execWithCircuitBreaker [server-mode: FAILED -> return: NG] 22:00:41.236 [main] INFO net.yyuki.breaker.BreakerSample2 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]
Half OpenモードからClosedモードへの遷移
サンプルコード
public class BreakerSample3 { private static final Logger logger = LoggerFactory.getLogger(BreakerSample3.class); public static void main(String[] args) { CircuitBreaker breaker = new CircuitBreaker() .failOn(IOException.class) .withFailureThreshold(3) .withSuccessThreshold(2) .onOpen(() -> logger.warn("##### CircuitBreaker on Open")) .onHalfOpen(() -> logger.warn("##### CircuitBreaker on Half Open")) .onClose(() -> logger.warn("##### CircuitBreaker on Close")) .withDelay(5, TimeUnit.SECONDS); // 呼び出し先のAPIServerは処理が失敗するモード APIServer.setExecMode(APIServer.ExecMode.FAILED); execWithCircuitBreaker(breaker); execWithCircuitBreaker(breaker); execWithCircuitBreaker(breaker); waitTime(1, TimeUnit.SECONDS); execWithCircuitBreaker(breaker); waitTime(6, TimeUnit.SECONDS); // 6秒待ってHalf Openへ遷移 // 呼び出し先のAPIServerは 処理がが成功するモード APIServer.setExecMode(APIServer.ExecMode.SUCCESS); execWithCircuitBreaker(breaker); execWithCircuitBreaker(breaker); waitTime(1, TimeUnit.SECONDS); execWithCircuitBreaker(breaker); } //(省略) }
先ほどの例との違いは、Half Open
モードになった後にAPIServerが成功の結果を返すように障害モードから復旧させている点です。
これにより、その後のAPIServerへのリクエストが成功し、これが2回達成されることでClosed
モードに遷移しています。
実行結果
22:06:20.965 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED] 22:06:21.973 [main] ERROR net.yyuki.breaker.BreakerSample3 - onFailure [java.io.IOException] 22:06:21.973 [main] INFO net.yyuki.breaker.BreakerSample3 - execWithCircuitBreaker [server-mode: FAILED -> return: NG] 22:06:21.973 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED] 22:06:22.977 [main] ERROR net.yyuki.breaker.BreakerSample3 - onFailure [java.io.IOException] 22:06:22.977 [main] INFO net.yyuki.breaker.BreakerSample3 - execWithCircuitBreaker [server-mode: FAILED -> return: NG] 22:06:22.977 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED] 22:06:23.982 [main] WARN net.yyuki.breaker.BreakerSample3 - ##### CircuitBreaker on Open 22:06:23.982 [main] ERROR net.yyuki.breaker.BreakerSample3 - onFailure [java.io.IOException] 22:06:23.982 [main] INFO net.yyuki.breaker.BreakerSample3 - execWithCircuitBreaker [server-mode: FAILED -> return: NG] 22:06:23.982 [main] INFO net.yyuki.breaker.BreakerSample3 - ---------------------------------- 22:06:23.983 [main] INFO net.yyuki.breaker.BreakerSample3 - wait....[1 SECONDS] 22:06:23.983 [main] INFO net.yyuki.breaker.BreakerSample3 - ---------------------------------- 22:06:24.985 [main] INFO net.yyuki.breaker.BreakerSample3 - execWithCircuitBreaker [server-mode: FAILED -> return: NG] 22:06:24.986 [main] INFO net.yyuki.breaker.BreakerSample3 - ---------------------------------- 22:06:24.986 [main] INFO net.yyuki.breaker.BreakerSample3 - wait....[6 SECONDS] 22:06:24.986 [main] INFO net.yyuki.breaker.BreakerSample3 - ---------------------------------- 22:06:30.988 [main] WARN net.yyuki.breaker.BreakerSample3 - ##### CircuitBreaker on Half Open 22:06:30.988 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: SUCCESS] 22:06:31.989 [main] INFO net.yyuki.breaker.BreakerSample3 - onSuccess 22:06:31.989 [main] INFO net.yyuki.breaker.BreakerSample3 - execWithCircuitBreaker [server-mode: SUCCESS -> return: OK] 22:06:31.989 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: SUCCESS] 22:06:32.990 [main] WARN net.yyuki.breaker.BreakerSample3 - ##### CircuitBreaker on Close 22:06:32.991 [main] INFO net.yyuki.breaker.BreakerSample3 - onSuccess 22:06:32.991 [main] INFO net.yyuki.breaker.BreakerSample3 - execWithCircuitBreaker [server-mode: SUCCESS -> return: OK] 22:06:32.991 [main] INFO net.yyuki.breaker.BreakerSample3 - ---------------------------------- 22:06:32.991 [main] INFO net.yyuki.breaker.BreakerSample3 - wait....[1 SECONDS] 22:06:32.991 [main] INFO net.yyuki.breaker.BreakerSample3 - ---------------------------------- 22:06:33.994 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: SUCCESS] 22:06:34.996 [main] INFO net.yyuki.breaker.BreakerSample3 - onSuccess 22:06:34.997 [main] INFO net.yyuki.breaker.BreakerSample3 - execWithCircuitBreaker [server-mode: SUCCESS -> return: OK]
まとめ
簡単なサンプルではありますが、FailSafe
でサーキットブレーカーパターンが扱えることがわかりました。
実際にはRetryPolicy
を組み合わせたり、もっと細かな制御をすることが可能です。