次の方法で共有


回復性がある HTTP アプリを構築する: 主要な開発パターン

一時的な障害エラーから復旧できる堅牢な HTTP アプリを構築することは、一般的な要件です。 この記事は、お客様が「回復力があるアプリの開発の概要」を既に読まれていることを前提に書かれています。この記事では、そこで説明されている主要な概念を発展させます。 回復性がある HTTP アプリの構築を支援するため、Microsoft.Extensions.Http.Resilience NuGet パッケージでは、特に HttpClient を対象とする回復性メカニズムが提供されています。 この NuGet パッケージが依存している Microsoft.Extensions.Resilience ライブラリと Polly は、よく知られたオープンソース プロジェクトです。 詳しくは、Polly のページをご覧ください。

はじめに

HTTP アプリで回復性パターンを使うには、Microsoft.Extensions.Http.Resilience NuGet パッケージをインストールします。

dotnet add package Microsoft.Extensions.Http.Resilience --version 8.0.0

詳しくは、「dotnet add package」または「.NET アプリケーションでパッケージの依存関係を管理する」をご覧ください。

HTTP クライアントに回復性を追加する

HttpClient に回復性を追加するには、使用できるいずれかの AddHttpClient メソッドの呼び出しから返される IHttpClientBuilder 型で呼び出しをチェーンします。 詳細については、「.NET を使用した IHttpClientFactory」を参照してください。

回復性中心の拡張機能がいくつか用意されています。 標準的で、さまざまな業界のベスト プラクティスが採用されているものもあれば、よりいっそうカスタマイズ可能なものもあります。 回復性を追加するときは、追加する回復性ハンドラーを 1 つだけにして、ハンドラーをスタックしないようにする必要があります。 複数の回復性ハンドラーを追加する必要がある場合は、回復性戦略をカスタマイズできる AddResilienceHandler 拡張メソッドの使用を検討する必要があります。

重要

この記事のすべての例では、IHttpClientBuilder インスタンスを返す Microsoft.Extensions.Http ライブラリの AddHttpClient API を利用しています。 IHttpClientBuilder インスタンスを使って、HttpClient を構成し、回復性ハンドラーを追加します。

標準の回復性ハンドラーを追加する

標準の回復性ハンドラーでは、相互に積み重ねられた複数の回復性戦略が使われており、要求を送信して一時的なエラーを処理する既定のオプションを備えています。 標準の回復性ハンドラーは、IHttpClientBuilder インスタンスで AddStandardResilienceHandler 拡張メソッドを呼び出すことによって追加されます。

var services = new ServiceCollection();

var httpClientBuilder = services.AddHttpClient<ExampleClient>(
    configureClient: static client =>
    {
        client.BaseAddress = new("https://jsonplaceholder.typicode.com");
    });

上記のコードでは次の操作が行われます。

  • ServiceCollection インスタンスを作成します。
  • ExampleClient 型の HttpClient をサービス コンテナーに追加します。
  • ベース アドレスとして "https://jsonplaceholder.typicode.com" を使うように HttpClient を構成します。
  • この記事の他の例で使われる httpClientBuilder を作成します。

さらに現実的な例では、「.NET での汎用ホスト」記事で説明されているように、ホスティングに依存します。 Microsoft.Extensions.Hosting NuGet パッケージを使って、次の更新された例を考えてみます。

using Http.Resilience.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

IHttpClientBuilder httpClientBuilder = builder.Services.AddHttpClient<ExampleClient>(
    configureClient: static client =>
    {
        client.BaseAddress = new("https://jsonplaceholder.typicode.com");
    });

上のコードは、手動の ServiceCollection 作成アプローチに似ていますが、代わりに Host.CreateApplicationBuilder() を利用して、サービスを公開するホストを構築しています。

ExampleClient は次のように定義されています。

using System.Net.Http.Json;

namespace Http.Resilience.Example;

/// <summary>
/// An example client service, that relies on the <see cref="HttpClient"/> instance.
/// </summary>
/// <param name="client">The given <see cref="HttpClient"/> instance.</param>
internal sealed class ExampleClient(HttpClient client)
{
    /// <summary>
    /// Returns an <see cref="IAsyncEnumerable{T}"/> of <see cref="Comment"/>s.
    /// </summary>
    public IAsyncEnumerable<Comment?> GetCommentsAsync()
    {
        return client.GetFromJsonAsAsyncEnumerable<Comment>("/comments");
    }
}

