一時的な障害エラーから復旧できる堅牢な 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
詳細については、「.NET アプリケーションでの dotnet パッケージの追加 または パッケージの依存関係の管理」を参照してください。
HTTP クライアントに回復性を追加する
HttpClient に回復性を追加するには、使用できるいずれかの IHttpClientBuilder メソッドの呼び出しから返される AddHttpClient 型で呼び出しをチェーンします。 詳細については、「.NET を使用した IHttpClientFactory」を参照してください。
回復性中心の拡張機能がいくつか用意されています。 標準的で、さまざまな業界のベスト プラクティスが採用されているものもあれば、よりいっそうカスタマイズ可能なものもあります。 回復性を追加するときは、追加する回復性ハンドラーを 1 つだけにして、ハンドラーをスタックしないようにする必要があります。 複数の回復性ハンドラーを追加する必要がある場合は、回復性戦略をカスタマイズできる AddResilienceHandler
拡張メソッドの使用を検討する必要があります。
重要
この記事内のすべての例は、AddHttpClient インスタンスを返す Microsoft.Extensions.Http ライブラリの IHttpClientBuilder API に依存しています。 IHttpClientBuilder インスタンスを使って、HttpClient を構成し、回復性ハンドラーを追加します。
標準の回復性ハンドラーを追加する
標準の回復性ハンドラーでは、相互に積み重ねられた複数の回復性戦略が使われており、要求を送信して一時的なエラーを処理する既定のオプションを備えています。 標準の回復性ハンドラーは、AddStandardResilienceHandler
インスタンスで IHttpClientBuilder 拡張メソッドを呼び出すことによって追加されます。
var services = new ServiceCollection();
var httpClientBuilder = services.AddHttpClient<ExampleClient>(
configureClient: static client =>
{
client.BaseAddress = new("https://jsonplaceholder.typicode.com");
});
上記のコードでは次の操作が行われます。
- ServiceCollection インスタンスを作成します。
-
HttpClient 型の
ExampleClient
をサービス コンテナーに追加します。 - ベース アドレスとして HttpClient を使うように
"https://jsonplaceholder.typicode.com"
を構成します。 - この記事の他の例で使われる
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");
}
}
上記のコードでは次の操作が行われます。
-
ExampleClient
を受け入れるコンストラクターを持つ HttpClient 型を定義します。 -
GetCommentsAsync
エンドポイントに GET 要求を送信して応答を返す/comments
メソッドを公開します。
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 と同様に、既定のオプションと適用される回復性戦略をカスタマイズできるオーバーロードがあります。
標準の回復性ハンドラーを削除する
以前に登録されたすべての回復性ハンドラーを削除するメソッド RemoveAllResilienceHandlers があります。 これは、既存の回復性ハンドラーをクリアしてカスタム ハンドラーを追加する必要がある場合に便利です。
次の例では、HttpClient メソッドを使用してカスタム AddHttpClient
を構成し、定義済みの回復性戦略をすべて削除して、新しいハンドラーに置き換える方法を示します。
この方法では、既存の構成をクリアし、特定の要件に従って新しい構成を定義できます。
// By default, we want all HttpClient instances to include the StandardResilienceHandler.
services.ConfigureHttpClientDefaults(builder => builder.AddStandardResilienceHandler());
// For a named HttpClient "custom" we want to remove the StandardResilienceHandler and add the StandardHedgingHandler instead.
services.AddHttpClient("custom")
.RemoveAllResilienceHandlers()
.AddStandardHedgingHandler();
上記のコードでは次の操作が行われます。
- ServiceCollection インスタンスを作成します。
- すべての HttpClient インスタンスに標準の回復性ハンドラーを追加します。
- "カスタム" HttpClientの場合:
- 以前に登録されたすべての定義済みの回復性ハンドラーを削除します。 これは、クリーンな状態から始めて独自のカスタム戦略を追加する場合に便利です。
-
StandardHedgingHandler
にHttpClientを追加します。AddStandardHedgingHandler()
は、再試行メカニズム、サーキット ブレーカー、その他の回復性の手法など、アプリケーションのニーズに合った戦略に置き換えることができます。
標準の回復性ハンドラーの既定値
既定の構成では、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
特定の HTTP メソッドの一覧の再試行を無効にする
既定では、標準の回復性ハンドラーは、すべての HTTP メソッドの再試行を行うために構成されます。 一部のアプリケーションでは、このような動作は望ましくないか、有害である可能性があります。 たとえば、POST 要求がデータベースに新しいレコードを挿入した場合、そのような要求に対して再試行を行うと、データの重複が発生する可能性があります。 特定の HTTP メソッドの一覧の再試行を無効にする必要がある場合は、DisableFor(HttpRetryStrategyOptions, HttpMethod[]) メソッドを使用できます。
httpClientBuilder.AddStandardResilienceHandler(options =>
{
options.Retry.DisableFor(HttpMethod.Post, HttpMethod.Delete);
});
または、DisableForUnsafeHttpMethods(HttpRetryStrategyOptions) メソッドを使用して、POST
、PATCH
、PUT
、DELETE
、および CONNECT
要求の再試行を無効にすることができます。 RFC によると、これらのメソッドは安全でないと見なされます。つまり、セマンティクスは読み取り専用ではありません。
httpClientBuilder.AddStandardResilienceHandler(options =>
{
options.Retry.DisableForUnsafeHttpMethods();
});
標準のヘッジ ハンドラーを追加する
標準のヘッジ ハンドラーは、要求の実行を標準のヘッジ メカニズムでラップします。 ヘッジ処理では、遅い要求が並列で再試行されます。
標準のヘッジ ハンドラーを使うには、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 にヘッジ ハンドラーを追加します。
-
IRoutingStrategyBuilder
メソッドを使って順序付けされたグループを構成するように、ConfigureOrderedGroups
を構成します。 - 要求の 3% を
EndpointGroup
エンドポイントにルーティングし、要求の 97% をorderedGroup
エンドポイントにルーティングするhttps://example.net/api/experimental
に、https://example.net/api/stable
を追加します。 -
IRoutingStrategyBuilder
メソッドを使って構成するように、ConfigureWeightedGroups
を構成します
重み付けされたグループを構成するには、ConfigureWeightedGroups
型で IRoutingStrategyBuilder
メソッドを呼び出します。 次の例では、IRoutingStrategyBuilder
メソッドを使って重み付けされたグループを構成するように、ConfigureWeightedGroups
を構成します。
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 にヘッジ ハンドラーを追加します。
-
IRoutingStrategyBuilder
メソッドを使って重み付けされたグループを構成するように、ConfigureWeightedGroups
を構成します。 -
SelectionMode
をWeightedGroupSelectionMode.EveryAttempt
に設定します。 - 要求の33%を
WeightedEndpointGroup
エンドポイントにルーティングし、33%をweightedGroup
エンドポイントにルーティングし、33%をhttps://example.net/api/a
エンドポイントにルーティングするhttps://example.net/api/b
にhttps://example.net/api/c
を追加します。
ヒント
ヘッジ試行の最大数は、構成されるグループの数に直接関連付けられます。 たとえば、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));
});
上記のコードでは次の操作が行われます。
-
"CustomPipeline"
で名前pipelineName
を指定して回復性ハンドラーをサービス コンテナーに追加します。 - エクスポネンシャル バックオフ、5 回の再試行、ジッター優先を備えた再試行戦略を、回復性ビルダーに追加します。
- サンプリング時間 10 秒、障害率 0.2 (20%)、最小スループット 3、および
RequestTimeout
とTooManyRequests
の HTTP 状態コードを処理する述語を備えたサーキット ブレーカー戦略を、回復性ビルダーに追加します。 - タイムアウトが 5 秒のタイムアウト戦略を、回復性ビルダーに追加します。
回復性戦略ごとに多くのオプションを使用できます。 詳しくは、戦略に関する Polly のドキュメントをご覧ください。
ShouldHandle
デリゲートの構成について詳しくは、事後対応戦略での障害処理に関する Polly のドキュメントをご覧ください。
Warnung
再試行戦略とタイムアウト戦略の両方を使用していて、再試行戦略で ShouldHandle
デリゲートを構成する場合は、Polly のタイムアウト例外を処理するかどうかを検討してください。 Polly は、標準のTimeoutRejectedException
ではなく、(Exceptionから継承される) TimeoutExceptionをスローします。
動的再読み込み
Polly では、構成された回復性戦略の動的な再読み込みがサポートされています。 つまり、実行時に回復性戦略の構成を変更できます。 動的再読み込みを有効にするには、AddResilienceHandler
を公開する適切な ResilienceHandlerContext
オーバーロードを使います。 特定のコンテキストに対応する回復性戦略オプションの 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);
});
上記のコードでは次の操作が行われます。
-
"AdvancedPipeline"
で名前pipelineName
を指定して回復性ハンドラーをサービス コンテナーに追加します。 - 指定された
"AdvancedPipeline"
オプションが変化したら常に、RetryStrategyOptions
パイプラインの再読み込みを有効にします。 - 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);
}
上記のコードでは次の操作が行われます。
- IServiceProvider から ServiceCollection をビルドします。
-
ExampleClient
から IServiceProvider を解決します。 -
GetCommentsAsync
でExampleClient
メソッドを呼び出してコメントを取得します。 - 各コメントをコンソールに書き込みます。
ネットワークがダウンするか、サーバーが応答しなくなる状況を想像してください。 次の図は、ExampleClient
と GetCommentsAsync
メソッドについて、回復性戦略が状況を処理する方法を示したものです。
上の図では次のことが示されています。
-
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 バージョン 2.22.0 以前を使用している場合、アプリケーションで回復性機能を有効にすると、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();
この問題は、.NET Application Insights をバージョン 2.23.0 以降に更新することで解決できます。 更新できない場合は、次に示すように、回復性機能の前に Application Insights サービスを登録すると、問題が解決します。
// We register Application Insights first, and now it will be working correctly.
services.AddApplicationInsightsTelemetry();
services.AddHttpClient().AddStandardResilienceHandler();
.NET