レート制限パターン

アプリケーションがサービスに要求を送信する速度を制御して、サービスの 調整 制限と全体的な容量内に収まるようにします。 このアプローチは、調整エラーを回避または最小限に抑え、スループットをより正確に予測するのに役立ちます。

レート制限は多くのシナリオで適切ですが、バッチ処理などの大規模で反復的な自動化タスクに特に役立ちます。

コンテキストと問題

調整されたサービスに対して多数の操作を実行すると、拒否された要求を追跡してから操作を再試行する必要があるため、トラフィックが増加し、スループットが低下する可能性があります。 操作数が増えると、スロットリング制限によりデータの再送信を複数回行う必要が生じる場合があり、その結果、パフォーマンスへの影響が大きくなります。

たとえば、データをAzure Cosmos DBに取り込むための、次の問題のあるエラー時の再試行プロセスを考えてみましょう。

  1. アプリケーションでは 10,000 件のレコードを Azure Cosmos DB に取り込む必要があります。 各レコードの取り込みには 10 個の要求ユニット (RU) が必要であるため、ジョブを完了するには合計 100,000 RU が必要です。

  2. ご使用の Azure Cosmos DB インスタンスには、20,000 個の RU 容量がプロビジョニングされています。

  3. 10,000 件すべてのレコードを Azure Cosmos DB に送信します。2,000 件のレコードが正常に書き込まれ、8,000 件のレコードが拒否されます。

  4. 残りの 8,000 件のレコードを Azure Cosmos DB に送信します。2,000 件のレコードが正常に書き込まれ、6,000 件のレコードが拒否されます。

  5. 残りの 6,000 件のレコードを Azure Cosmos DB に送信します。2,000 件のレコードが正常に書き込まれ、4,000 件のレコードが拒否されます。

  6. 残りの 4,000 件のレコードを Azure Cosmos DB に送信します。2,000 件のレコードが正常に書き込まれ、2,000 件のレコードが拒否されます。

  7. 残りの 2,000 件のレコードを Azure Cosmos DB に送信します。 すべて正常に書き込まれました。

インジェスト ジョブは正常に完了しますが、Azure Cosmos DBに 30,000 レコードを送信した後にのみ完了します。 データ セット全体は、10,000 個のレコードのみで構成されます。

この例では、他にも考慮すべき要素があります。

  • また、多数のエラーが発生すると、これらのエラーをログに記録し、結果のログ データを処理するための余分な作業が発生する可能性があります。 上記の方法では 20,000 個のエラーが処理され、これらのエラーをログに記録すると、処理、メモリ、またはストレージ リソースのコストが発生する可能性があります。

  • インジェスト サービスの調整制限がわからないため、データ処理にかかる時間に対する期待値を設定する方法はありません。 レート制限を使用すると、インジェストに必要な時間を計算できます。

解決策

レート制限を使用すると、特定の期間にサービスに送信されるレコードの数が減るため、トラフィックを減らすことができ、スループットを改善できる可能性があります。

サービスは、時間の経過と同時に次のようなさまざまなメトリックに基づいて要求を調整できます。

  • 操作の数 (例: 1 秒あたり 20 件の要求)。
  • データの量 (例: 1 分あたり 2 GiB)。
  • 操作の相対的なコスト (例: 1 秒あたり 20,000 個の RU)。

調整に使用するメトリックに関係なく、レート制限の実装では、特定の期間中にサービスに送信される操作の数やサイズを制御する必要があります。 レート制限により、調整容量を超えることなくサービスの使用が最適化されます。

API が制限されたインジェストサービスで許可されているよりも速い速度で要求を処理するシナリオでは、サービスの使用速度を適切に管理する必要があります。 調整をデータ レートの不一致としてのみ扱い、サービスが復旧するまでインジェスト要求をバッファリングするとリスクが発生します。 このシナリオでアプリケーションの応答が停止すると、バッファー内のデータが失われる可能性があります。

このリスクを回避するには、レコードを、フル インジェスト レートを処理できる永続性のあるメッセージング システムに送信することを検討してください (Azure Event Hubs などのサービスは、1 秒あたり何百万もの操作を処理できます)。その後、1 つ以上のジョブ プロセッサを使用して、調整されたサービスの制限内にある制御されたレートでメッセージング システムからレコードを読み取ることができます。 メッセージング システムにレコードを送信すると、特定の時間間隔で処理できるレコードのみをデキューすることができるため、内部メモリを節約できます。

Azure には、このパターンで使用できるいくつかの永続的なメッセージング サービスが用意されています。たとえば、次のものがあります。

永続的なメッセージング フローを示す図。3つのジョブ プロセッサがスロットルされたサービスを呼び出す。

