覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

FailSafeによるサーキットブレーカーパターン

クラウド、マイクロサービスなどのアーキテクチャとともにサーキットブレーカーというパターンがよく取り上げられます。

詳細は、上記リンク先などを参照していただくのがよいです。

超大雑把にいうと、クラウドのサービスなどを利用する際に、
クライアントから見てリクエスト先のサービスはネットワーク越し(インターネット越し)のリモート呼び出しになっています。

リモート呼び出しはローカルの呼び出しに比べて、色々な理由によりクライアントにエラーが返ってくる可能性が高まります。
クライアントはエラーが返ってくる可能性を織り込んでおく必要があり、リトライ(再試行)は必須となります。

が、リクエスト先のサービスが過負荷の状態に陥っていて、複数クライアントからのリクエストを上手くさばけずに、
エラーを返している状況では、クライアントからの頻繁なリトライは余計に問題を悪化させる可能性があります。

クライアントとクラウドサービスの間にブレーカーを設けるのがサーキットブレーカーパターンです。
リクエスト先から特定のエラーが複数回連続で返された場合は、一旦ブレーカーをおろして、サービスへのリクエストを遮断(クラウドサービスへリクエストを送らずして失敗させる)というような動作をします。
その後一定時間経過後に、クラウドサービスへのリクエスト経路を復活させ、リクエストを送りながら正常になったかどうかを判断して、正常と見なされれば元の状態に戻します。

と書いておいてなんですが、文章だとわかりにくいので、先ほどのリンク先などを見てもらうのが良いです。


サーキットブレーカーパターンは以下の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

FailSafeCircuitBreakerは以下のように扱います

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を組み合わせたり、もっと細かな制御をすることが可能です。


関連エントリ