【.NET】~リトライ、独自実装していませんか?~ Pollyを活用してレジリエントなアプリケーションをシンプルに実装!

PollyTitle.jpg みなさんこんにちは。 主にWebバックエンド等のフルスクラッチ開発を担当しております山本です。
この場では主に.NETやMicrosoft Azureを用いた開発の情報をお届けしていきます!

はじめに

近年の複雑化したアプリケーション開発において、セキュアかつ持続性のあるアプリケーションをスピーディかつシンプルに提供するためにはクラウドやSaaS等の外部サービス利用の利用が不可欠です。
また、規模の大きいアプリケーションでは機能をスケール可能なマイクロサービスの連携で実現することで拡張性を高めたいというニーズもあるでしょう。

しかし、外部サービスの呼び出しを含むアプリケーションにおいては、すべての機能を一つのマシン上で実行するモノリシックなアプリケーションとは異なる課題に対処する必要があります。

システム連携時の困り事…

Azure等のクラウドサービスではマシンメンテナンス等による実行インスタンスの切り替わりなど、予期せずサービスが瞬断するタイミングがあります。
また、自社のオンプレミス環境で提供するサーバであっても、キャンペーンや唐突な”バズり”等で想定以上の負荷がかかり、クライアントからの要求に応じることができなくなるかもしれません。

このような通信の失敗や一時的なサービスダウン等のエラーをどのようにハンドリングするかは、ユーザー体験やシステムの安定性に大きく影響します。

対応するには…

クラウドネイティブなアプリケーションを設計する際には、WebサーバやDBを始めとした外部リソースの呼び出し時にはエラーが発生することを当然のこととして、エラーに対してレジリエンス(回復力)の高いアプリケーションを構築することが望ましいです。

もちろんエラー処理をすべて独自実装してもよいのですが、.NET開発においてはレジリエンス実装を支援するPollyという強力なライブラリを利用することが可能です。
本記事では、Pollyの基本的な機能や利用方について解説し、よりレジリエントなアプリケーションを構築するためのヒントを提供します。

Pollyとは

Polly-Logo@2x.png Pollyは、以下の回復性パターンに沿ったエラーハンドリング機能を提供する.NET用ライブラリです。
Polly公式ドキュメント

  • リトライ(Retry): 処理が失敗した際、リトライを行う。
  • フォールバック(Fallback): 処理が失敗した際、代替の処理を行う。
  • サーキットブレーカー(Circuit Breaker): 処理が失敗した際、停止してリソースを保護する。
  • キャッシュ(Cache): 一時的な応答キャッシュを実装する。
  • バルクヘッド(Bulkhead): 同時処理数を制限し、過負荷を未然防止する。
  • タイムアウト(Timeout): 指定した時間で処理を打ち切る。

PollyのAPI設計は極めてシンプルかつ、.NET標準ライブラリとの親和性も高く直感的に利用できます。

実装

今回はMicrosoft.Extensions.Http.Pollyパッケージを用いてASP.NET Core WebAPIアプリケーションへHTTPリトライ処理を実装します。
Microsoft.Extensions.Http.PollyパッケージはASP.NETのIHttpClientFactoryと統合したPolly機能を提供するパッケージです。

事前準備

  • Visual Studio 2022及び.NET 8のインストール
  • ASP.NET Core WebAPIプロジェクトの作成
  • Microsoft.Extensions.Http.Polly NuGetパッケージのインストール

HTTPリトライ処理の実装

Microsoft.Extensions.Http.Pollyを導入することで、ASP.NET Coreにおける名前付きHttpClientのDI登録時に、HTTPエラー発生時の回復ポリシーを設定することができます。

プロジェクトテンプレートからプロジェクトを作成した場合、Program.cs内のMainメソッド内にWebApplicationBuilderインスタンスにサービスを登録している処理を見つけられるかと思います。
ここにならい、名前付きHttpClientのDI登録及びHttpClientにポリシーハンドラーを追加しましょう。


builder.Services.AddHttpClient("ClientForWebAPI1")  
    .AddPolicyHandler(GetRetryPolicy());

AddPolicyHandlerメソッドにはいくつかオーバーロードがありますが、IAsyncPolicy<HttpResponseMessage>を戻り値として持つデリゲートもしくはメソッドを引数に設定する必要があります。
今回はサンプルですので、以下のメソッドをProgramクラス内に実装しておきます。


private static IAsyncPolicy<HttpResponseMessage>GetRetryPolicy()  
{  
    return HttpPolicyExtensions  
           .HandleTransientHttpError()  
           .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));  
}  

