The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

SmallRye Fault Tolerance

One of the challenges brought by the distributed nature of microservices is that communication with external systems is inherently unreliable. This increases demand on resiliency of applications. To simplify making more resilient applications, Quarkus provides SmallRye Fault Tolerance, an implementation of the MicroProfile Fault Tolerance specification.

このガイドでは、 @Timeout@Fallback@Retry@CircuitBreaker などの MicroProfile Fault Tolerance アノテーションの使用方法を説明します。

前提条件

このガイドを完成させるには、以下が必要です:

  • 約15分

  • IDE

  • JDK 11+ がインストールされ、 JAVA_HOME が適切に設定されていること

  • Apache Maven 3.8.1+

  • 使用したい場合、 Quarkus CLI

  • ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること

シナリオ

このガイドで作成したアプリケーションは、グルメ・コーヒーのe-ショップのシンプルなバックエンドをシミュレートしています。このアプリケーションは、店舗で販売しているコーヒーのサンプルに関する情報を提供するRESTエンドポイントを実装しています。

Let’s imagine, although it’s not implemented as such, that some methods in our endpoint require communication to external services like a database or an external microservice, which introduces a factor of unreliability.

ソリューション

次のセクションで紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。

Gitレポジトリをクローンするか git clone https://github.com/quarkusio/quarkus-quickstarts.gitアーカイブ をダウンロードします。

ソリューションは microprofile-fault-tolerance-quickstart ディレクトリ にあります。

Mavenプロジェクトの作成

まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します。

CLI
quarkus create app org.acme:microprofile-fault-tolerance-quickstart \
    --extension=smallrye-fault-tolerance,resteasy-reactive-jackson \
    --no-code
cd microprofile-fault-tolerance-quickstart

Gradleプロジェクトを作成するには、 --gradle または --gradle-kotlin-dsl オプションを追加します。

Quarkus CLIのインストール方法については、Quarkus CLIガイドをご参照ください。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:2.11.1.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=microprofile-fault-tolerance-quickstart \
    -Dextensions="smallrye-fault-tolerance,resteasy-reactive-jackson" \
    -DnoCode
cd microprofile-fault-tolerance-quickstart

Gradleプロジェクトを作成するには、 -DbuildTool=gradle または -DbuildTool=gradle-kotlin-dsl オプションを追加します。

This command generates a project, importing the extensions for RESTEasy Reactive/JAX-RS and SmallRye Fault Tolerance.

すでにQuarkusプロジェクトが設定されている場合は、プロジェクトのベースディレクトリーで以下のコマンドを実行することで、プロジェクトに smallrye-fault-tolerance エクステンションを追加することができます。

CLI
quarkus extension add 'smallrye-fault-tolerance'
Maven
./mvnw quarkus:add-extension -Dextensions="smallrye-fault-tolerance"
Gradle
./gradlew addExtension --extensions="smallrye-fault-tolerance"

This will add the following to your build file:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-fault-tolerance</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-smallrye-fault-tolerance")

アプリケーションの準備: RESTエンドポイントとCDI Bean

このセクションでは、アプリケーションのスケルトン (骨組) を作成します。これにより、後から拡張したり、フォールトトレランス機能を追加したりすることができます。

まず、お店のコーヒーサンプルを表すシンプルなエンティティを作成します。

package org.acme.microprofile.faulttolerance;

public class Coffee {

    public Integer id;
    public String name;
    public String countryOfOrigin;
    public Integer price;

    public Coffee() {
    }

    public Coffee(Integer id, String name, String countryOfOrigin, Integer price) {
        this.id = id;
        this.name = name;
        this.countryOfOrigin = countryOfOrigin;
        this.price = price;
    }
}

続いて、コーヒーのサンプルのrepositoryとして動作するシンプルなCDIビーンです。