上記のコードでは次の操作が行われます。

  • HttpClient を受け入れるコンストラクターを持つ ExampleClient 型を定義します。
  • /comments エンドポイントに GET 要求を送信して応答を返す GetCommentsAsync メソッドを公開します。

Comment 型は次のように定義されています。

namespace Http.Resilience.Example;

public record class Comment(
    int PostId, int Id, string Name, string Email, string Body);

IHttpClientBuilder (httpClientBuilder) を作成し、ExampleClient の実装とそれに対応する Comment モデルを理解したので、次の例を考えてみます。

httpClientBuilder.AddStandardResilienceHandler();

上のコードでは、標準の回復性ハンドラーが HttpClient に追加されます。 ほとんどの回復性 API と同様に、既定のオプションと適用される回復性戦略をカスタマイズできるオーバーロードがあります。

標準の回復性ハンドラーの既定値

既定の構成では、5 つの回復性戦略が次の順序でチェーンされます (最も外側から最も内側へ)。

受注 戦略 説明 既定
1 レート リミッター レート リミッター パイプラインでは、依存関係に送信される同時要求の最大数が制限されます。 キュー: 0
許可: 1_000
2 合計タイムアウト 合計要求タイムアウト パイプラインは、全体的なタイムアウトを実行に適用し、再試行を含めて、要求が構成された制限を超えないようにします。 合計タイムアウト: 30 秒
3 再試行 再試行パイプラインは、依存関係が遅い場合、または一時的なエラーを返した場合に、要求を再試行します。 最大再試行回数: 3
バックオフ: Exponential
ジッタの使用: true
遅延: 2 秒
4 サーキット ブレーカー サーキット ブレーカーは、検出された直接的エラーまたはタイムアウトが多すぎる場合に、実行をブロックします。 失敗率: 10%
最小スループット: 100
サンプリング時間: 30 秒
中断時間: 5 秒
5 試行タイムアウト 試行タイムアウト パイプラインは、各要求の試行時間を制限し、それを超えた場合はスローします。 試行タイムアウト: 10 秒

再試行とサーキット ブレーカー

再試行とサーキット ブレーカーの戦略は両方とも、特定の HTTP ステータス コードと例外のセットを処理します。 以下の HTTP ステータス コードを考えてみましょう。

  • HTTP 500 以上 (サーバー エラー)
  • HTTP 408 (要求タイムアウト)
  • HTTP 429 (要求過多)

さらに、これらの戦略は以下の例外も処理します。

  • HttpRequestException
  • TimeoutRejectedException

標準のヘッジ ハンドラーを追加する

標準のヘッジ ハンドラーは、要求の実行を標準のヘッジ メカニズムでラップします。 ヘッジ処理では、遅い要求が並列で再試行されます。

標準のヘッジ ハンドラーを使うには、AddStandardHedgingHandler 拡張メソッドを呼び出します。 次の例では、標準のヘッジ ハンドラーを使うように ExampleClient を構成します。

httpClientBuilder.AddStandardHedgingHandler();

上のコードでは、標準のヘッジ ハンドラーが HttpClient に追加されます。

標準のヘッジ ハンドラーの既定値

標準のヘッジでは、サーキット ブレーカーのプールを使って、異常なエンドポイントがヘッジされないようにします。 既定では、URL 機関 (スキーム + ホスト + ポート) に基づいてプールからの選択が行われます。

ヒント

さらに高度なシナリオでは、StandardHedgingHandlerBuilderExtensions.SelectPipelineByAuthority または StandardHedgingHandlerBuilderExtensions.SelectPipelineBy を呼び出すことによって、戦略の選択方法を構成することをお勧めします。

上のコードでは、標準のヘッジ ハンドラーが IHttpClientBuilder に追加されます。 既定の構成では、5 つの回復性戦略が次の順序でチェーンされます (最も外側から最も内側へ)。