HandleTransientHttpErrorメソッドで捕捉できるHttpStatusCodeは5XXもしくは408 Request Timeoutとなります。
このリトライポリシーを設定されたHttpClientは疎通先から5XXもしくは408応答された際、WaitAndRetryAsyncメソッドで指定された通り、指数バックオフに基づき最大3回のリトライを試行します。

指数バックオフとはリトライ戦略の一つで、リトライ試行までの待機時間を試行回数に応じて指数関数的に増やしていくことで、サーバへの過負荷を防止する仕組みです。
今回の実装では、2の試行回数乗の待機時間を次回のリトライまで待機させます。

サンプルには実装していませんが、同時刻に複数の呼び出しに対してサーバエラーが多発した際それぞれのリトライ試行間隔が揃うことによるアクセス集中を避けるために、リトライ間隔にランダムなゆらぎをもたせるジッターバックオフと併用すると尚良いですね!

応用

先程は定義済みのHandleTransientHttpErrorメソッドを用いて補足可能なエラーに対して無条件でリトライを実施していました。
ただし、真の意味でアプリケーションのレジリエンスを高めるためには、リトライすべきエラー/リトライすべきでないエラーを明確に区別するなど、エラー時にどのような回復処理が最適であるかという設計が重要になります。

例としてHandleTransientHttpErrorで捕捉される500 Internal Server Error応答は特定の要求に対するサーバアプリケーション不具合の可能性も含むため、同様のリクエストを繰り返したとて状況の改善が見込まれないかもしれません。
また、429 Too Many Requests応答はサーバ側のスロットリング制限によるもので、一定間隔を空けて再試行することで正常な応答を得ることができる可能性があります。
このようなケースを個別に検討し方針を決めることで、アプリケーションをよりよく実装することが可能でしょう。

Pollyは個別のステータスコードに対してハンドリングを細かく制御したり、複数のポリシーを組み合わせたポリシーを作成することも可能です。
状況に応じて使い分けましょう。


private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    // Policy.WrapAsyncメソッドで、複数のポリシーを包括したポリシーを作成
    return Policy.WrapAsync(
        // RequestTimeoutもしくはTooManiRequestsの場合リトライを実行するポリシー
        Policy<HttpResponseMessage>.HandleResult(response =>
        response.StatusCode == HttpStatusCode.RequestTimeout 
        || response.StatusCode == HttpStatusCode.TooManyRequests)
        .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))),
        // InternalServerErrorもしくはServiceUnavailableが3回続いた場合、10分サーキットブレイカーを開くポリシー
        Policy<HttpResponseMessage>.HandleResult(response =>
        response.StatusCode == HttpStatusCode.InternalServerError 
        || response.StatusCode == HttpStatusCode.ServiceUnavailable)
        .CircuitBreakerAsync(3, TimeSpan.FromMinutes(10)));
}

また、PollyはHTTP通信のエラーハンドリングに限ったライブラリではありません
例えばPolicy.Handle<TException>メソッドの型引数にSystem.IOExceptionを設定して、ファイルI/Oアクセス時のエラーに対して回復処理を仕込むことも可能です。


var fileRetryPolicy = Policy.Handle<IOException>().Retry(3);
fileRetryPolicy.Execute(() =>
{
    var content = File.ReadAllText("data.txt");
});

エラー時の回復処理として回復性パターンに則った処理が有効な際は、Pollyが利用できないか是非検討してみてください!

まとめ

以上、複雑化するモダンアプリケーション開発におけるエラーハンドリングをシンプルかつ集約的に実装することが可能なPollyをご紹介させていただきました。
本記事でご紹介した内容が皆様のレジリエントなアプリケーション開発の一助になれば幸いです!

また、株式会社フロッグポッドではMicrosoft Azureを用いたクラウドネイティブなWebアプリ/モバイルアプリ開発のほか、Power Platformとクラウドサービスを併用したハイブリッド開発を手掛けております。
柔軟かつスケーラブルなソリューションをお求めの方は、ぜひ弊社までご相談ください。

参考リンク

IHttpClientFactory ポリシーと Polly ポリシーで指数バックオフを含む HTTP 呼び出しの再試行を実装する
Polly公式ドキュメント
Microsoft.Extensions.Http.Polly パッケージ

お気軽にご依頼・ご相談ください

前へ

今すぐ使いたい!モダンCSS新機能の活用法

次へ

【ローコード開発】プリザンターで業務アプリ作成を始める(第2回)