DEV Community

Yoshio Terada for Microsoft Azure

Posted on • Edited on

MicroProfile Fault Tolerance について

マイクロサービスでサービスを構築する際、耐障害性を考慮して実装する事がとても重要です。 リトライ・ポリシー、バルクヘッド、サーキットブレーカーなどは、マイクロサービスのデザイン・パターンにも定義される、とても重要な概念です。

MicroProfile の Fault Tolerance はこうした、耐障害性のあるサービスを構築するために必要な機能を提供しています。実装は CDI によるアノテーション・ベースで容易に開発ができ、CDI のインターセプターを利用して動作しています (クラスは CDI の Bean として実装しなければなりません)。
これにより、ビジネス・ロジックと Fault Torerance 用の冗長的なコードを分離し、かんたんに実装できるようになっています。

MicroProfile の Fault Tolerance のポリシーは外部設定に外だしして管理することが可能になっており、MicroProfile Config を利用してポリシー管理を行うこともできます。

Fault Tolerance の仕様に含まれる主な機能

Fault Tolerance で提供する機能 使用するアノテーションと概要説明
1. タイムアウト: @Timeout アノテーションを利用。 処理に要する最大時間を定義します
2. リトライ: @Retryアノテーションを利用。 処理に失敗した際のリトライ(再試行)の動作を設定します
3. フォールバック: @Fallback アノテーションを利用。 処理に失敗した際の代替の方法を提供(別メソッドの呼び出し)します
4. バルクヘッド(隔壁): @Bulkhead アノテーションを利用。同時実行数を制限します。これにより、高負荷時に単一の処理に負荷が集中してレスポンスが低下し、これを起因としたシステム全体への連鎖的な障害を防ぎます
5. サーキット・ブレーカー: @CircuitBreaker アノテーションを利用。処理が繰り返して失敗する場合、その処理呼び出しを自動的に即時に失敗しるようにします
6. 非同期: @Asynchronous アノテーションを利用。 処理を非同期にします

基本的に、上記のいずれかのポリシーを適用したい場合(複数の指定も可)、実装するクラス、もしくはメソッドにアノテーションを付加するだけで設定できます。


1. タイムアウト (@Timeout) ポリシー

タイムアウトを設定する事により、処理の完了を待ち続けるのを防ぎます。 仮にタイムアウトを設定しない場合、ネットワーク障害や、接続先が高負荷でレスポンスをただちに返せないような場合、呼び出し元の接続プールのワーカー・スレッドが枯渇するなど、呼び出し元にも負荷をかけてしまいかねません。

そこで、複数のマイクロサービスを実装する際、もしくは外部サービスを呼び出すような場合、各サービス間の連携においてタイムアウトを設定します。

@Timeout(400) // 接続タイムアウト値 400ms (0.4 sec)
public Connection getConnectionForServiceA() {
   Connection conn = connectionService();
   return conn;
}

@Timeout のアノテーションはクラス、もしくはメソッドレベルで付加できます。タイムアウト値に達した場合、TimeoutException が送出されます。

2. リトライ (@Retry) ポリシー

軽いネットワーク障害や、接続先からの返信が返ってこないような場合、@Retry アノテーションを使用して、処理呼び出しを再試行できます。

リトライポリシーでは以下を構成できます。

パラメータ 説明
maxRetries: 最大のリトライ回数
delay: リトライ間隔
delayUnit: delay のユニット
maxDuration: 再試行を実行する最大期間
durationUnit: duration ユニット
jitter: 再試行遅延のランダムな変化 (クロック信号のタイミング(もしくは周期)のズレ)
jitterDelayUnit: jitter ユニット
retryOn: 再試行する失敗 (Exception, Error) を指定
abortOn: 中止する失敗 (Exception, Error) を指定

@Retry アノテーションはクラスレベル、もしくはメソッドレベルで付加可能で、クラスに付加した場合、クラス内に存在する全メソッドに適用されます。メソッドに付加した場合、指定したメソッドだけが対象になります。クラスでアノテーションを付加し、メソッドにも付加した場合は、メソッドで指定した設定が有効になります。

  1. 正常に処理が終了した場合、結果を正常に返します。
  2. 送出された例外を abortOn で指定した場合、スローされた例外を再送出します
  3. 送出された例外を retryOn で指定した場合、メソッド呼び出しが再試行されます
  4. それ以外の場合、送出された例外を再送出します

