高頻度イベントを接続制限のあるデータベースに保存する


13

サーバーに大量のイベントが流入し、平均して1秒あたり約1000イベント(ピークは〜2000)に対処しなければならない状況があります。

問題

私たちのシステムはHerokuでホストされ、比較的高価なHeroku Postgres DBを使用します。これにより、最大500のDB接続が可能になります。接続プーリングを使用して、サーバーからDBに接続します。

DB接続プールが処理できるよりも速くイベントが入ります

私たちが抱えている問題は、イベントが接続プールが処理できるよりも速く来るということです。1つの接続がサーバーからDBへのネットワークラウンドトリップを終了するまでに、n追加のイベントが入るよりも多く、プールに解放されます。

最終的に、イベントは蓄積され、保存されるのを待機します。プールに使用可能な接続がないため、タイムアウトし、システム全体が動作不能になります。

クライアントから遅いペースで問題のある高周波イベントを発信することで緊急事態を解決しましたが、その高周波イベントを処理する必要がある場合にこのシナリオを処理する方法を知りたいです。

制約

他のクライアントがイベントを同時に読み取りたい場合があります

他のクライアントは、DBにまだ保存されていない場合でも、特定のキーを持つすべてのイベントの読み取りを継続的に要求します。

クライアントはGET api/v1/events?clientId=1、クライアント1によって送信されたすべてのイベントを照会して取得できます。それらのイベントがまだDBに保存されていない場合でもです。

これに対処する方法に関する「教室」の例はありますか?

可能な解決策

サーバーのイベントをキューに登録します

サーバー上のイベントをキューに入れることができます(キューの最大同時実行数は400であるため、接続プールが不足することはありません)。

次の理由により、これは悪い考えです。

  • 使用可能なサーバーメモリを使い果たします。スタックされたエンキューイベントは、大量のRAMを消費します。
  • サーバーは24時間ごとに1回再起動します。これはHerokuによって課される厳しい制限です。イベントがエンキューされている間にサーバーを再起動すると、エンキューされたイベントが失われます。
  • サーバーに状態が導入されるため、スケーラビリティが低下します。マルチサーバー設定があり、クライアントがキューに入れられたイベントと保存されたイベントをすべて読みたい場合、キューに入れられたイベントがどのサーバーに存在するかはわかりません。

別のメッセージキューを使用する

メッセージキュー(RabbitMQなど)を使用して、メッセージをポンプで送り、もう一方の端にはDB上のイベントの保存のみを処理する別のサーバーがあると仮定します。

メッセージキューがエンキューされたイベント(まだ保存されていない)のクエリを許可するかどうかわからないので、別のクライアントが別のクライアントのメッセージを読みたい場合、DBから保存されたメッセージとキューから保留中のメッセージを取得できますそれらを連結して、読み取り要求クライアントに返送できるようにします。

複数のデータベースを使用し、それぞれが中央のDBコーディネーターサーバーでメッセージの一部を保存して、それらを管理します

しかし、もう1つの解決策は、中央の「DBコーディネーター/ロードバランサー」で複数のデータベースを使用することです。イベントを受信すると、このコーディネーターはメッセージを書き込むデータベースの1つを選択します。これにより、複数のHerokuデータベースを使用できるようになり、接続の制限がデータベースの500倍になります。

読み取りクエリで、このコーディネーターはSELECT各データベースにクエリを発行し、すべての結果をマージして、読み取りを要求したクライアントにそれらを送り返すことができます。

次の理由により、これは悪い考えです。

  • この考えは...ええと..オーバーエンジニアリングのように聞こえますか?同様に管理するのは悪夢です(バックアップなど)。構築と保守は複雑で、絶対に必要でない限り、KISS違反のように聞こえます。
  • 一貫性を犠牲にます。このアイデアを採用すれば、複数のDBでトランザクションを実行することはできません。

3
あなたのボトルネックはどこですか?接続プールについて言及していますが、それは挿入ごとの速度ではなく、並列性にのみ影響します。接続数が500で、たとえば2000QPSの場合、各クエリが250ミリ秒以内に完了する場合、これは問題なく機能するはずです。なぜ15msを超えるのですか?また、PaaSを使用することにより、データベースハードウェアのスケーリングやリードレプリカを使用してプライマリデータベースの負荷を軽減するなど、大幅な最適化の機会を放棄することに注意してください。展開が最大の問題でない限り、Herokuには価値がありません。
アモン

