レート制限パターン

Azure Service Bus
Azure Queue Storage
Azure Event Hubs

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

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

コンテキストと問題

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

一例として、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 に送信します。 すべて正常に書き込まれました。

インジェスト ジョブは正常に完了しましたが、データ セット全体の構成レコード数は 10,000 件だけであるにもかかわらず、Azure Cosmos DB に 30,000 件のレコードを送信しなければ完了しませんでした。

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

  • エラーの数が多いと、それらのエラーをログに記録して結果のログ データを処理する作業が増えるおそれがあります。 この単純なアプローチでは 20,000 個のエラーが処理され、これらのエラーをログに記録すると、処理、メモリ、またはストレージ リソースのコストが発生する可能性があります。
  • この素朴なアプローチでは、インジェスト サービスの調整の制限がわからないため、データ処理にかかる時間を予測することができません。 レート制限を使用すると、インジェストに必要な時間を計算できます。

Solution

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

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

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

調整に使用されるメトリックに関係なく、レート制限の実装には、特定の期間にサービスに送信される操作の数とサイズ、またはその一方を制御することや、調整の容量を超えないようにしながらサービスの使用を最適化することが含まれます。

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

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

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

スロットルされたサービスを呼び出す 3 つのジョブ プロセッサを備えた、耐久性のあるメッセージ フロー。

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

必須ではありませんが、スループットを向上させるために、より少ないレコードをより頻繁に送信することをお勧めします。 そのため、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 のパーティション

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

問題と注意事項

このパターンの実装方法を決めるときには、以下にご注意ください。

  • レート制限パターンは調整エラーの数を減らすことができますが、発生する可能性のある調整エラーをアプリケーションで適切に処理する必要があります。
  • お客様のアプリケーションに、調整された同じサービスにアクセスする複数のワークストリームがある場合、それらすべてをお客様のレート制限戦略に統合する必要があります。 たとえば、データベースへのレコードの一括読み込みだけでなく、その同じデータベース内のレコードのクエリもサポートできるかもしれません。 すべてのワークストリームが同じレート制限メカニズムによって制御されるようにすることで、容量を管理することができます。 または、ワークストリームごとに別々の容量プールを予約することもできます。
  • 制限されたサービスは、複数のアプリケーションで使用される場合があります。 すべてではありませんが、場合によっては、この使用方法を調整することができます (上記を参照)。 予想以上の数の調整エラーが発生し始めた場合、これはサービスにアクセスするアプリケーション間の競合の兆候である可能性があります。 その場合は、他のアプリケーションからの使用量が減少するまで、レート制限メカニズムによって課されるスループットを一時的に削減することを検討する必要があります。

このパターンを使用する状況

このパターンは次の目的で使用します。

  • スロットル制限のあるサービスで発生するスロットリング エラーを削減する。
  • エラーが発生したら再試行するという素朴なアプローチよりもトラフィックを削減する。
  • レコードを処理する容量がある場合にだけそれをデキューすることで、メモリ消費量を削減する。

ワークロード設計

設計者は、Azure Well-Architected Framework の柱で説明されている目標と原則に対処するために、ワークロードの設計でどのようにレート制限パターンを使用できるかを評価する必要があります。 例えば次が挙げられます。

Pillar このパターンが柱の目標をサポートする方法
信頼性 設計の決定により、ワークロードが 誤動作 に対して復元力を持ち、障害発生後も完全に機能する状態に 回復 することができます。 この戦術では、サービスが過度な使用の回避を求めている場合に、そのサービスと通信するときの制限とコストを確認して尊重することで、クライアントを保護します。

- RE:07 自己保護

設計決定と同様に、このパターンで導入される可能性のある他の柱の目標とのトレードオフを考慮してください。

Example

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

  1. Validation
  2. Enrichment
  3. データベースへのレコードの挿入

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

データベースに書き込む、リース用に分割されたストレージを備えたマルチキュー・マルチプロセッサのフロー。

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

  1. ユーザーが A タイプの 10,000 件のレコードを API に送信します。
  2. API により、これらの 10,000 件のレコードがキュー A にエンキューされます。
  3. ユーザーが B タイプの 5,000 件のレコードを API に送信します。
  4. API により、これらの 5,000 件のレコードがキュー B にエンキューされます。
  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 秒後も、一方または両方のジョブが完了しません。 リースの有効期限が切れると、プロセッサではデキューおよび書き込みを行う要求の数を減らす必要もあります。

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

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

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

  • Throttling. こちらで説明するレート制限パターンは、通常、調整されたサービスに応じて実装されます。
  • Retry. スロットリングされているサービスへの要求でスロットリング エラーが発生した場合、通常は、適切な間隔を置いてそれらの要求を再試行するのが適切です。

キュー ベースの負荷平準化は、レート制限パターンと似ていますが、いくつかの重要な点で異なります。

  • レート制限では負荷の管理に必ずしもキューを使用する必要はないのに対し、こちらでは永続的なメッセージング サービスを使用する必要があります。 たとえば、レート制限パターンでは、Apache Kafka や Azure Event Hubs などのサービスを使用できます。
  • レート制限パターンでは、パーティション単位の分散相互排他システムという概念を導入します。これにより、同じスロットル対象サービスと通信する、互いに調整されていない複数のプロセスの処理能力を管理できます。
  • キュー ベースの負荷平準化パターンは、サービス間でパフォーマンスの不一致がある場合や回復力を向上させる場合にいつでも適用できます。 そのため、これは、スロットルされたサービスに効率的にアクセスすることを主に対象とするレート制限よりも、より幅広いパターンです。