また、他の Fault Tolerance のアノテーションと共に併用できます。

    /**
     * serviceA() メソッド呼び出しで、例外が送出された場合に、
     * 例外が IOException でない場合は再試行します。
     */
    @Retry(retryOn = Exception.class, abortOn = IOException.class)
    public void invokeService() {
        callServiceA();
    }

    /**
     * 最大再試行回数は90、再試行を実行する最大期間は 1000 ミリ秒に設定
     * 再試行の最大期間に達すると、最大再試行回数に達していない場合でも、再試行は実行されない。
     */
    @Retry(maxRetries = 90, maxDuration= 1000)
    public void serviceB() {
        callServiceB();
    }

    /**
    * クロック周波数のズレ(jitter)を 400ms と仮定した場合、-400ms 〜 400ms つまり
    * 0 (delay - jitter) 〜 800ms (delay + jitter )の差で再試行が行われることが予想されます。
    * 最大遅延が発生した場合を想定し、3200/800=4 で、最低試行回数は4回以上、
    * 最大でも 10 回を超えない試行回数を設定します
    */
    @Retry(delay = 400, maxDuration= 3200, jitter= 400, maxRetries = 10)
    public Connection serviceA() {
        return getConnectionForServiceA();
    }

3. フォールバック (@Fallback) ポリシー

@Fallback アノテーションはメソッドレベルで指定できます。アノテーションが付加されたメソッドで例外が発生し終了した場合、フォールバックメソッドで指定したメソッドが呼び出されます。

@Fallback アノテーションは、単体もしくは他の Fault Tolerance アノテーションと一緒に使用できます。 他のアノテーションと併用した場合、フォールバックは、他のすべての Fault Tolerance 処理が行われた後に呼び出されます。

たとえば、@Retry が定義されている場合、リトライが最大試行回数を超えた場合にフォールバックの処理が実行されます。
また、@CircuitBreaker が共に定義されている場合、メソッド呼び出しが失敗した場合に直ちに呼び出されます。そしてサーキットがオープンしている場合は常に、フォールバック・メソッドが呼び出されます。

3.1 FallbackHandler の実装によるフォールバック処理の実装例

FallbackHandler インタフェースを実装した FallbackHandler のクラス (ServiceInvocationAFallbackHandler) を定義します。そして handle メソッド内で代替の処理を実装します。

ここでは、MicroProfile Config を利用して app.serviceinvokeA.FallbackReplyMessage のプロパティ、もしくは環境変数などで定義した文字列を返信するように実装しています。

@Dependent
public class ServiceInvocationAFallbackHandler implements FallbackHandler<String> {

    @ConfigProperty(name="app.serviceinvokeA.FallbackReplyMessage", defaultValue = "Unconfigured Default Reply")
    private String replyString;

    @Override
    public String handle(ExecutionContext ec) {
        return replyString;
    }
}

下記の、BusinessLogicServiceBean#invokeServiceA() メソッドが呼び出されると、ここでは内部的に RuntimeException が発生しますが 3 回処理の再試行します、全ての再試行に失敗したのち、ServiceInvocationAFallbackHandler#handle() が呼び出されます。

@RequestScoped
public class BusinessLogicServiceBean {

    // FallbackHandler の実装クラスを指定し @Fallback アノテーションを付加
    // 最大のリトライ回数 (3回)を超えた場合、FallbackHandler の handle() メソッドが呼ばれる
    @Retry(maxRetries = 3)
    @Fallback(ServiceInvocationAFallbackHandler.class)
    public String invokeServiceA() {
        throw new RuntimeException("Connection failed");
        return null;
    }   
}

3.2 fallbackMethod を指定したフォールバック処理の実装例

@Fallback アノテーション内で直接、代替で呼び出すメソッド名を記述します。

ここでは、fallbackForServiceB() メソッドを代替メソッドとして定義しています。

@RequestScoped
public class BusinessLogicServiceBean {

    @Retry(maxRetries = 3)
    @Fallback(fallbackMethod= "fallbackForServiceB")
    public String invokeServiceB() {
        counterForInvokingServiceB++;
       return nameService();
    }


    @ConfigProperty(name="app.serviceinvokeB.FallbackReplyMessage", defaultValue = "Unconfigured Default Reply")
    private String replyString;

    private String fallbackForInvokeServiceB() {
        return replyString;
    }

4. バルクヘッド (@Bulkhead) ポリシー

バルクヘッド・パターンは、システムの一部の障害がシステム全体に伝播し、システム全体がダウンするのを防ぐために利用します。MicroProfile の実装では、インスタンスにアクセスする同時リクエスト数を制限します。

Bulkheadパターンは、大量に呼び出される可能性のあるコンポーネントや、高負荷時にレスポンス低下を招くようなサービスに対して適用すると効果的です。

@Bulkhead アノテーションはクラスレベル、もしくはメソッドレベルで付加可能で、クラスに付加した場合、クラス内に存在する全メソッドに適用されます。メソッドに付加した場合、指定したメソッドだけが対象になります。クラスでアノテーションを付加し、メソッドにも付加した場合は、メソッドで指定した設定が有効になります。

バルクヘッドには下記の2種類の方法で設定可能です。