@amonボトルネックは確かに接続プールです。ANALYZEクエリ自体を実行しましたが、問題はありません。また、接続プールの仮説をテストするためのプロトタイプを作成し、これが実際に問題であることを確認しました。データベースとサーバー自体は異なるマシンに存在するため、待ち時間が発生します。また、どうしても必要な場合を除き、Herokuをあきらめたくはありません。展開について心配しないことは、私たちにとって大きなプラスです。
ニックキリアキデス

1
そうは言っても、現在の問題を解決するのに役立つ可能性のあるマイクロ最適化があることを理解しています。私の問題に対するスケーラブルなアーキテクチャソリューションがあるかどうか疑問に思っています。
ニックキリアキデス

3
接続プールが問題であることをどの程度正確に確認しましたか?@amonは彼の計算で正しいです。select null500接続で発行してみてください。接続プールが問題ではないことに気付くでしょう。
usr

1
select nullに問題がある場合は、おそらく正しいでしょう。そのすべての時間が費やされる場所は面白いでしょうが。そんなに遅いネットワークはありません。
usr

回答:


9

入力ストリーム

1000イベント/秒がピークを表しているのか、それが連続的な負荷なのかは明確ではありません。

  • ピークの場合は、メッセージキューをバッファーとして使用して、DBサーバーの負荷をより長い時間分散させることができます。
  • 一定の負荷の場合、DBサーバーは追いつくことができないため、メッセージキューだけでは不十分です。次に、分散データベースについて考える必要があります。

提案されたソリューション

直観的には、どちらの場合でも、Kafkaベースのイベントストリームに行きます。

  • すべてのイベントは、カフカのトピックに関して体系的に公開されています
  • コンシューマーはイベントをサブスクライブし、データベースに保存します。
  • クエリプロセッサは、クライアントからの要求を処理し、DBをクエリします。

これは、すべてのレベルで非常にスケーラブルです。

  • DBサーバーがボトルネックの場合は、複数のコンシューマを追加するだけです。それぞれがトピックをサブスクライブし、異なるDBサーバーに書き込むことができます。ただし、DBサーバー間で分散がランダムに発生する場合、クエリプロセッサはDBサーバーが複数のDBサーバーを取得し、クエリする必要があると予測することはできません。これは、クエリ側の新しいボトルネックにつながる可能性があります。
  • したがって、イベントストリームをいくつかのトピックに整理することにより、DB分散スキームを予測できます(たとえば、キーまたはプロパティのグループを使用して、予測可能なロジックに従ってDBをパーティション分割します)。
  • 1つのメッセージサーバーでは、増大する大量の入力イベントを処理するのに十分ではない場合、kafkaパーティションを追加してkafkaトピックを複数の物理サーバーに分散できます。

DBにまだ書き込まれていないイベントをクライアントに提供する

クライアントは、まだパイプ内にあり、まだDBに書き込まれていない情報にもアクセスできるようにする必要があります。これはもう少し繊細です。

オプション1:データベースクエリを補完するためにキャッシュを使用する

私は詳細に分析していませんが、頭に浮かぶ最初のアイデアは、クエリプロセッサをカフカトピックの消費者にするが、別のカフカ消費者グループにすることです。要求プロセッサは、DBライターが受信するすべてのメッセージを受信しますが、独立しています。その後、それらをローカルキャッシュに保持できます。その後、クエリはDB +キャッシュで実行されます(+重複の排除)。

設計は次のようになります。

ここに画像の説明を入力してください

このクエリレイヤーのスケーラビリティは、クエリプロセッサ(それぞれ独自のコンシューマグループ内)を追加することで実現できます。

オプション2:デュアルAPIを設計する

IMHOのより良いアプローチは、デュアルAPIを提供することです(個別のコンシューマグループのメカニズムを使用します)。

  • DBのイベントにアクセスしたり、分析を行ったりするためのクエリAPI
  • トピックから直接メッセージを直接転送するストリーミングAPI

利点は、何が面白いかをクライアントに決定させることです。これにより、クライアントが新しい着信イベントのみに関心がある場合に、DBデータをキャッシュされたばかりのデータと体系的にマージすることを回避できます。新しいイベントとアーカイブされたイベントの微妙なマージが本当に必要な場合、クライアントはそれを整理する必要があります。