package org.acme.microprofile.faulttolerance;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class CoffeeRepositoryService {

    private Map<Integer, Coffee> coffeeList = new HashMap<>();

    public CoffeeRepositoryService() {
        coffeeList.put(1, new Coffee(1, "Fernandez Espresso", "Colombia", 23));
        coffeeList.put(2, new Coffee(2, "La Scala Whole Beans", "Bolivia", 18));
        coffeeList.put(3, new Coffee(3, "Dak Lak Filter", "Vietnam", 25));
    }

    public List<Coffee> getAllCoffees() {
        return new ArrayList<>(coffeeList.values());
    }

    public Coffee getCoffeeById(Integer id) {
        return coffeeList.get(id);
    }

    public List<Coffee> getRecommendations(Integer id) {
        if (id == null) {
            return Collections.emptyList();
        }
        return coffeeList.values().stream()
                .filter(coffee -> !id.equals(coffee.id))
                .limit(2)
                .collect(Collectors.toList());
    }
}

Finally, create the org.acme.microprofile.faulttolerance.CoffeeResource class as follows:

package org.acme.microprofile.faulttolerance;

import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;

import org.jboss.logging.Logger;

@Path("/coffee")
public class CoffeeResource {

    private static final Logger LOGGER = Logger.getLogger(CoffeeResource.class);

    @Inject
    CoffeeRepositoryService coffeeRepository;

    private AtomicLong counter = new AtomicLong(0);

    @GET
    public List<Coffee> coffees() {
        final Long invocationNumber = counter.getAndIncrement();

        maybeFail(String.format("CoffeeResource#coffees() invocation #%d failed", invocationNumber));

        LOGGER.infof("CoffeeResource#coffees() invocation #%d returning successfully", invocationNumber);
        return coffeeRepository.getAllCoffees();
    }

    private void maybeFail(String failureLogMessage) {
        if (new Random().nextBoolean()) {
            LOGGER.error(failureLogMessage);
            throw new RuntimeException("Resource failure.");
        }
    }
}

この時点で、コーヒーのサンプルのリストをJSON形式で表示するRESTメソッドを1つ公開しています。なお、 CoffeeResource#maybeFail() メソッドに障害を起こすコードを導入したため、約50%のリクエストで CoffeeResource#coffees() エンドポイント・メソッドに障害が発生することになります。

アプリケーションが動作することを確認してみましょう。次のようにQuarkusの開発サーバーを起動してください。

CLI
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

and open http://localhost:8080/coffee in your browser. Make a couple of requests (remember, some of them are expected to fail). At least some requests should show us the list of our coffee samples in JSON, the rest will fail with a RuntimeException thrown in CoffeeResource#maybeFail().

おめでとうございます!(多少信頼性に欠けるものの)Quarkusアプリケーションが完成しました。

レジリエンスの追加: リトライ

Quarkus開発サーバーを起動し、IDEで @Retry アノテーションを CoffeeResource#coffees() メソッドに以下のように追加し、ファイルを保存します。

import org.eclipse.microprofile.faulttolerance.Retry;
...

public class CoffeeResource {
    ...
    @GET
    @Retry(maxRetries = 4)
    public List<Coffee> coffees() {
        ...
    }
    ...
}

ブラウザの更新ボタンを押してください。Quarkus開発サーバーが自動的に変更を検出し、アプリを再コンパイルしますので、再起動する必要はありません。

さらに何度か更新ボタンを押してみてください。実質的にすべてのリクエストが成功しているはずです。 CoffeeResource#coffees() メソッドは、実際にはまだ約50%の確率で失敗していますが、そのたびにプラットフォームは自動的に呼び出しを再試行します。

To see that the failures still happen, check the output of the development server. The log messages should be similar to these:

2019-03-06 12:17:41,725 INFO  [org.acm.fau.CoffeeResource] (XNIO-1 task-1) CoffeeResource#coffees() invocation #5 returning successfully
2019-03-06 12:17:44,187 INFO  [org.acm.fau.CoffeeResource] (XNIO-1 task-1) CoffeeResource#coffees() invocation #6 returning successfully
2019-03-06 12:17:45,166 ERROR [org.acm.fau.CoffeeResource] (XNIO-1 task-1) CoffeeResource#coffees() invocation #7 failed
2019-03-06 12:17:45,172 ERROR [org.acm.fau.CoffeeResource] (XNIO-1 task-1) CoffeeResource#coffees() invocation #8 failed
2019-03-06 12:17:45,176 INFO  [org.acm.fau.CoffeeResource] (XNIO-1 task-1) CoffeeResource#coffees() invocation #9 returning successfully

