Apache Camelでサーキットブレーカーパターン(Circuit Breaker Pattern)


サーキットブレーカーパターン(Circuit Breaker Pattern)

サーキットブレーカーパターン(Circuit Breaker Pattern)はデザインパターンの一つです。
アプリケーションがリモートサービスにアクセスした際に、高負荷やDBのフェイルオーバーなどにより一時的な障害が発生することがあります。これは一時的な障害であり、復旧するまで待てばエラーになることなく処理が成功します。
このようなときに用いられるのがサーキットブレーカーパターンで、障害を検知した場合は復旧するまでの間、アクセスを行わないようにします。
これにより、大量のエラーが発生しない、エラーが発生すると分かっているので処理を継続しない、リモート側に不要な負荷をかけない、などのメリットが生まれます。
負荷が高いとブレーカーを落とすということで、サーキットブレーカーパターンと名付けられています。

サーキットブレーカーは以下の3つの状態を持ちます。

  • CLOSED
    正常な状態。直近のエラー数をカウントし、その回数が閾値を超えるとOPENに移行します。
  • OPEN
    エラーによりリクエストを制限している状態。一定時間経過することでHALF-OPENに移行します。
  • HALF-OPEN
    リモートシステムが障害から回復したかを確認する。少数のリクエストを実行し処理が成功すればCLOSED(正常)に移行します。

サーキットブレーカーパターンはRelease It!という書籍で2007年に発表(?)されました(私はチラ見しかしていませんが)。Relase It!は日本語版も発売されており、2018年は以下のSecond Editionも発売されました(日本語版はなし)。

また、サーキットブレーカーパターンはMartin Fowler氏のブログにも記載があります。

本記事のその2もあります。
Apache Camelでサーキットブレーカーパターン(Circuit Breaker Pattern)その2(Hystrix Dashboardを試す)

Apache Camelでのサーキットブレーカーパターンを使用する

Apache Camelでは、以下の2つのサーキットブレーカーを利用することができます。

  • Circuit Breaker Load Balancer
  • Hystrix EIP

Circuit Breaker Load Balancerは、サーキットブレーカーパターンを実現する簡易な実装になります。Hystrix EIPと比較し機能は少ないですが、容易に利用できます。ただし、v2.23.0ではDeprecatedになっていますので、次のHystrix EIPを使用することになります。
Hystrix EIPは、NetFlixが開発したHystrixというフォールトトレランスのためのライブラリをCamelで用いるためのコンポーネントです。Hystrixはサーキットブレーカーとしても多くの機能を持ちます。

Hystrix EIPについて、以降でサンプルプログラムを作成し動かしてみます。

また、Circuit Breaker Load BalancerとHystrix EIPの公式サイトは以下になります。

※以下のソースを確認する限り、Load Balancerのサーキットブレーカーは2.19.0からDeprecatedになっているようです。
https://github.com/apache/camel/blob/camel-2.19.x/camel-core/src/main/java/org/apache/camel/model/loadbalancer/CircuitBreakerLoadBalancerDefinition.java

Hystrix EIP

それではHystrix EIPを試してみましょう。
まずは以下のライブラリを追加します。x.x.xは使用しているCamelのバージョンを指定します。

pom.xml
<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-hystrix</artifactId>
    <version>x.x.x</version>
</dependency>

Hystrixには多くのオプションがあり、以下のように設定することができます。

  • タイムアウトを5秒に設定する(executionTimeoutInMilliseconds)。
  • 状態がオープンになったときに再びリクエストする前にサーキットブレーカが5秒間待機する。待機中はリクエストを送信しないが、consumeは継続される。5秒後もリクエストがエラーになる場合は再度5秒間待機する(circuitBreakerSleepWindowInMilliseconds)。
  • エラーを検知するタイムウィンドウを5秒間に設定する(circuitBreakerSleepWindowInMilliseconds)。
  • 状態をオープンにする際のエラーの閾値を20%に設定する(circuitBreakerErrorThresholdPercentage)。
  • ウィンドウ内でOpenに移行を検討する最小のリクエスト数を2回に設定する(circuitBreakerRequestVolumeThreshold)。
                        .hystrixConfiguration()
                            .circuitBreakerErrorThresholdPercentage(20)
                            .executionTimeoutInMilliseconds(5000)
                            .circuitBreakerSleepWindowInMilliseconds(5000)
                            .circuitBreakerRequestVolumeThreshold(2)