バリアント

必要に応じてサーバーを再起動できるように、永続的なメッセージを含む非常に大容向け設計されているため、私はkafkaを提案しました。

RabbitMQで同様のアーキテクチャを構築できます。ただし、永続キューが必要な場合は、パフォーマンスが低下する可能性があります。また、私が知る限り、RabbitMQで複数のリーダー(ライター+キャッシュなど)が同じメッセージを並行して消費する唯一の方法は、キュー複製することです。したがって、スケーラビリティが高いほど、価格が高くなる可能性があります。


ステラ; どういう意味a distributed database (for example using a specialization of the server by group of keys)ですか?また、RabbitMQではなくKafkaを使用する理由は何ですか?一方を選択する特別な理由はありますか?
ニックキリアキデス

@NicholasKyriakidesありがとう!1)いくつかの独立したデータベースサーバーを考えていましたが、コマンドを効果的にディスパッチするために使用できる明確なパーティションスキーム(キー、地理など)がありました。2)直観的には、Kafkaはサーバーを再起動する必要がある永続メッセージで非常に高いスループットを実現するように設計されているためでしょうか?私は確かのRabbitMQは、分散シナリオの柔軟なようで、そして永続的なキューがあることないんだけど、パフォーマンスが低下する
クリストフ

1)だから、これは私のUse multiple databases考えにかなり似ているが、あなたは私がランダムに(またはラウンドロビンで)各データベースにメッセージを分配するべきではないと言っている。正しい?
ニックキリアキデス

はい。私が最初に考えたのは、クエリ(つまり、ほとんどの場合、両方の複数のDBのクエリ)の処理負荷が増加する可能性があるためです。分散DBエンジン(egIgnite?)も検討できます。しかし、十分な情報に基づいた選択を行うには、DBの使用パターン(dbの他の要素、クエリの頻度、クエリの種類、個々のイベントを超えるトランザクションの制約など)を十分に理解する必要があります。
クリストフ

3
kafkaは非常に高いスループットを実現できますが、おそらくほとんどの人のニーズを超えていると言いたいだけです。kafkaとそのAPIを扱うことは私たちにとって大きな間違いであることがわかりました。RabbitMQは前かがみではなく、MQに期待するインターフェイスがあります
imel96

11

私の推測では、拒否したアプローチをより慎重に検討する必要があると思います

  • サーバーのイベントをキューに登録します

私の提案はLMAXアーキテクチャに関して発表された様々な記事を読み始めることです。彼らはユースケースに合わせて大量のバッチ処理を行うことができ、トレードオフをより自分のものに見せることができるかもしれません。

また、読み取りを邪魔にならないかどうかを確認したい場合があります。理想的には、書き込みとは無関係に読み取りをスケーリングできるようにしたいと考えています。これは、CQRS(コマンドクエリの責任分離)を調べることを意味する場合があります。

イベントがエンキューされている間にサーバーを再起動すると、エンキューされたイベントが失われます。

分散システムでは、メッセージが失われることをかなり確信で​​きると思います。シーケンスバリアについて慎重に判断することで、その影響の一部を軽減できる場合があります(たとえば、イベントがシステムの外部で共有される前に永続ストレージへの書き込みが行われるようにします)。

  • 複数のデータベースを使用し、それぞれが中央のDBコーディネーターサーバーでメッセージの一部を保存して、それらを管理します

たぶん-データを分割する自然な場所があるかどうかを確認するために、あなたのビジネスの境界線を見る可能性が高いでしょう。

データの損失が許容可能なトレードオフになる場合がありますか?

まあ、私はあるかもしれないと思うが、それは私が行っていた場所ではない。重要なのは、メッセージの損失が発生した場合でも、設計が進行するために必要な堅牢性を設計に組み込む必要があるということです。

これがよくあるのは、通知を伴うプルベースのモデルです。プロバイダーは、メッセージを順序付けられた永続ストアに書き込みます。消費者はストアからメッセージを引き出し、独自の最高水準点を追跡します。プッシュ通知は待ち時間を短縮するデバイスとして使用されますが、通知が失われた場合でも、消費者が定期的なスケジュールでプルしているため(最終的に)メッセージがフェッチされます(通知が受信された場合、プルがより早く発生するという違いがあります) )。

