resilience について
.NET 開発者向けに、Polly v8 と .NET 組み込みのレート制限を使ったレジリエンスを解説します。HTTP と非 HTTP の処理を対象に、タイムアウト、再試行、サーキットブレーカー、テレメトリー、安全な再試行を扱います。
主な機能
- 01Polly v8 のレジリエンスパイプライン
- 02HTTP クライアントの標準・カスタムハンドラー
- 03再試行、サーキットブレーカー、タイムアウト、フォールバック
- 04低遅延読み取り向けのヘッジング
- 05テレメトリーと組み込みレート制限
活用シーン
- 01外部 HTTP API にレジリエンスを設定する
- 02データベースやメッセージキュー用の名前付きパイプラインを登録する
- 03低遅延読み取りに並列リクエストを使う
- 04サーキットブレーカーを監視し API リクエストを制限する
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill resilienceインストール後、Claude Code で/resilienceを入力して読み込まれたか確認します。
詳細
レジリエンス
基本原則
- v7 ポリシーではなく、Polly v8 のレジリエンスパイプラインを使用する — Polly v8 では
PolicyがResiliencePipelineに置き換えられました。PolicyBuilder、Policy.Handle<>()、ISyncPolicyは決して使用しないでください。新しい API は合成可能で、型安全であり、IHttpClientFactoryとネイティブに統合されます。 - 手動でラップせず、
AddResilienceHandlerで設定する — HTTP 呼び出しには、DI を通じてHttpClientにパイプラインを直接追加するMicrosoft.Extensions.Http.Resilienceを使用してください。手動でExecuteAsyncをラップする必要はありません。 - ストラテジーはネストせず、組み合わせる — 1 つの
ResiliencePipelineで、リトライ + サーキットブレーカー + タイムアウトを連鎖させることができます。ストラテジーは外側から内側へ実行されます(最初に追加したものが最外側です)。ネストした try/catch や手動のオーケストレーションは不要です。 - 必ずタイムアウトを設定する — 外部への呼び出しにはすべてタイムアウトが必要です。試行ごとに適用されるよう、Polly の
AddTimeout()を最も内側のストラテジーとして使用し、必要に応じて合計経過時間を制限する外側のタイムアウトも追加してください。 - すべてを計測する — Polly v8 は
Meteringイベントを出力し、OpenTelemetry 用のTelemetryOptionsをサポートしています。これらを使用して、リトライ率、サーキットブレーカーの状態、タイムアウトの頻度を監視してください。
パターン
HTTP クライアントのレジリエンス(推奨されるデフォルト)
// Program.cs — Standard resilience handler covers 90% of use cases
builder.Services.AddHttpClient<IPaymentGateway, PaymentGatewayClient>(client =>
{
client.BaseAddress = new Uri("https://api.payments.example.com");
})
.AddStandardResilienceHandler(); // Retry + circuit breaker + timeout out of the box
// That's it. The standard handler configures:
// - Retry: 3 attempts, exponential backoff, jitter
// - Circuit breaker: 10% failure ratio over 30s sampling, 30s break
// - Attempt timeout: 10s per attempt
// - Total request timeout: 30s
理由:Microsoft.Extensions.Http.Resilience の AddStandardResilienceHandler() は、本番環境に対応したデフォルト設定を適用します。しきい値を変更する必要がある場合にのみ、上書きしてください。
HTTP のカスタムレジリエンス設定
builder.Services.AddHttpClient<ICatalogService, CatalogServiceClient>(client =>
{
client.BaseAddress = new Uri("https://api.catalog.example.com");
})
.AddResilienceHandler("catalog", builder =>
{
// Total timeout — outermost, caps total elapsed time
builder.AddTimeout(TimeSpan.FromSeconds(15));
// Retry — exponential backoff with jitter
builder.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
Delay = TimeSpan.FromMilliseconds(500),
ShouldHandle = static args => ValueTask.FromResult(
args.Outcome.Result?.StatusCode is HttpStatusCode.RequestTimeout
or HttpStatusCode.TooManyRequests
or HttpStatusCode.ServiceUnavailable
|| args.Outcome.Exception is HttpRequestException)
});
// Circuit breaker — prevent cascading failures
builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(10),
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(30)
});
// Per-attempt timeout — innermost
builder.AddTimeout(TimeSpan.FromSeconds(5));
});
理由:名前付きレジリエンスハンドラーを使うと、サービスごとに調整できます。順序が重要です。合計タイムアウト > リトライ > サーキットブレーカー > 試行ごとのタイムアウト。
HTTP 以外のレジリエンスパイプライン
// For database calls, message queues, or any non-HTTP operation
builder.Services.AddResiliencePipeline("database", builder =>
{
builder
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
Delay = TimeSpan.FromMilliseconds(200),
ShouldHandle = new PredicateBuilder()
.Handle<TimeoutException>()
.Handle<InvalidOperationException>(ex =>
ex.Message.Contains("deadlock", StringComparison.OrdinalIgnoreCase))
})
.AddTimeout(TimeSpan.FromSeconds(10));
});
// Inject and use
public sealed class OrderRepository(
AppDbContext db,
[FromKeyedServices("database")] ResiliencePipeline pipeline)
{
public async Task<Order?> GetByIdAsync(Guid id, CancellationToken ct)
{
return await pipeline.ExecuteAsync(
async token => await db.Orders.FindAsync([id], token),
ct);
}
}
理由:AddResiliencePipeline は、DI に名前付きパイプラインを登録します。[FromKeyedServices] を使って注入すると、シンプルでテストしやすいコードになります。
型付きレジリエンスパイプライン
// When the operation returns a specific type, use ResiliencePipeline<T>
builder.Services.AddResiliencePipeline<string, HttpResponseMessage>("external-api", builder =>
{
builder
.AddFallback(new FallbackStrategyOptions<HttpResponseMessage>
{
FallbackAction = static args =>
{
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("{\"status\":\"degraded\",\"data\":[]}")
};
return Outcome.FromResultAsValueTask(response);
},
ShouldHandle = static args => ValueTask.FromResult(
args.Outcome.Exception is not null
|| args.Outcome.Result?.IsSuccessStatusCode == false)
})
.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
{
MaxRetryAttempts = 2,
Delay = TimeSpan.FromMilliseconds(500)
})
.AddTimeout(TimeSpan.FromSeconds(5));
});
理由:型付きパイプラインを使うと、すべてのリトライを使い切った際にデフォルト値を返すフォールバックストラテジーを追加できます。これは適切な縮退に不可欠です。
ヘッジング(並列リクエスト)
builder.Services.AddHttpClient<ISearchService, SearchServiceClient>()
.AddResilienceHandler("search-hedging", builder =>
{
builder.AddHedging(new HttpHedgingStrategyOptions
{
MaxHedgedAttempts = 2,
Delay = TimeSpan.FromMilliseconds(500) // Send parallel request after 500ms
});
builder.AddTimeout(TimeSpan.FromSeconds(3));
});
理由:ヘッジングでは、最初のリクエストが遅延時間内に応答しない場合、並列リクエストを送信します。重複した処理を許容できる、レイテンシーに敏感な読み取りに使用してください。
テレメトリの統合
builder.Services.AddResiliencePipeline("monitored", (builder, context) =>
{
// Polly v8 emits metrics via System.Diagnostics.Metrics automatically.
// ConfigureTelemetry wires structured logging for strategy events.
builder
.ConfigureTelemetry(new TelemetryOptions
{
LoggerFactory = context.ServiceProvider.GetRequiredService<ILoggerFactory>()
})
.AddRetry(new RetryStrategyOptions { MaxRetryAttempts = 3 })
.AddCircuitBreaker(new CircuitBreakerStrategyOptions())
.AddTimeout(TimeSpan.FromSeconds(10));
});
// In Program.cs — wire up OpenTelemetry to capture Polly metrics
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => metrics.AddMeter("Polly"));
レート制限(.NET 組み込み)
.NET には AddRateLimiter() による組み込みのレート制限ミドルウェアが用意されています。外部パッケージは不要です。アルゴリズム:AddFixedWindowLimiter、AddSlidingWindowLimiter、AddTokenBucketLimiter、AddConcurrencyLimiter。
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("fixed", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromSeconds(60);
opt.QueueLimit = 0;
});
// Always return ProblemDetails with Retry-After on 429
options.OnRejected = async (context, ct) =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
context.HttpContext.Response.Headers.RetryAfter =
((int)retryAfter.TotalSeconds).ToString();
await context.HttpContext.Response.WriteAsJsonAsync(
new ProblemDetails { Title = "Too many requests", Status = 429 }, ct);
};
});
app.UseRateLimiter();
app.MapGet("/api/orders", ListOrders).RequireRateLimiting("fixed");
アンチパターン
悪い例:Polly v7 API の使用
// BAD — v7 policy syntax, do not use
var retryPolicy = Policy
.Handle<HttpRequestException>()
.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)));
var response = await retryPolicy.ExecuteAsync(() => httpClient.GetAsync("/api/data"));
良い例:Polly v8 のレジリエンスパイプライン
// GOOD — v8 pipeline via DI
builder.Services.AddHttpClient<IDataService, DataServiceClient>()
.AddStandardResilienceHandler();
悪い例:すべての呼び出しを手動でラップする
// BAD — manual resilience per call site
public async Task<Order> GetOrderAsync(Guid id)
{
try
{
return await _pipeline.ExecuteAsync(async ct =>
await _httpClient.GetFromJsonAsync<Order>($"/orders/{id}", ct));
}
catch (TimeoutRejectedException)
{
return Order.Empty;
}
catch (BrokenCircuitException)
{
return Order.Empty;
}
}
良い例:HttpClient の DI を通じてパイプラインですべてを処理する
// GOOD — resilience is configured at the HttpClient level
public async Task<Order?> GetOrderAsync(Guid id, CancellationToken ct)
{
var response = await _httpClient.GetAsync($"/orders/{id}", ct);
if (!response.IsSuccessStatusCode) return null;
return await response.Content.ReadFromJsonAsync<Order>(ct);
}
悪い例:冪等でない操作のリトライ
// BAD — retrying a POST that creates a resource risks duplicates
builder.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 5 // This will create 5 orders on transient failures!
});
良い例:冪等な操作のみをリトライするか、冪等性キーを使用する
// GOOD — use idempotency key header for non-idempotent operations
builder.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
ShouldHandle = static args => ValueTask.FromResult(
args.Outcome.Result?.StatusCode is HttpStatusCode.RequestTimeout
or HttpStatusCode.ServiceUnavailable)
});
// Pair with idempotency key in the request
httpClient.DefaultRequestHeaders.Add("Idempotency-Key", Guid.NewGuid().ToString());
悪い例:監視を行わないサーキットブレーカー
// BAD — circuit breaker with no visibility into state changes
builder.AddCircuitBreaker(new CircuitBreakerStrategyOptions());
// How do you know when it trips? You don't.
良い例:テレメトリを備えたサーキットブレーカー
// GOOD — Polly v8 metrics captured via OpenTelemetry
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => metrics.AddMeter("Polly"));
// Dashboard alerts on: polly.circuit_breaker.state = Open
判断ガイド
| シナリオ | 戦略 | 設定 |
|---|---|---|
| 外部 API への HTTP 呼び出し | AddStandardResilienceHandler() | デフォルトを使用し、特定のしきい値のみを上書きします |
| 独自のしきい値を設定する HTTP | AddResilienceHandler("name", ...) | サービスごとに調整できる名前付きハンドラー |
| データベース/EF Core 呼び出し | AddResiliencePipeline("db", ...) | デッドロック/タイムアウト時に再試行し、サーキットブレーカーは使用しません |
| メッセージキューへの公開 | AddResiliencePipeline("mq", ...) | 指数バックオフとタイムアウトを伴う再試行 |
| レイテンシー重視の読み取り | AddHedging(...) | 遅延のしきい値を超えた後に並列リクエストを送信します |
| グレースフルデグラデーション | AddFallback(...) | 全体的な失敗時にキャッシュ値/デフォルト値を返します |
| 試行ごとの時間制限 | 最も内側に AddTimeout(...) | 操作に応じて 2~10 秒 |
| 操作全体の時間制限 | 最も外側に AddTimeout(...) | すべての再試行時間の合計+バッファー |
| 冪等でない書き込み | 冪等性キーを使用して再試行 | または再試行せず、即座に失敗させます |
| 読み取り中心のマイクロサービス | 標準ハンドラー+ヘッジング | 冗長性による低レイテンシー |
| API のレート制限 | AddRateLimiter() + RequireRateLimiting() | エンドポイントごとに固定、スライディング、またはトークンバケットを使用します |