XML DSLの場合は以下の記述になります。

        <route id="main_route">
            <from uri="timer:trigger?period=300" />
                <log message="Client request" />
                <hystrix>
                    <hystrixConfiguration circuitBreakerErrorThresholdPercentage="20" executionTimeoutInMilliseconds="5000" circuitBreakerSleepWindowInMilliseconds="5000" circuitBreakerRequestVolumeThreshold="2" />
                    <to uri="http4://localhost:8888/hello"/>
                    <onFallback>
                        <transform>
                            <constant>Fallback message</constant>
                        </transform>
                    </onFallback>
                </hystrix>
                <log message="response: ${body}" />
        </route>

先ほどのサーキットブレーカーの設定を試すルートとして以下を作成します。
このルートではTimerコンポーネントで300msごとに処理されます。
"to("http4://localhost:8888/hello")"でリモートのシステムへリクエストするようになっています。

            public void configure() throws Exception {
                from("timer:trigger?period=300")
                    .log("Client request")
                    .hystrix()
                        .hystrixConfiguration()
                            .circuitBreakerErrorThresholdPercentage(20)
                            .executionTimeoutInMilliseconds(5000)
                            .circuitBreakerSleepWindowInMilliseconds(5000)
                            .circuitBreakerRequestVolumeThreshold(2)
                        .end()
                        .to("http4://localhost:8888/hello")
                    .end()
                    .log("response: ${body}");
            }

フォールバック(fallback)

Hystrixではフォールバック(fallback)にも対応しています。
フォールバックは日本語では縮退運転(かな?)のことで、機能を制限して動かすことです。
Hystrixではエラーが閾値を超えると、onFallbackメソッドが呼ばれます。上述のソースでは、Fallback messageをメッセージのBODYに格納するようになっています。Closed状態では、コマンドを実行するスレッドとフォールバックのスレッドは別になっており、コマンドの実行に影響を及ぼさないようになっています。

また、フォールバックでは、onFallbackメソッドとonFallbackViaNetworkメソッドがあります。
同一アプリ内でフォールバックを処理する場合はonFallbackメソッド、リモートのサービスを呼び出す場合はonFallbackViaNetworkメソッドを使用します。
onFallbackViaNetworkメソッドでは、スレッドを完全に分離するために独自のスレッドプールに持ちます。これによりOPENの状態でも完全にスレッドが分離されます。

onFallbackViaNetworkメソッドでログを出力すると、以下のようにスレッド名が"hystrix-CamelHystrix-10"となり、別スレッドであることが分かります。

[2019-02-06 20:32:38.985], [INFO ], route1, hystrix-CamelHystrix-10, route1, Fallback message

プロパティ

使用されることが多いプロパティの一部を下表に記載します。

プロパティ名 デフォルト値 タイプ 説明
circuitBreakerErrorThresholdPercentage 50 Integer このプロパティは、サーキットがトリップしてフォールバックロジックへのショート要求を開始するエラー率を設定します。
circuitBreakerRequestVolumeThreshold 20 Integer このプロパティは、ウィンドウ内でサーキットをトリップ(オープン)する最小要求数を設定します。
circuitBreakerSleepWindowInMilliseconds 5000 Integer このプロパティは、サーキットをトリップした後、サーキットを再び閉じる必要があるかどうかを再試行することを許可するまでに要求を拒否するまでの時間を設定します。
executionTimeoutInMilliseconds 1000 Integer このプロパティは、実行完了のタイムアウトをミリ秒単位で設定します。

その他のプロパティは以下の公式サイトを参照してください。

JMXで状態の確認

JMXでHystrixの各種プロパティの値を取得することができます。
hawtioを使用したJMXの情報の表示例は以下のとおりです。

特に重要なのは以下の3つです。

  • Circuit breaker open
    サーキットブレーカーがOPENであるかを示します。
  • Hystrix error count
    タイムウィンドウ内のエラーの回数を示します。
  • Hystrix error percentage
    タイムウィンドウ内のエラーのパーセンテージを示します。

以下、mainメソッドも含めたソース全体になります。

    public static void main(String[] args) {
        try {
            CamelContext context = new DefaultCamelContext();
            context.addRoutes(createRouteBuilder());

            context.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static RouteBuilder createRouteBuilder() {
        return new RouteBuilder() {
            public void configure() throws Exception {
                from("timer:trigger?period=300")
                    .log("Client request")
                    .hystrix()
                        .hystrixConfiguration()
                            .circuitBreakerErrorThresholdPercentage(20)
                            .executionTimeoutInMilliseconds(5000)
                            .circuitBreakerSleepWindowInMilliseconds(5000)
                            .circuitBreakerRequestVolumeThreshold(2)
                        .end()
                        .to("http4://localhost:8888/hello")
                    .end()
                    .log("response: ${body}");
            }
        };
    }

参考