レコードを送信する際に使用する時間単位は、サービスがスロットル制御を行う時間単位よりも細かい場合があります。 システムは、多くの場合、簡単に理解して操作できる期間に基づいてスロットルを設定します。 ただし、サービスを実行しているコンピューターの場合、これらの期間は、情報を処理できる速度と比較して非常に長くなる可能性があります。 たとえば、システムで 1 秒または 1 分単位で調整される場合でも、通常、コードはナノ秒またはミリ秒単位で処理されます。

必須ではありませんが、スループットを向上させるために、より少ない数のレコードをより頻繁に送信することをお勧めします。 つまり、1秒ごとや1分ごとにレコードをまとめてリリースするのではなく、それより細かい粒度で制御することで、リソース消費量(メモリ、CPU、ネットワーク)をより均一なペースに保つことができます。 この方法では、要求の突然のバーストによって引き起こされる潜在的なボトルネックを防ぐことができます。 たとえば、サービスで 1 秒あたり 100 操作が許可されている場合、レートリミッターの実装では、次のグラフに示すように、200 ミリ秒ごとに 20 の操作を解放することで要求が均等に出力される可能性があります。

時間の経過に伴うレート制限付きフローを示すグラフ。

さらに、調整されていない複数のプロセスで、調整されたサービスを共有する必要がある場合もあります。 このシナリオでレート制限を実装するには、サービスの容量を論理的にパーティション分割し、分散相互除外システムを使用してそれらのパーティションの排他ロックを管理できます。 その場合、連携していないプロセスは、容量が必要になるたびに、それらのパーティションのロックを奪い合う可能性があります。 プロセスでは、ロックを保持しているパーティションごとに、一定の容量が付与されます。

たとえば、調整されたシステムで毎秒 500 件の要求が許可されている場合、20 個のパーティションを作成し、それぞれ毎秒 25 個の要求を処理できるようにすることができます。 プロセスで 100 件の要求を発行する必要がある場合、分散相互排他システムに 4 つのパーティションを要求します。 このシステムから 10 秒間に 2 つのパーティションが許可されます。 そこで、プロセスは制限を 1 秒あたり 50 件の要求に調整し、2 秒でタスクを完了してから、ロックを解放します。

このパターンを実装する 1 つの方法は、Azure Storageを使用することです。 このシナリオでは、コンテナー内の論理パーティションごとに 0 バイトの BLOB を 1 つ作成します。 その後、アプリケーションは短期間 (たとえば、15 秒) にわたって、これらの BLOB に対する排他的リースを直接取得できます。 アプリケーションに付与されるリースごとに、そのパーティションの容量を使用できます。 その後、アプリケーションはリース時間を追跡して、有効期限が切れたときに、アプリケーションが付与された容量の使用を停止できるようにする必要があります。 このパターンを実装する場合、容量が必要なときに各プロセスでランダム パーティションのリースを試みることがよくあります。

待機時間をさらに短縮するために、プロセスごとに少量の排他容量を割り当てることもできます。 この場合、プロセスでは、予約容量を超える容量が必要な場合にのみ、共有容量のリースを取得しようとします。

Azure Blob Storageの BLOB パーティションで排他的リースを競合する複数のプロセスを示す図。

Azure Storageの代わりに、ZooKeeperetcdRedis/Redsync などのテクノロジを使用して、この種のリース管理システムを実装することもできます。

問題と考慮事項

このパターンを実装する方法を決定するときは、次の点を考慮してください。

  • レート制限パターンでは調整エラーの数を減らすことができますが、発生する可能性のある調整エラーをアプリケーションで適切に処理する必要があります。

  • 再試行がレート制限と調整されていることを確認します。 ブラインド再試行または過度にアグレッシブな再試行では、負荷が増加し、再試行の嵐が発生する可能性があるため、バックプレッシャー信号 (たとえば、 Retry-Afterを含む HTTP 429) を伝達し、試行間のランダムな遅延が少ない限られた回数の再試行を使用します。

  • アプリケーションに、同じ調整されたサービスにアクセスする複数のワークストリームがある場合は、それらのすべてをレート制限戦略に統合する必要があります。 たとえば、データベースへのレコードの一括読み込みだけでなく、その同じデータベース内のレコードのクエリもサポートできるかもしれません。 すべてのワークストリームが同じレート制限メカニズムによってゲートされるようにすることで、容量を管理できます。 または、ワークストリームごとに別々の容量プールを予約することもできます。

  • 制限されたサービスは、複数のアプリケーションで使用される場合があります。 場合によっては、(この記事で前に示したように) その使用法を調整することができます。 予想を超える数の調整エラーが表示され始めた場合、その数が増えると、サービスにアクセスするアプリケーション間の競合が示される可能性があります。 この場合は、他のアプリケーションからの使用量が減少するまで、レート制限メカニズムによって課されるスループットを一時的に削減することを検討する必要があります。