受注 戦略 説明 既定
1 合計要求タイムアウト 合計要求タイムアウト パイプラインは、全体的なタイムアウトを実行に適用し、ヘッジの試行を含めて、要求が構成された制限を超えないようにします。 合計タイムアウト: 30 秒
2 ヘッジング ヘッジ戦略は、依存関係が遅い場合、または一時的なエラーを返した場合、複数のエンドポイントに対して要求を実行します。 ルーティングはオプションであり、既定では元の HttpRequestMessage によって提供される URL をヘッジするだけです。 最小試行回数: 1
最大試行回数: 10
遅延: 2 秒
3 レート リミッター (エンドポイントごと) レート リミッター パイプラインでは、依存関係に送信される同時要求の最大数が制限されます。 キュー: 0
許可: 1_000
4 サーキット ブレーカー (エンドポイントごと) サーキット ブレーカーは、検出された直接的エラーまたはタイムアウトが多すぎる場合に、実行をブロックします。 失敗率: 10%
最小スループット: 100
サンプリング時間: 30 秒
中断時間: 5 秒
5 試行タイムアウト (エンドポイントごと) 試行タイムアウト パイプラインは、各要求の試行時間を制限し、それを超えた場合はスローします。 タイムアウト: 10 秒

ヘッジ ハンドラーのルート選択をカスタマイズする

標準のヘッジ ハンドラーを使う場合は、IRoutingStrategyBuilder 型でさまざまな拡張機能を呼び出すことによって、要求エンドポイントの選択方法をカスタマイズできます。 これは、要求の一定の割合を別のエンドポイントにルーティングする A/B テストなどのシナリオに役立ちます。

httpClientBuilder.AddStandardHedgingHandler(static (IRoutingStrategyBuilder builder) =>
{
    // Hedging allows sending multiple concurrent requests
    builder.ConfigureOrderedGroups(static options =>
    {
        options.Groups.Add(new UriEndpointGroup()
        {
            Endpoints =
            {
                // Imagine a scenario where 3% of the requests are 
                // sent to the experimental endpoint.
                new() { Uri = new("https://example.net/api/experimental"), Weight = 3 },
                new() { Uri = new("https://example.net/api/stable"), Weight = 97 }
            }
        });
    });
});

上記のコードでは次の操作が行われます。

  • IHttpClientBuilder にヘッジ ハンドラーを追加します。
  • ConfigureOrderedGroups メソッドを使って順序付けされたグループを構成するように、IRoutingStrategyBuilder を構成します。
  • 要求の 3% を https://example.net/api/experimental エンドポイントにルーティングし、要求の 97% を https://example.net/api/stable エンドポイントにルーティングする orderedGroup に、EndpointGroup を追加します。
  • ConfigureWeightedGroups メソッドを使って構成するように、IRoutingStrategyBuilder を構成します

重み付けされたグループを構成するには、IRoutingStrategyBuilder 型で ConfigureWeightedGroups メソッドを呼び出します。 次の例では、ConfigureWeightedGroups メソッドを使って重み付けされたグループを構成するように、IRoutingStrategyBuilder を構成します。

httpClientBuilder.AddStandardHedgingHandler(static (IRoutingStrategyBuilder builder) =>
{
    // Hedging allows sending multiple concurrent requests
    builder.ConfigureWeightedGroups(static options =>
    {
        options.SelectionMode = WeightedGroupSelectionMode.EveryAttempt;

        options.Groups.Add(new WeightedUriEndpointGroup()
        {
            Endpoints =
            {
                // Imagine A/B testing
                new() { Uri = new("https://example.net/api/a"), Weight = 33 },
                new() { Uri = new("https://example.net/api/b"), Weight = 33 },
                new() { Uri = new("https://example.net/api/c"), Weight = 33 }
            }
        });
    });
});

上記のコードでは次の操作が行われます。

  • IHttpClientBuilder にヘッジ ハンドラーを追加します。
  • ConfigureWeightedGroups メソッドを使って重み付けされたグループを構成するように、IRoutingStrategyBuilder を構成します。
  • SelectionModeWeightedGroupSelectionMode.EveryAttempt に設定します。
  • 要求の 33% を https://example.net/api/a エンドポイントにルーティングし、要求の 33% を https://example.net/api/b エンドポイントにルーティングし、要求の 33% を https://example.net/api/c エンドポイントにルーティングする weightedGroup に、WeightedEndpointGroup を追加します。

ヒント

ヘッジ試行の最大数は、構成されるグループの数に直接関連付けられます。 たとえば、2 つのグループがある場合、試行の最大数は 2 です。

詳しくは、Polly のドキュメントの「ヘッジ回復性戦略」をご覧ください。