呼び出しが失敗するたびに、すぐに次の呼び出しが行われ、1つが成功するまで繰り返されることがわかります。4回の再試行を許可しているので、ユーザーが実際に失敗にさらされるためには、5回連続して失敗する必要があります。これはかなり起こりにくいことです。

レジリエンスの追加: タイムアウト

では、MicroProfile Fault Toleranceには他に何があるのでしょうか。タイムアウトについて見てみましょう。

CoffeeResource のエンドポイントに以下の2つのメソッドを追加します。繰り返しになりますが、サーバーを再起動する必要はなく、コードを貼り付けてファイルを保存するだけです。

import org.eclipse.microprofile.faulttolerance.Timeout;
...
public class CoffeeResource {
    ...
    @GET
    @Path("/{id}/recommendations")
    @Timeout(250)
    public List<Coffee> recommendations(int id) {
        long started = System.currentTimeMillis();
        final long invocationNumber = counter.getAndIncrement();

        try {
            randomDelay();
            LOGGER.infof("CoffeeResource#recommendations() invocation #%d returning successfully", invocationNumber);
            return coffeeRepository.getRecommendations(id);
        } catch (InterruptedException e) {
            LOGGER.errorf("CoffeeResource#recommendations() invocation #%d timed out after %d ms",
                    invocationNumber, System.currentTimeMillis() - started);
            return null;
        }
    }

    private void randomDelay() throws InterruptedException {
        Thread.sleep(new Random().nextInt(500));
    }
}

いくつかの新しい機能を追加しました。ユーザーが現在見ているコーヒーに基づいて、関連するコーヒーをお勧めできるようにしたいのです。これは重要な機能ではなく、あれば望ましい機能です。システムに負荷がかかり、おすすめ情報を得るためのロジックの実行に時間がかかりすぎる場合は、むしろタイムアウトしておすすめ情報のないUIを表示したいと考えています。

Note that the timeout was configured to 250 ms, and a random artificial delay between 0 and 500 ms was introduced into the CoffeeResource#recommendations() method.

お使いのブラウザで http://localhost:8080/coffee/2/recommendations にアクセスして、2、3回更新してください。

org.eclipse.microprofile.faulttolerance.exceptions.TimeoutException でいくつかのリクエストがタイムアウトになるはずです。タイムアウトにならなかったリクエストでは、JSONに2つのおすすめコーヒーサンプルが表示されます。

レジリエンスの追加: フォールバック

関連するコーヒーを入手するための代替手段(おそらくより迅速な方法)を提供することで、おすすめ機能をさらに改善しましょう。

CoffeeResource にフォールバック・メソッドを、 CoffeeResource#recommendations() メソッドに @Fallback アノテーションを以下のように追加します。

import java.util.Collections;
import org.eclipse.microprofile.faulttolerance.Fallback;

...
public class CoffeeResource {
    ...
    @Fallback(fallbackMethod = "fallbackRecommendations")
    public List<Coffee> recommendations(int id) {
        ...
    }

    public List<Coffee> fallbackRecommendations(int id) {
        LOGGER.info("Falling back to RecommendationResource#fallbackRecommendations()");
        // safe bet, return something that everybody likes
        return Collections.singletonList(coffeeRepository.getCoffeeById(1));
    }
    ...
}

http://localhost:8080/coffee/2/recommendations を何回も更新してください。 TimeoutException はもう表示されないはずです。代わりに、タイムアウトが発生した場合、元のメソッドが返す2つのレコメンデーションではなく、フォールバックメソッド fallbackRecommendations() にハードコードされた1つのレコメンデーションが表示されます。

フォールバックが本当に行われているかどうか、サーバーの出力を確認してください。