Udi Dahanによる分散トランザクションのない信頼性のあるメッセージング(Andyによって既に参照されています)およびGreg YoungによるPolyglot Dataを参照してください。


In a distributed system, I think you can be pretty confident that messages are going to get lost。本当に?データの損失が許容可能なトレードオフになる場合がありますか?私は、データの損失=失敗という印象を受けていました。
ニックキリアキデス

1
@NicholasKyriakides、通常は受け入れられないため、OPはイベントを発行する前に耐久性のあるストアに書き込む可能性を提案しました。チェックこの記事このビデオ、彼はより詳細に問題に対処するのUdi漢によると。
アンディ

6

私が正しく理解している場合、現在のフローは次のとおりです。

  1. 受信およびイベント(HTTPを介して想定していますか?)
  2. プールからの接続を要求します。
  3. DBにイベントを挿入します
  4. プールへの接続を解放します。

そうだとすれば、デザインの最初の変更は、イベントを処理するたびにプールへの接続を返すコードの処理を停止することだと思います。代わりに、DB接続の数で1対1の挿入スレッド/プロセスのプールを作成します。これらはそれぞれ専用のDB接続を保持します。

ある種の並行キューを使用して、これらのスレッドに並行キューからメッセージをプルさせて挿入させます。理論的には、接続をプールに返す必要や新しい接続を要求する必要はありませんが、接続が悪くなった場合の処理​​を組み込む必要があります。スレッド/プロセスを強制終了して新しいスレッド/プロセスを開始するのが最も簡単な場合があります。

これにより、接続プールのオーバーヘッドが効果的に排除されます。もちろん、各接続で1秒あたり少なくとも1000 /接続イベントをプッシュできる必要があります。同じテーブルで500の接続を使用すると、DBで競合が発生する可能性があるため、異なる数の接続を試してみるとよいかもしれませんが、それはまったく異なる質問です。考慮すべきもう1つのことは、バッチ挿入の使用です。つまり、各スレッドは多数のメッセージをプルし、それらを一度にプッシュします。また、同じ行を更新しようとする複数の接続を避けてください。


5

仮定

あなたが説明する負荷は一定であると仮定します。それは解決するのがより難しいシナリオだからです。

また、Webアプリケーションプロセスの外部で、トリガーされた長時間実行されるワークロードを実行する何らかの方法があると仮定します。

解決

ボトルネック(プロセスとPostgresデータベース間の遅延)を正しく特定したと仮定すると、それが解決すべき主要な問題になります。ソリューションは、イベントを受信した後、できるだけ早くイベントを読みたい他のクライアントとの一貫性の制約を考慮する必要があります。

レイテンシの問題を解決するには、保存するイベントごとに発生するレイテンシの量を最小限に抑える方法で作業する必要があります。これは、ハードウェアを変更したくない、または変更できない場合に達成する必要がある重要なことです。PaaSサービスを利用しており、ハードウェアやネットワークを制御できない場合、イベントごとの遅延を減らす唯一の方法は、イベントの何らかのバッチ書き込みを使用することです。

イベントのキューをローカルに保存する必要があります。このキューは、指定されたサイズに達するか、一定の時間が経過すると、フラッシュされて定期的にデータベースに書き込まれます。プロセスは、このキューを監視して、ストアへのフラッシュをトリガーする必要があります。選択した言語で定期的にフラッシュされる並行キューを管理する方法については、多くの例があるはずです-これ、人気のあるSerilogロギングライブラリの定期バッチシンクのC#の例です。

このSOの答えは、Postgresでデータをフラッシュする最速の方法を説明しています-ただし、バッチ処理ではディスクにキューを保存する必要があります。

制約

別の答えはすでにCQRSに言及しており、それが制約を解決する正しいアプローチです。各イベントが処理されるたびに読み取りモデルをハイドレートします。メディエーターパターンは、イベントをカプセル化し、インプロセスの複数のハンドラーに配布するのに役立ちます。したがって、1つのハンドラーが、クライアントが照会できるメモリ内の読み取りモデルにイベントを追加し、別のハンドラーが最終的なバッチ書き込みのためにイベントをキューに入れることができます。