順序付けされたグループまたは重み付けされたグループのどちらかを構成するのが一般的ですが、両方とも構成しても問題ありません。 順序付けされたグループと重み付けされたグループを使うと、A/B テストの場合など、要求の一定の割合を異なるエンドポイントに送信するシナリオで役に立ちます。

カスタム回復性ハンドラーを追加する

さらに詳細に制御を行うには、AddResilienceHandler API を使って回復性ハンドラーをカスタマイズします。 このメソッドは、回復性戦略の作成に使われる ResiliencePipelineBuilder<HttpResponseMessage> インスタンスを構成するデリゲートを受け入れます。

名前付きの回復性ハンドラーを構成するには、ハンドラーの名前を指定して AddResilienceHandler 拡張メソッドを呼び出します。 次の例では、"CustomPipeline" という名前の回復性ハンドラーを構成します。

httpClientBuilder.AddResilienceHandler(
    "CustomPipeline",
    static builder =>
{
    // See: https://www.pollydocs.org/strategies/retry.html
    builder.AddRetry(new HttpRetryStrategyOptions
    {
        // Customize and configure the retry logic.
        BackoffType = DelayBackoffType.Exponential,
        MaxRetryAttempts = 5,
        UseJitter = true
    });

    // See: https://www.pollydocs.org/strategies/circuit-breaker.html
    builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
    {
        // Customize and configure the circuit breaker logic.
        SamplingDuration = TimeSpan.FromSeconds(10),
        FailureRatio = 0.2,
        MinimumThroughput = 3,
        ShouldHandle = static args =>
        {
            return ValueTask.FromResult(args is
            {
                Outcome.Result.StatusCode:
                    HttpStatusCode.RequestTimeout or
                        HttpStatusCode.TooManyRequests
            });
        }
    });

    // See: https://www.pollydocs.org/strategies/timeout.html
    builder.AddTimeout(TimeSpan.FromSeconds(5));
});

上記のコードでは次の操作が行われます。

  • pipelineName で名前 "CustomPipeline" を指定して回復性ハンドラーをサービス コンテナーに追加します。
  • エクスポネンシャル バックオフ、5 回の再試行、ジッター優先を備えた再試行戦略を、回復性ビルダーに追加します。
  • サンプリング時間 10 秒、障害率 0.2 (20%)、最小スループット 3、および RequestTimeoutTooManyRequests の HTTP 状態コードを処理する述語を備えたサーキット ブレーカー戦略を、回復性ビルダーに追加します。
  • タイムアウトが 5 秒のタイムアウト戦略を、回復性ビルダーに追加します。

回復性戦略ごとに多くのオプションを使用できます。 詳しくは、戦略に関する Polly のドキュメントをご覧ください。 ShouldHandle デリゲートの構成について詳しくは、事後対応戦略での障害処理に関する Polly のドキュメントをご覧ください。

動的再読み込み

Polly では、構成された回復性戦略の動的な再読み込みがサポートされています。 つまり、実行時に回復性戦略の構成を変更できます。 動的再読み込みを有効にするには、ResilienceHandlerContext を公開する適切な AddResilienceHandler オーバーロードを使います。 特定のコンテキストに対応する回復性戦略オプションの EnableReloads を呼び出します。

httpClientBuilder.AddResilienceHandler(
    "AdvancedPipeline",
    static (ResiliencePipelineBuilder<HttpResponseMessage> builder,
        ResilienceHandlerContext context) =>
    {
        // Enable reloads whenever the named options change
        context.EnableReloads<HttpRetryStrategyOptions>("RetryOptions");

        // Retrieve the named options
        var retryOptions =
            context.GetOptions<HttpRetryStrategyOptions>("RetryOptions");

        // Add retries using the resolved options
        builder.AddRetry(retryOptions);
    });

上記のコードでは次の操作が行われます。

  • pipelineName で名前 "AdvancedPipeline" を指定して回復性ハンドラーをサービス コンテナーに追加します。
  • 指定された RetryStrategyOptions オプションが変化したら常に、"AdvancedPipeline" パイプラインの再読み込みを有効にします。
  • IOptionsMonitor<TOptions> サービスから指定されたオプションを取得します。
  • 取得したオプションを含む再試行戦略を回復性ビルダーに追加します。

詳しくは、高度な依存関係の挿入に関する Polly のドキュメントをご覧ください。