2020-01-09 13:21:34,250 INFO  [org.acm.fau.CoffeeResource] (executor-thread-1) CoffeeResource#recommendations() invocation #1 returning successfully
2020-01-09 13:21:36,354 ERROR [org.acm.fau.CoffeeResource] (executor-thread-1) CoffeeResource#recommendations() invocation #2 timed out after 250 ms
2020-01-09 13:21:36,355 INFO  [org.acm.fau.CoffeeResource] (executor-thread-1) Falling back to RecommendationResource#fallbackRecommendations()
フォールバックメソッドには、元のメソッドと同じパラメータが必要です。

レジリエンスの追加: サーキットブレーカー

サーキットブレーカーは,システムの一部が一時的に不安定になったときに,システム内で発生する障害の数を制限するのに有効です。サーキットブレーカーは、あるメソッドの成功と失敗を記録し、失敗したメソッドの割合が指定された閾値に達すると、サーキットブレーカーが 開き、それ以降のメソッドの呼び出しを一定時間ブロックします。

次のコードを CoffeeRepositoryService ビーンに追加して、サーキットブレーカーの動作をデモできるようにします。

import java.util.concurrent.atomic.AtomicLong;
import org.eclipse.microprofile.faulttolerance.CircuitBreaker;
...

public class CoffeeRepositoryService {
    ...

    private AtomicLong counter = new AtomicLong(0);

    @CircuitBreaker(requestVolumeThreshold = 4)
    public Integer getAvailability(Coffee coffee) {
        maybeFail();
        return new Random().nextInt(30);
    }

    private void maybeFail() {
        // introduce some artificial failures
        final Long invocationNumber = counter.getAndIncrement();
        if (invocationNumber % 4 > 1) { // alternate 2 successful and 2 failing invocations
            throw new RuntimeException("Service failed.");
        }
    }
}

そして、以下のコードを CoffeeResource のエンドポイントに注入します。

public class CoffeeResource {
    ...
    @Path("/{id}/availability")
    @GET
    public Response availability(int id) {
        final Long invocationNumber = counter.getAndIncrement();

        Coffee coffee = coffeeRepository.getCoffeeById(id);
        // check that coffee with given id exists, return 404 if not
        if (coffee == null) {
            return Response.status(Response.Status.NOT_FOUND).build();
        }

        try {
            Integer availability = coffeeRepository.getAvailability(coffee);
            LOGGER.infof("CoffeeResource#availability() invocation #%d returning successfully", invocationNumber);
            return Response.ok(availability).build();
        } catch (RuntimeException e) {
            String message = e.getClass().getSimpleName() + ": " + e.getMessage();
            LOGGER.errorf("CoffeeResource#availability() invocation #%d failed: %s", invocationNumber, message);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity(message)
                    .type(MediaType.TEXT_PLAIN_TYPE)
                    .build();
        }
    }
    ...
}

もう一つの機能を追加しました。このアプリケーションは、私たちの店における、指定されたコーヒーの残りのパッケージの量を返すことができます(単なる乱数です)。

今回は、CDIビーンに人工的な失敗を導入しました。 CoffeeRepositoryService#getAvailability() メソッドは、2回の成功と2回の失敗の呼び出しを交互に行うことになります。

We also added a @CircuitBreaker annotation with requestVolumeThreshold = 4. CircuitBreaker.failureRatio is by default 0.5, and CircuitBreaker.delay is by default 5 seconds. That means that a circuit breaker will open when 2 of the last 4 invocations failed, and it will stay open for 5 seconds.

これを試すには、次のようにします。

  1. http://localhost:8080/coffee/2/availability をブラウザで開いて下さい。数字が返ってくるのが見えるでしょう。

  2. リフレッシュすると、この2回目のリクエストが再び成功し、数字が返ってくるはずです。

  3. さらに2回リフレッシュします。2回とも、"RuntimeException:Service failed." というテキストが表示されます。これは、 CoffeeRepositoryService#getAvailability() によってスローされる例外です。

  4. さらに数回更新します。長く待ちすぎていなければ、再び例外が表示されるはずですが、今回は "CircuitBreakerOpenException: getAvailability" となっています。この例外は、サーキットブレーカーが開き、 CoffeeRepositoryService#getAvailability() メソッドが呼び出されなくなったことを示しています。

  5. Give it 5 seconds during which circuit breaker should close, and you should be able to make two successful requests again.