このパターンを使用する場合

このパターンは次の状況で使用します。

  • レート制限付きサービスによって発生する調整エラーを減らす必要があります。

  • 単純な再試行オンエラーアプローチと比較して、トラフィックを最小限に抑えたいと考えています。

  • レコードを処理するのに十分な容量がある場合にのみ、レコードをデキューすることによってメモリ消費量を削減する必要があります。

このパターンは、次の場合に適さない場合があります。

  • この操作では、待機時間が非常に短い即時同期完了が必要であり、キューまたは遅延処理を許容できません。

  • 主なボトルネックはリクエスト率ではなく、むしろ同時実行数やリソース競合(たとえば、CPU の飽和や長時間実行中の処理)です。 このような場合は、スケーリングまたはコンカレンシー制御がより適切です。

ワークロード設計

ワークロードの設計でレート制限パターンを使用して、Azure Well-Architected Framework の柱で説明されている目標と原則に対処する方法を評価します。 次の表は、このパターンが各柱の目標をサポートする方法に関するガイダンスを示しています。

このパターンが柱の目標をサポートする方法
信頼性設計の決定は、故障に対するワークロードの回復性を高め、障害の発生後にワークロードを完全な機能状態に回復させるために役立ちます。 この戦術は、サービスが過剰な使用を回避することを好む場合に、サービスとの通信に関する制限事項とコストを認識し、尊重することで、クライアントを保護します。

- RE:07 自己保護

このパターンによって柱内にトレードオフが生じる場合は、他の柱の目標に照らして検討してください。

Example

次のアプリケーションの例では、ユーザーはさまざまな種類のレコードを API に送信できます。 各レコードの種類には、次の手順を実行する一意のジョブ プロセッサがあります。

  1. 検証
  2. 濃縮
  3. データベースへのレコードの挿入

アプリケーションのすべてのコンポーネント (API、ジョブ プロセッサ A、ジョブ プロセッサ B) は、個別にスケーリングできる個別のプロセスです。 プロセスは互いに直接通信しません。

スロットルされたデータベースに書き込む、パーティション分割されたリース ストレージを含むマルチキュー、マルチプロセッサのフローを示す図。

この図は、共有 API を介してレコードを送信している 2 人のユーザーを示しています。 各レコードの種類は、個別のキューにルーティングされ、専用ジョブ プロセッサによって処理され、Azure Storageの BLOB パーティション リースによって制御される制御されたレートで調整されたデータベースに書き込まれます。 2 人のユーザーはそれぞれ、メッセージのバッチを API コンポーネントに送信します。 1 人のユーザーが 10,000 メッセージを送信し、もう 1 人が 5,000 メッセージを送信します。 この API は、10,000 個のメッセージをキュー A にルーティングし、5,000 メッセージをキュー B にルーティングします。キュー A はジョブ プロセッサ A に接続し、キュー B はジョブ プロセッサ B に接続します。ジョブ プロセッサ A は、1 秒あたり 300 レコードの速度でデータベースに書き込みます。 ジョブ プロセッサ B は、1 秒あたり 500 レコードでデータベースに書き込みます。 どちらのジョブ プロセッサも、"1 秒あたり 800 レコードで調整済み" というラベルの付いたデータベース コンポーネントに接続します。 2 つのジョブ プロセッサの下に、Azure Storageには 0 から 7 のラベルが付いた 8 つの BLOB パーティションが含まれています。 メモは、各パーティションが 1 秒あたり 100 レコードの価値があることを示しています。 複数の矢印を使用して、ジョブ プロセッサを BLOB パーティションに接続します。 緑色の矢印は、ジョブ プロセッサ A からパーティション 0、4、および 6 を指しています。これは、プロセッサがこれら 3 つのパーティションのリースを保持していることを示します。 これらのリースによって、毎秒 300 レコードの処理速度が実現されています。 緑色の矢印は、ジョブ プロセッサ B からパーティション 1、2、3、5、および 7 を指しています。 これらの矢印は、このプロセッサが 5 つのパーティションでリースを保持していることを示しています。 これらのリースが、1秒あたり500レコードの処理速度の理由です。 失敗したリース試行は、他のプロセッサによって既に保持されているパーティションに交差する赤い矢印として表示されます。 これらの矢印は競合の解決を表します。 この図は、両方のプロセッサで保持されているすべてのパーティションの合計が 1 秒あたり 800 レコードであり、データベースのスロットル制限に一致することを示しています。

この例では、各 BLOB リースは、許可されたデータベース スループットの固定共有を表します。 プロセッサは、現在保持されているリースの合計レートでのみデキューおよび書き込みを行うことができます。 プロセッサが時間の経過と同時にリースを取得または失うにつれて、許可された書き込み速度が変更されます。これにより、データベース トラフィックの合計が構成された制限内に保持され、キューに登録されているすべての作業が進行します。