CQRSの主な利点は、概念的な読み取りモデルと書き込みモデルを分離することです。これは、あるモデルに書き込み、別のまったく異なるモデルから読み取るという素晴らしい方法です。CQRSからスケーラビリティの利点を得るには、通常、各モデルがその使用パターンに最適な方法で個別に保存されるようにします。この場合、トランザクションのデータベースを使用してデータを書き込む一方で、読み取りを高速かつ一貫性のあるものにするために、Redisキャッシュや単純なインメモリなどの集計読み取りモデルを使用できます。


3

DB接続プールが処理できるよりも速くイベントが入ります

各プロセスで1つのデータベース接続が必要な場合、これは問題です。各ワーカーが1つのデータベース接続のみを必要とし、各ワーカーが複数のイベントを処理できるワーカーのプールがあるように、システムを設計する必要があります。

メッセージキューはその設計で使用できます。イベントをメッセージキューにプッシュし、ワーカー(コンシューマ)がキューからのメッセージを処理するメッセージプロデューサーが必要です。

他のクライアントがイベントを同時に読み取りたい場合があります

この制約は、イベントが処理なしでデータベースに保存されている場合にのみ可能です(生のイベント)。イベントがデータベースに格納される前に処理される場合、イベントを取得する唯一の方法はデータベースからです。

クライアントが生のイベントを照会するだけの場合は、Elastic Searchなどの検索エンジンを使用することをお勧めします。クエリ/検索APIも無料で入手できます。

データベースに保存する前にイベントをクエリすることが重要であると考えると、Elastic Searchのようなシンプルなソリューションが機能するはずです。基本的にはすべてのイベントをその中に保存し、データベースにコピーして同じデータを複製しないでください。

Elastic Searchのスケーリングは簡単ですが、基本的な構成でも非常に高いパフォーマンスを発揮します。

処理が必要な場合、プロセスはESからイベントを取得し、データベースに処理して保存できます。この処理に必要なパフォーマンスレベルはわかりませんが、ESからのイベントのクエリとは完全に分離されます。とにかく接続の問題はないはずです。固定数のワーカーを持ち、それぞれが1つのデータベース接続を持つことができるからです。


2

データベースに適切なスキーマとストレージエンジンがあれば、1秒あたり1kまたは2kイベント(5KB)はそれほど多くありません。@eddyceが示唆するように、1つ以上のスレーブを持つマスターは、読み取りクエリを書き込みのコミットから分離できます。より少ないDB接続を使用すると、全体的なスループットが向上します。

他のクライアントがイベントを同時に読み取りたい場合があります

これらの要求では、読み取りスレーブへのレプリケーションラグがあるため、マスターデータベースから読み取る必要があります。

非常に大量の書き込みを行うために、(Percona)MySQLとTokuDBエンジンを使用しました。書き込みロードに適したLSMtreesに基づくMyRocksエンジンもあります。これらのエンジンとおそらくPostgreSQLの両方について、書き込み容量を劇的に増やすことができるトランザクションの分離とコミットの同期動作の設定があります。以前は、dbクライアントにコミットされたと報告された最大1秒の損失データを受け入れました。他のケースでは、損失を避けるためにバッテリーでバックアップされたSSDがありました。

MySQLフレーバーのAmazon RDS Auroraは、ゼロコストレプリケーション(スレーブがマスターとファイルシステムを共有するのと同様)で書き込みスループットが6倍高いと主張されています。Aurora PostgreSQLフレーバーには、異なる高度なレプリケーションメカニズムもあります。


TBH十分なハードウェア上で適切に管理されたデータベースは、この負荷に対処できるはずです。OPの問題は、データベースのパフォーマンスではなく、接続の遅延のようです。私の推測では、PaaSプロバイダーとしてのHerokuは、異なるAWSリージョンでPostgresインスタンスを販売しています。
アモン

1

私はherokuをまとめて削除します。つまり、集中型アプローチを削除します。最大プール接続をピークにする複数の書き込みは、dbクラスターが発明された主な理由の1つであり、主に書き込みをロードしません。クラスター内の他のデータベースによって実行できる読み取り要求を含むデータベース、さらにマスタースレーブトポロジを試してみます-他の誰かが既に述べたように、独自のデータベースをインストールすると、全体を調整できますクエリ伝播時間が正しく処理されることを確認するシステム。

幸運を

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.