ランタイム設定

application.properties ファイルの中で、実行時にアノテーションパラメータをオーバーライドすることができます。

先ほどのリトライの例で言えば

package org.acme;

import org.eclipse.microprofile.faulttolerance.Retry;
...

public class CoffeeResource {
    ...
    @GET
    @Retry(maxRetries = 4)
    public List<Coffee> coffees() {
        ...
    }
    ...
}

以下の設定項目により、 maxRetries パラメータをオーバーライドして、リトライ回数を4回から6回に変更することができます。

org.acme.CoffeeResource/coffees/Retry/maxRetries=6
フォーマットは fully-qualified-class-name/method-name/annotation-name/property-name=value です。また、 annotation-name/property-name=value を通じて、すべてのアノテーションに対するプロパティを設定することもできます。

まとめ

SmallRye Fault Toleranceは、ビジネスロジックの複雑さに影響を与えることなく、アプリケーションの耐障害性を向上させます。

Quarkusのフォールトトレランス機能を有効にするために必要なのは

  • quarkus-maven-pluginsmallrye-fault-tolerance Quarkus エクステンションをプロジェクトに追加することです :

    CLI
    quarkus extension add 'smallrye-fault-tolerance'
    Maven
    ./mvnw quarkus:add-extension -Dextensions="smallrye-fault-tolerance"
    Gradle
    ./gradlew addExtension --extensions="smallrye-fault-tolerance"
  • または、以下のMavenの依存関係を単純に追加することもできます。

    pom.xml
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-smallrye-fault-tolerance</artifactId>
    </dependency>
    build.gradle
    implementation("io.quarkus:quarkus-smallrye-fault-tolerance")

Additional resources

SmallRye Fault Tolerance has more features than shown here. Please check the SmallRye Fault Tolerance documentation to learn about them.

In Quarkus, you can use the SmallRye Fault Tolerance optional features out of the box.

Support for Mutiny is present, so your asynchronous methods can return Uni in addition to CompletionStage.

MicroProfile Context Propagation is integrated with Fault Tolerance, so existing contexts are automatically propagated to your asynchronous methods.

This also applies to the CDI request context: if it is active on the original thread, it is propagated to the new thread, but if it’s not, then the new thread won’t have it either. This is contrary to MicroProfile Fault Tolerance specification, which states that the request context must be active during the @Asynchronous method invocation.

We believe that in presence of MicroProfile Context Propagation, this requirement should not apply. The entire point of context propagation is to make sure the new thread has the same contexts as the original thread.

Non-compatible mode is enabled by default. This means that methods that return CompletionStage (or Uni) have asynchronous fault tolerance applied without any @Asynchronous, @Blocking or @NonBlocking annotation. It also means that circuit breaker, fallback and retry automatically inspect the exception cause chain if the exception itself is insufficient to decide what should happen.

This mode is not compatible with the MicroProfile Fault Tolerance specification, albeit the incompatibility is very small. To restore full compatibility, add this configuration property:

smallrye.faulttolerance.mp-compatibility=true

The programmatic API is present, including Mutiny support, and integrated with the declarative, annotation-based API. You can use the FaultTolerance and MutinyFaultTolerance APIs out of the box.

Support for Kotlin is present (assuming you use the Quarkus extension for Kotlin), so you can guard your suspend functions with fault tolerance annotations.

Metrics are automatically discovered and integrated. If your application uses the Quarkus extension for Micrometer, SmallRye Fault Tolerance will emit metrics to Micrometer. If your application uses the Quarkus extension for SmallRye Metrics, SmallRye Fault Tolerance will emit metrics to MicroProfile Metrics. Otherwise, SmallRye Fault Tolerance metrics will be disabled.