  1. スレッド・プールの分離 : (@Asynchronous アノテーションと併用した場合) スレッド・プール内の待機中のキューサイズで最大同時リクエスト数を設定します。
  2. セマフォの分離:(@Asynchronous アノテーションと併用しない場合) 同時リクエスト数の設定のみが許可されます。

4.1 スレッド・プールによる分離例

@Asynchronous アノテーションと併用した場合、スレッド・プールの分離が適用されます。下記の例では、最大で5つの同時リクエストが許可され、8つのリクエストが待機キューで保持されます。

// 最大5つの同時リクエストが許可され、最大 8つのリクエストが待機キューで許可される
@Asynchronous
@Bulkhead(value = 5, waitingTaskQueue = 8)
public Future<Connection> invokeServiceA() {
   Connection conn = null;
   counterForInvokingServiceA++;
   conn = connectionService();
   return CompletableFuture.completedFuture(conn);
}

4.2 セマフォによる分離例

@Asynchronous アノテーションを併用しない場合は、単に同時リクエスト数を定義します。

@Bulkhead(5) // 最大5つの同時要求が許可されます
public Connection invokeServiceA() {
   Connection conn = null;
   counterForInvokingServiceA++;
   conn = connectionService();
   return conn;
}

5. サーキット・ブレーカー (@CircuitBreaker) ポリシー

サーキット・ブレーカーは、障害のあるサービスに対して繰り返しの呼び出しを防いで、障害のあるサービスもしくは API 呼び出しで直ちに失敗するようにします。サービス呼び出しが頻繁に失敗する場合、サーキットブレーカーがオープンし、一定の時間が経過するまでそのサービスへの呼び出しは試行されません。

@CircuitBreaker アノテーションはクラスレベル、もしくはメソッドレベルで付加可能で、クラスに付加した場合、クラス内に存在する全メソッドに適用されます。メソッドに付加した場合、指定したメソッドだけが対象になります。クラスでアノテーションを付加し、メソッドにも付加した場合は、メソッドで指定した設定が有効になります。

サーキットブレーカーの3つの状態

クローズド: (通常時)

通常サーキットブレーカーは閉じています。サーキットブレーカーは、各呼び出しが成功したか失敗したかを記録しており最新の結果を追跡します。障害の割合が failureRatio を超えると、サーキット・ブレーカーがオープンします。

オープン: (障害発生時)

サーキットブレーカーが開いている場合、サーキットブレーカーで動作しているサービスへの呼び出しは、CircuitBreakerOpenException で直ちに失敗します。しばらくした後(設定可能)、サーキットブレーカーはハーフ・オープン状態に移行します。

ハーフ・オープン: (障害復旧の確認中)

ハーフオープン状態では、サービス呼び出しの試行が始まります(設定可能な数)。仮にいずれかの呼び出しで障害が発生した場合、再度サーキットブレーカはオープン状態に戻ります。すべての試行が成功した場合、サーキットブレーカーはクローズド状態に移行します。

サーキット・ブレーカの実装例1

@CircuitBreaker(successThreshold = 10, requestVolumeThreshold = 4, failureRatio=0.5, delay = 1000)
public Connection serviceA() {
   Connection conn = null;
   counterForInvokingServiceA++;
   conn = connectionService();
   return conn;
}
パラメータ 説明
requestVolumeThreshold: サーキットブレーカーが「クローズ」のときに使用するローリングウィンドウ(障害比率を計算するための分母の数)のサイズ
failureRatio: サーキットブレーカーを「オープン」にするための、ローリングウィンドウ内の障害比率
successThreshold: サーキットブレーカーが「ハーフ・オープン」の時、クローズドに移行するための試行回数
delayおよびdelayUnit: サーキットブレーカーを「オープン」にしつづける時間

上記では、requestVolumeThreshold で指定したローリングウィンドウ数である 4 回の連続した呼び出し中に 2 回(4 x 0.5)の障害が発生すると、サーキットが「オープン」します。 サーキットは 1,000 ミリ秒間「オープン」のままになり、その後「ハーフ・オープン」に移ります。 「ハーフ・オープン」で 10 回呼び出しが成功すると、サーキットは再び「クローズ」になります。

リクエスト1-成功
リクエスト2-失敗
リクエスト3-成功
リクエスト4-成功
リクエスト5-失敗
リクエスト6-CircuitBreakerOpenException

上記のリクエストの場合、最後の4つのリクエストのうち2つが失敗し、failureRatio が 0.5 に達するため、「リクエスト5」 でサーキットが 「オープン」 になり CircuitBreakerOpenException が送出されます。

成功/失敗とみなす例外定義を追加

failOn パラメーターと skipOn パラメーターは、サーキットブレーカーを「オープン」にするか否かを決定するため、どの例外を失敗と見なすかを定義するために使用します。

@CircuitBreaker(successThreshold = 10, requestVolumeThreshold = 4, failureRatio=0.5, delay = 1000,failOn = {ExceptionA.class, ExceptionB.class}, skipOn = ExceptionBSub.class))
public Connection serviceA() {
   Connection conn = null;
   counterForInvokingServiceA++;
   conn = connectionService();
   return conn;
}

