多くのサービスは、リソース消費を制御するためにスロットリングを使用し、アプリケーションがアクセスできるレートに制限を課しています。レート制限パターンは、スロットルエラーを回避し、特にバッチ処理のような大規模な反復タスクでスループットを正確に予測するのに役立ちます。
コンテキストと問題
スロットルされたサービスに対して大量の操作を実行すると、トラフィックが増加し、効率が低下する可能性があります。拒否されたリクエストを追跡し、操作を再試行する必要があり、作業を完了するために複数のパスが必要になる可能性があります。
データベースへのデータ取り込みの例を考えてみましょう:
- アプリケーションは10,000件のレコードを取り込む必要があります。各レコードは10リクエストユニット(RU)を消費し、合計100,000 RUが必要です。
- データベースインスタンスには20,000 RUのプロビジョニング容量があります。
- 10,000件のレコードをすべて送信します。2,000件が成功し、8,000件が拒否されます。
- 8,000件のレコードで再試行します。2,000件が成功し、6,000件が拒否されます。
- 6,000件のレコードで再試行します。2,000件が成功し、4,000件が拒否されます。
- 4,000件のレコードで再試行します。2,000件が成功し、2,000件が拒否されます。
- 2,000件のレコードで再試行します。すべて成功します。
ジョブは完了しましたが、実際のデータセットサイズの3倍である30,000件のレコードを送信した後でした。
この単純なアプローチには追加の問題があります:
- エラー処理のオーバーヘッド:20,000件のエラーをログに記録して処理する必要があり、メモリとストレージを消費します。
- 予測不可能な完了時間:スロットリング制限を知らなければ、処理にかかる時間を見積もることができません。
ソリューション
レート制限は、時間の経過とともにサービスに送信されるレコード数を制御することで、トラフィックを削減し、スループットを向上させます。
サービスは、さまざまなメトリックに基づいてスロットルします:
- 操作の数(例:1秒あたり20リクエスト)
- データ量(例:1分あたり2 GiB)
- 操作の相対コスト(例:1秒あたり20,000 RU)
レート制限の実装は、容量を超えることなく使用を最適化し、サービスに送信される操作を制御する必要があります。
永続的なメッセージングシステムの使用
APIがスロットルされたサービスが許可するよりも速くリクエストを処理できる場合、取り込み速度を管理する必要があります。単にリクエストをバッファリングするのはリスクがあります。アプリケーションがクラッシュすると、バッファリングされたデータが失われます。
代わりに、完全な取り込みレートを処理できる永続的なメッセージングシステムにレコードを送信します。ジョブプロセッサを使用して、スロットルされたサービスの制限内で制御されたレートでレコードを読み取ります。
永続的なメッセージングオプションには次のものがあります:
- メッセージキュー(例:RabbitMQ、ActiveMQ)
- イベントストリーミングプラットフォーム(例:Apache Kafka)
- クラウドベースのキューサービス
(高レート)"] --> B["永続的
メッセージキュー"] B --> C["ジョブプロセッサ 1"] B --> D["ジョブプロセッサ 2"] B --> E["ジョブプロセッサ 3"] C --> F["スロットルされたサービス
(制限レート)"] D --> F E --> F style A fill:#e1f5ff style B fill:#fff4e1 style F fill:#ffe1e1
細かい時間間隔
サービスは、理解しやすい時間範囲(秒単位または分単位)に基づいてスロットルすることがよくありますが、コンピュータははるかに高速に処理します。1秒に1回バッチでリリースするのではなく、より頻繁に少量を送信することで:
- リソース消費(メモリ、CPU、ネットワーク)を均等に流し続ける
- 突然のリクエストバーストによるボトルネックを防ぐ
たとえば、サービスが1秒あたり100操作を許可する場合、200ミリ秒ごとに20操作をリリースします:
複数の非協調プロセスの管理
複数のプロセスがスロットルされたサービスを共有する場合、サービスの容量を論理的に分割し、分散相互排他システムを使用してそれらのパーティションのロックを管理します。
例:
スロットルされたシステムが1秒あたり500リクエストを許可する場合:
- 1秒あたり25リクエストの価値がある20のパーティションを作成します
- 100リクエストを必要とするプロセスは4つのパーティションを要求します
- システムは10秒間2つのパーティションを付与します
- プロセスは1秒あたり50リクエストにレート制限し、2秒で完了し、ロックを解放します
実装アプローチ:
ブロブストレージを使用して、論理パーティションごとに1つの小さなファイルを作成します。アプリケーションは、短期間(例:15秒)これらのファイルの排他的リースを取得します。付与された各リースに対して、アプリケーションはそのパーティションの容量を使用できます。
25 req/s"] L2["リース 2
25 req/s"] L3["リース 3
25 req/s"] end space:3 block:service:3 S["スロットルされたサービス
合計500 req/s"] end P1 --> L1 P2 --> L2 P3 --> L3 L1 --> S L2 --> S L3 --> S style processes fill:#e1f5ff style leases fill:#fff4e1 style service fill:#ffe1e1
レイテンシを削減するために、各プロセスに少量の排他的容量を割り当てます。プロセスは、予約容量を超えた場合にのみ共有容量リースを求めます。
リース管理の代替技術には、Zookeeper、Consul、etcd、Redis/Redsyncがあります。
問題と考慮事項
💡 主要な考慮事項
スロットルエラーの処理:レート制限はエラーを削減しますが、完全には排除しません。アプリケーションは、発生するスロットルエラーを処理する必要があります。
複数のワークストリーム:アプリケーションに同じスロットルされたサービスにアクセスする複数のワークストリーム(例:一括ロードとクエリ)がある場合、すべてをレート制限戦略に統合するか、それぞれに個別の容量プールを予約します。
マルチアプリケーション使用:複数のアプリケーションが同じスロットルされたサービスを使用する場合、スロットルエラーの増加は競合を示している可能性があります。他のアプリケーションからの使用が減少するまで、一時的にスループットを削減することを検討してください。
!!!
このパターンを使用する場合
このパターンを使用して:
- スロットル制限されたサービスからのスロットルエラーを削減する
- 単純な再試行エラーアプローチと比較してトラフィックを削減する
- 処理する容量がある場合にのみレコードをデキューすることでメモリ消費を削減する
- バッチ処理の完了時間の予測可能性を向上させる
アーキテクチャの例
ユーザーがさまざまなタイプのレコードをAPIに送信するアプリケーションを考えてみましょう。各レコードタイプには、検証、エンリッチメント、データベース挿入を実行する固有のジョブプロセッサがあります。
すべてのコンポーネント(API、ジョブプロセッサ)は、独立してスケールし、直接通信しない別々のプロセスです。
(タイプAレコード)"] API --> QB["キュー B
(タイプBレコード)"] QA --> JPA["ジョブプロセッサ A"] QB --> JPB["ジョブプロセッサ B"] JPA --> LS["リースストレージ
(ブロブ 0-9)"] JPB --> LS JPA --> DB["データベース
(1000 req/s制限)"] JPB --> DB style API fill:#e1f5ff style QA fill:#fff4e1 style QB fill:#fff4e1 style LS fill:#f0e1ff style DB fill:#ffe1e1
ワークフロー:
- ユーザーがタイプAの10,000件のレコードをAPIに送信します
- APIはキューAにレコードをエンキューします
- ユーザーがタイプBの5,000件のレコードをAPIに送信します
- APIはキューBにレコードをエンキューします
- ジョブプロセッサAがブロブ2のリースを試みます
- ジョブプロセッサBがブロブ2のリースを試みます
- ジョブプロセッサAは失敗します。ジョブプロセッサBは15秒間リースを取得します(100 req/s容量)
- ジョブプロセッサBは100件のレコードをデキューして書き込みます
- 1秒後、両方のプロセッサが追加のリースを試みます
- ジョブプロセッサAはブロブ6を取得します(100 req/s)。ジョブプロセッサBはブロブ3を取得します(現在合計200 req/s)
- プロセッサはリースを競い合い、付与されたレートでレコードを処理し続けます
- リースが期限切れになると(15秒後)、プロセッサはそれに応じてリクエストレートを削減します
関連パターン
スロットリング:レート制限は通常、スロットルされたサービスに応答して実装されます。
再試行:リクエストがスロットルエラーになった場合、適切な間隔の後に再試行します。
キューベースの負荷平準化:レート制限に似ていますが、より広範です。主な違い:
- レート制限は必ずしもキューを必要としませんが、永続的なメッセージングが必要です
- レート制限は、非協調プロセス間で容量を管理するためのパーティションの分散相互排他を導入します
- キューベースの負荷平準化は、サービス間のパフォーマンスの不一致に適用されます。レート制限は特にスロットルされたサービスに対処します