この図には、次のワークフローが組み込まれています。

  1. ユーザーが A タイプの 10,000 件のレコードを API に送信します。
  2. API は、キュー A にこれらの 10,000 レコードをエンキューします。
  3. ユーザーが B タイプの 5,000 件のレコードを API に送信します。
  4. API は、キュー B にこれらの 5,000 個のレコードをエンキューします。
  5. ジョブ プロセッサ A は、キュー A にレコードがあることを確認し、BLOB 2 で排他的リースを取得しようとします。
  6. ジョブ プロセッサ B は、キュー B にレコードがあることを確認し、BLOB 2 で排他的リースを取得しようとします。
  7. ジョブ プロセッサ A がリースを取得できません。
  8. ジョブ プロセッサ B は、BLOB 2 のリースを 15 秒間取得します。 これで、データベースへの要求を毎秒 100 件というレートでレート制限できるようになりました。
  9. ジョブ・プロセッサー B は、キュー B から 100 個のレコードをデキューして書き込みます。
  10. 1 秒が経過します。
  11. ジョブ プロセッサ A は、キュー A にさらに多くのレコードがあることを把握し、blob 6 の排他的リースを取得しようとします。
  12. ジョブ プロセッサ B は、キュー B にさらに多くのレコードがあることを把握し、blob 3 の排他的リースを取得しようとします。
  13. ジョブ プロセッサ A は、BLOB 6 のリースを 15 秒間取得します。 これで、データベースへの要求を毎秒 100 件というレートでレート制限できるようになりました。
  14. ジョブ プロセッサ B は、BLOB 3 のリースを 15 秒間取得します。 これで、データベースへの要求を毎秒 200 件というレートでレート制限できるようになりました (blob 2 のリースも保持しています。)
  15. ジョブ プロセッサ A は、キュー A から 100 個のレコードをデキューして書き込みます。
  16. ジョブ・プロセッサー B は、キュー B から 200 個のレコードをデキューし、それらを書き込みます。
  17. 1 秒が経過します。
  18. ジョブ プロセッサ A は、キュー A にさらに多くのレコードがあると判断し、blob 0 に対する排他リースの取得を試みます。
  19. ジョブ プロセッサ B は、キュー B にさらに多くのレコードがあることを把握し、BLOB 1 の排他的リースを取得しようとします。
  20. ジョブ プロセッサ A は、BLOB 0 のリースを 15 秒間取得します。 これで、データベースへの要求を毎秒 200 件というレートでレート制限できるようになりました (blob 6 のリースも保持しています。)
  21. ジョブ プロセッサ B は、BLOB 1 のリースを 15 秒間取得します。 これで、データベースへの要求を毎秒 300 件というレートでレート制限できるようになりました (blob 2 および 3 のリースも保持しています。)
  22. ジョブ プロセッサ A は、キュー A から 200 個のレコードをデキューして書き込みます。
  23. ジョブ・プロセッサー B は、キュー B から 300 個のレコードをデキューし、それらを書き込みます。
  24. などなど。

15 秒後も、一方または両方のジョブは完了しません。 リースの有効期限が切れると、プロセッサはデキューして書き込む要求の数も減らす必要があります。

このパターンの実装は、さまざまなプログラミング言語で使用できます。

  • Go の実装は GitHubでできます。
  • Java の実装は GitHubでできます。

次のステップ

このパターンを実装する場合は、次のガイダンスも関連する場合があります。

このパターンを実装する場合は、次のパターンとガイダンスも関連する場合があります。

  • Throttling. レート制限パターンは通常、スロットリングされるサービスに対応するために実装されます。

  • Retry. 調整されたサービスに対する要求によって調整エラーが発生する場合は、通常、適切な間隔でこれらの要求を再試行することをお勧めします。

  • Queue-Based 負荷平準化 はレート制限パターンに似ていますが、いくつかの重要な方法で異なります。

    • レート制限では負荷の管理に必ずしもキューを使用する必要はないのに対し、こちらでは永続的なメッセージング サービスを使用する必要があります。 たとえば、レート制限パターンでは、Apache Kafka や Event Hubs などのサービスを使用できます。

    • レート制限パターンでは、パーティションに分散相互除外システムの概念が導入されています。これにより、同じ調整されたサービスと通信する複数の調整されていないプロセスの容量を管理できます。

    • キューベースの負荷平準化パターンは、サービス間で処理能力のミスマッチがある場合や、耐障害性を向上させたい場合に有効です。 そのため、レート制限よりも広いパターンです。これは、調整されたサービスへの効率的なアクセスに特に関係しています。