failOn に指定した例外が発生した場合は、失敗とみなします
skipOn に指定した例外が発生した場合は、成功とみなします

6. 非同期 (@Asynchronous) ポリシー

Fault Tolerance の主な機能は Architecture に記述されているように、上記 1-5 までにあげた機能です。そこで、非同期処理は直接 Fault Tolerance と関連するわけではありません。しかし、分散処理において非同期処理はとても重要で Fault Tolerance の各種機能と組み合わせる事により、より有効的に働くため仕様内に取り込まれました。

引用元:Architecture

As mentioned above, the Fault Tolerance specification is to focus on the following aspects:

  • Timeout: Define a duration for timeout
  • Retry: Define a criteria on when to retry
  • Fallback: provide an alternative solution for a failed execution.
  • CircuitBreaker: offer a way of fail fast by automatically failing execution to prevent the system overloading and indefinite wait or timeout by the clients.
  • Bulkhead: isolate failures in part of the system while the rest part of the system can still function.

@Asynchronousアノテーションはクラスレベル、もしくはメソッドレベルで付加可能で、クラスに付加した場合、クラス内に存在する全メソッドに適用されます。メソッドに付加した場合、指定したメソッドだけが対象になります。クラスでアノテーションを付加し、メソッドにも付加した場合は、メソッドで指定した設定が有効になります。

@Asynchronous アノテーションが付加されたメソッドが呼び出されると、すぐに Future もしくは CompletionStage を返します。残りのメソッド本体の処理は別スレッドで実行されます。非同期処理が完了するまで、返却された Future もしくは CompletionStage は正しい値を持ちません。仮に処理中に例外が発生した場合は、Future または CompletionStage はその例外で終了します。
処理が正常に完了した場合、Future もしくは CompletionStage は戻り値(それ自体がFutureまたはCompletionStage)を返します。

@Asynchronous
public CompletionStage <Connection> serviceA(){
   Connection conn = null;
   counterForInvokingServiceA ++;
   conn = connectionService();
   return CompletableFuture.completedFutureconn;
}

上記の例では、serviceA メソッドへの呼び出しが非同期処理になります。serviceA の呼び出しはCompletionStage を返し、メソッド本体の実行は別スレッドで実行されます。

注意:

CDI の RequestScope から @Asynchronous を呼び出す場合、非同期メソッド呼び出し中 RequestScope がアクティブでなければなりません。@Asynchronousアノテーションが付加されたメソッドは、java.util.concurrentパッケージの Future もしくは CompletionStage を返す必要があります。そうでない場合、FaultToleranceDefinitionExceptionが発生します。

ソースコードに記載した設定値の上書き方法

各節で確認したように、Fault Tolerance のポリシーは一部を除いてほとんどの場合、アノテーションを使用して適用できます。
ソースコードの実装後、仮にアノテーションで実装した値を変更したい場合は、MicroProfile Config を使用して設定値を上書きすることもできます。

アノテーション内のパラメーターは、次の命名規則を使用して、設定プロパティーで上書きできます:

<classname>/<methodname>/<annotation>/<parameter>

たとえば、ある特定のメソッドで指定した Timeout や Retry のアノテーションで指定したパラメータを外部で上書き設定したい場合、MicroProfile Config で下記のように記述します。

com.yoshio3.FaultToleranceService.resilient.ResilienceController/checkTimeout/Timeout/value=2000
com.yoshio3.FaultToleranceService.resilient.ResilienceController/checkTimeout/Retry/maxDuration=3000

仮に、クラス全体に適用したい場合は下記のように、メソッド名の部分を削除してクラス全体に適用することもできます。

com.yoshio3.FaultToleranceService.resilient.ResilienceController/Timeout/value=2000
com.yoshio3.FaultToleranceService.resilient.ResilienceController/Retry/maxDuration=3000

そして、プロジェクト内の全コードに対して同一ルールを適用したい場合は、アノテーションとパラメータ設定だけを記載することもできます。

Timeout/value=2000
Retry/maxDuration=3000

さいごに

ここでは、MicroProfile Fault Tolerance を利用して耐障害性を高めるアプリケーションを構築するためのコードを確認しました。次は、Fault Tolerance を利用したアプリケーションを実際に構築し、Azure 上で耐障害性を持つ複数のサービスを連携をしてみたいと思います。

Top comments (0)