web-dev-qa-db-ja.com

分散システムで同時実行性の高いデクリメントカウンター

私は、カウンターを減らすための呼び出しがサービスに来るという問題に取り組んでいます。カウンターがゼロより大きい場合、呼び出しはそれを減らすことができるはずです。そうでない場合は失敗します。

かなり簡単ですか?ハァッ!

リクエストの場合、カウンター値を取得し、減らして元に戻します

さて、それは以下の制約で面白くなります:

  1. リクエストはサンドボックス化されます。したがって、リクエストは任意のホストに送信でき、各リクエストは新しいスレッドを作成し、レスポンスを返した後に終了します。 (したがって、すぐに使用できるバッチ更新はありません。つまり、各要求が-1を実行したい場合、10個の要求に代わって-10でカウンターを更新することはできません)
  2. 同じカウンター更新の並列リクエストの成功率を最大化する
  3. ソリューションによる遅延の影響を最小限に抑える(<700ms)
  4. カウンターは一部のデータストアに保存されます(たとえば、DynamoDBは、ホットパーティションを引き起こし、この奇妙な呼び出しパターンをサポートするためだけにスループットを上げることは受け入れられないため、同じキーに高速でアクセスするための適切なデータストアではない可能性があります)

正確には何が問題なのですか?

  1. 同じレコードに何度もアクセスすると、リクエスト/アクセスパターンが一種の攻撃のように見えるため、データストアの下のほとんどがスロットルを開始するホットパーティションシナリオが作成される傾向があります。 (パターンをサポートするために高スループットを維持することを提案しないでください。受け入れられません!)

  2. 競合がない場合、または同じカウンターを更新する並列要求が少ないと言った場合に、以前は機能していた要求を直接処理します。現在、ほとんどのリクエスト(99%)は、ロック/条件付きの失敗ケースが原因で失敗し、再試行はすべてが成功するまでに非常に長い時間がかかります。 (私は大丈夫ですいくつかのリクエストは〜10%失敗します)

失敗について:「カウンターが0に達したために失敗した場合は再試行できませんが、「ロック/条件付き失敗ケースによる失敗」は再試行できます。

並列リクエストの成功率を可能な限り最大化することを目的としています。

サイドノート:

私は特定のデータモデルやストアに制限されたり制限されたりしていません。つまり、問題を解決するのに役立つ任意のデータモデルを思い付くことができ、効率的な方法で、そのようなユースケースに適していると思われるデータストアを選択できます。

私は後で話すことができるかなり良い解決策(ランダム性を使用)を持っています。 (単一の解決策について議論するのではなく、問題をオープンで興味深いものにして解決するために、それを前もって置かないでください)

ここで考えを集めたかった、どのようにそれにアプローチするか!

1
gitesh.tyagi

簡単な解決策は、カウンターをN個の異なる「バケット」に分割することです。各バケットには数値X/N(四捨五入)が含まれます。ここで、Xはカウンターの初期値です。

次に、各スレッドはランダムなバケットを選択し、その値を減らします。バケットの値がすでに0の場合、スレッドは次のバケットへのアクセスを試みて繰り返すことができます。この場合、何も書き込んでいないため、実行可能なバケットが見つかるまで、新しい読み取り操作のコストのみがかかります。

これにより、衝突率が元の状況の1/Nに減少します。実際のオーバーヘッドはバケットの数とアクセス時間に依存するため、700msなどの特定の制限時間に対して判断することは不可能です。それが実際の問題になる場合(私は強く疑っています)、スケジュールされたジョブを追加してバケット内の値を再配布することでソリューションを拡張できます(すべてのバケットを合計して結果を再度分散し、バケットを持つ可能性を減らします)値0が含まれているため、新しい読み取りが必要になります)。

もちろん最良の部分は、カウンター内のより複雑なロジックと1つではなくいくつかのレコードを除いて、新しいアーキテクチャ要素(キュープロセッサなど)を設定する必要がないことです。

3
lud1977

カウンターなどのデータ値に信頼できる唯一の情報源レコードが存在し、この値が実行中のすべてのプロセスに共通であり、多くのプロセスがその値の値を変更する操作を実行している可能性があるため、不安定な場合は、何らかのレコードが必要になりますまたはフィールドのトランザクションロック。一度に1つのプロセスのみがカウンタ値を変更できます。それだけは確かです。

テーブルレコードをロックする方法の詳細については説明しません。次に、競合するすべてのプロセスによってデクリメントカウンターへの公平で公平なアクセスを最適に管理する方法と、呼び出しの失敗を最小限に抑えるパフォーマンスの高い方法でこれを行う方法が課題になります。結局のところ、10個の並行プロセスが一貫して値を受け取り、他の20個の潜在プロセスがほとんど失敗を受け取ることは望ましくありません。

私が最初に行うことは、カウンター値の取得と値のデクリメントが単一の完全なトランザクションとして存在することを確認することです。それらが2つの別々の呼び出しである場合、受信した値は、デクリメント操作が呼び出されるまでに古くなっている可能性があります。

次に、アクセスIMOを分散するための最も公平で公平な方法は、非同期 メッセージキュー インターフェイスを使用することです。カウンターのデクリメントと値の取得の要求はキューに入れられ、要求が送信された順序を保持して一度に1つずつ処理できます。メッセージを送信するMQクライアントは、相関IDを介して応答キューで非同期に適切な応答を受信します。インターフェイスへのクライアントは、失敗することを決定する前に、指定されたタイムアウト期間待機することができます。

上記のアプローチの問題は、タイムアウトを防ぐために、メッセージを処理する能力が平均リクエストのピーク時間よりも平均して速くなければならないことです。そうでない場合は、キューの最後で大量のリクエストがタイムアウトし、処理される前に失敗する可能性があります。

1
maple_shaft