この例では、appsettings.json ファイルなど、変更可能なオプション セクションに依存しています。 以下の appsettings.json ファイルについて考えます:

{
    "RetryOptions": {
        "Retry": {
            "BackoffType": "Linear",
            "UseJitter": false,
            "MaxRetryAttempts": 7
        }
    }
}

ここで、これらのオプションはアプリの構成にバインドされ、HttpRetryStrategyOptions"RetryOptions" セクションにバインドされていたものとします。

var section = builder.Configuration.GetSection("RetryOptions");

builder.Services.Configure<HttpStandardResilienceOptions>(section);

詳しくは、「.NET でのオプション パターン」をご覧ください。

使用例

アプリでは、依存関係の挿入を利用して、ExampleClient とそれに対応する HttpClient を解決します。 このコードでは、IServiceProvider がビルドされ、それから ExampleClient が解決されます。

IHost host = builder.Build();

ExampleClient client = host.Services.GetRequiredService<ExampleClient>();

await foreach (Comment? comment in client.GetCommentsAsync())
{
    Console.WriteLine(comment);
}

上記のコードでは次の操作が行われます。

  • ServiceCollection から IServiceProvider をビルドします。
  • IServiceProvider から ExampleClient を解決します。
  • ExampleClientGetCommentsAsync メソッドを呼び出してコメントを取得します。
  • 各コメントをコンソールに書き込みます。

ネットワークがダウンするか、サーバーが応答しなくなる状況を想像してください。 次の図は、ExampleClientGetCommentsAsync メソッドについて、回復性戦略が状況を処理する方法を示したものです。

回復性パイプラインを使用した HTTP GET ワーク フローの例。

上の図では次のことが示されています。

  • ExampleClient は、HTTP GET 要求を /comments エンドポイントに送信します。
  • HttpResponseMessage が評価されます。
    • 応答が成功である場合 (HTTP 200)、応答が返されます。
    • 応答が失敗である場合 (HTTP が 200 以外)、回復性パイプラインは構成されている回復性戦略を採用します。

これは簡単な例ですが、回復性戦略を使って一時的なエラーを処理できる方法がわかります。 詳しくは、戦略に関する Polly のドキュメントをご覧ください。

既知の問題

次のセクションでは、さまざまな既知の問題について詳しく説明します。

Grpc.Net.ClientFactory パッケージとの互換性

Grpc.Net.ClientFactoryバージョン 2.63.0 以前使用している場合は、gRPC クライアントに対して標準の回復性ハンドラーまたはヘッジ ハンドラーを有効にすると、ランタイム例外が発生する可能性があります。 具体的には、次のコードサンプルを検討してください。

services
    .AddGrpcClient<Greeter.GreeterClient>()
    .AddStandardResilienceHandler();

上記のコードを実行すると、次の例外が発生します。

System.InvalidOperationException: The ConfigureHttpClient method is not supported when creating gRPC clients. Unable to create client with name 'GreeterClient'.

この問題を解決するには、 Grpc.Net.ClientFactory バージョン 2.64.0 以降にアップグレードすることをお勧めします。

Grpc.Net.ClientFactory バージョン 2.63.0 以前を使用しているかどうかを確認するビルド時チェックがあり、使用している場合にはコンパイル警告が生成されます。 プロジェクト ファイルで次のプロパティを設定すると、警告を抑制できます。

<PropertyGroup>
  <SuppressCheckGrpcNetClientFactoryVersion>true</SuppressCheckGrpcNetClientFactoryVersion>
</PropertyGroup>

.NET Application Insights との互換性

.NET Application Insights を使用している場合、アプリケーションで回復性機能を有効にすると、すべての Application Insights テレメトリが失われる可能性があります。 この問題は、Application Insights サービスより前に回復性機能が登録されている場合に発生します。 次のサンプルを見てみましょう。この場合は問題が発生します。

// At first, we register resilience functionality.
services.AddHttpClient().AddStandardResilienceHandler();

// And then we register Application Insights. As a result, Application Insights doesn't work.
services.AddApplicationInsightsTelemetry();

この問題は、Application Insights のバグによって発生します。次に示すように、回復性機能の前に Application Insights サービスを登録することで修正できます。

// We register Application Insights first, and now it will be working correctly.
services.AddApplicationInsightsTelemetry();
services.AddHttpClient().AddStandardResilienceHandler();