サーバーに着信する大量のイベントに対処する必要がある状況があり、平均で毎秒約1000イベントです(ピークは〜2000になる可能性があります)。
私たちのシステムは Herok でホストされており、比較的高価な Heroku Postgres DB を使用しているため、最大500のDB接続が可能です。接続プーリングを使用して、サーバーからDBに接続します。
私たちが抱えている問題は、接続プールが処理できるよりも速くイベントが発生することです。 1つの接続がサーバーからDBへのネットワークラウンドトリップを完了するまでに、解放されてプールに戻ることができるようになると、n
以上の追加のイベントが発生します。
最終的に、イベントは蓄積され、保存されるのを待っています。プールに使用可能な接続がないため、イベントがタイムアウトし、システム全体が動作しなくなります。
問題のある高周波イベントをクライアントから遅いペースで送信することで緊急事態を解決しましたが、その高周波イベントを処理する必要がある場合に、このシナリオを処理する方法を知りたいです。
他のクライアントは、まだDBに保存されていない場合でも、特定のキーを持つすべてのイベントの読み取りを継続的に要求します。
クライアントはGET api/v1/events?clientId=1
そして、それらのイベントがまだDBに保存されていない場合でも、クライアント1から送信されたすべてのイベントを取得します。
これに対処する方法に関する「教室」の例はありますか?
サーバー上のイベントをエンキューできます(キューの最大同時実行性は400なので、接続プールが不足しません)。
これは悪い考えです:
RabbitMQ ?などのメッセージキューを使用して、メッセージをそこに送り込み、もう一方の端に、DBへのイベントの保存のみを処理する別のサーバーがあると想定します。
メッセージキューがエンキューされたイベント(まだ保存されていない)のクエリを許可するかどうかわからないので、別のクライアントが別のクライアントのメッセージを読みたい場合は、DBから保存されたメッセージとキューから保留中のメッセージを取得できますそれらを連結して、読み取り要求クライアントに送り返すことができます。
もう1つの解決策は、中央に「DBコーディネーター/ロードバランサー」を備えた複数のデータベースを使用することです。イベントを受け取ると、このコーディネーターはメッセージを書き込むデータベースの1つを選択します。これにより、複数のHerokuデータベースを使用できるようになり、接続制限が500 xデータベースの数まで増加します。
読み取りクエリ時に、このコーディネーターは各データベースにSELECT
クエリを発行し、すべての結果をマージして、読み取りを要求したクライアントに送り返すことができます。
これは悪い考えです:
1000イベント/秒がピークを表しているのか、それが継続的な負荷であるのかは明らかではありません。
直感的には、どちらの場合も、Kafkaベースのイベントストリームを使用します。
これは、すべてのレベルで非常にスケーラブルです。
クライアントがまだパイプ内にあり、まだDBに書き込まれていない情報にもアクセスできるようにする必要があります。これはもう少しデリケートです。
私は詳細に分析していませんが、私の頭に浮かぶ最初のアイデアは、クエリプロセッサをkafkaトピックのコンシューマ)にすることですが、異なる- kafkaコンシューマグループ 。リクエストプロセッサは、次のすべてのメッセージを受信します。 DBライターは受信しますが、独立しています。ローカルキャッシュにそれらを保持できます。クエリはDB +キャッシュ(+重複の排除)で実行されます。
デザインは次のようになります。
このクエリレイヤーのスケーラビリティは、クエリプロセッサを追加することで実現できます(それぞれ独自のコンシューマーグループにあります)。
IMHOのより良いアプローチは、デュアルAPIを提供することです(個別のコンシューマーグループのメカニズムを使用)。
利点は、何が面白いかをクライアントに決定させることです。これにより、クライアントが新しい着信イベントのみに関心がある場合に、DBデータを新しくキャッシュされたデータと体系的にマージすることを回避できます。新鮮なイベントとアーカイブされたイベントの微妙なマージが本当に必要な場合、クライアントはそれを整理する必要があります。
私はkafkaそれは 非常に大量 用に設計されているため、永続的なメッセージを使用して、必要に応じてサーバーを再起動できるように提案しました。
RabbitMQで同様のアーキテクチャを構築できます。ただし、永続キューが必要な場合は パフォーマンスが低下する可能性があります 。また、私が知る限り、RabbitMQを使用して複数のリーダー(ライター+キャッシュなど)が同じメッセージを並行して消費する唯一の方法は、 キューのクローン を使用することです。したがって、スケーラビリティーが高くなると、価格も高くなる可能性があります。
私の推測では、拒否したアプローチをより慎重に検討する必要があると思います
私の提案は、 LMAXアーキテクチャ について公開されたさまざまな記事を読み始めることです。彼らは何とかユースケースに応じて大量のバッチ処理を行うことができ、トレードオフをより似たものにすることが可能かもしれません。
また、読み取りを邪魔にならないようにできるかどうかを確認することもできます。理想的には、書き込みとは無関係に読み取りをスケーリングできるようにする必要があります。これは、CQRS(コマンドクエリの責任の分離)を調べることを意味する場合があります。
イベントがエンキューされている間にサーバーが再起動すると、エンキューされたイベントが失われます。
分散システムでは、メッセージが失われることにかなり自信があると思います。シーケンスの障壁を慎重に検討することで、その影響の一部を軽減できる場合があります(たとえば、イベントがシステムの外部で共有される前に、永続ストレージへの書き込みが行われるようにする)。
多分-私はあなたのビジネスの境界を見て、データをシャーディングする自然な場所があるかどうかを確認する可能性が高いでしょう。
データの損失が許容できるトレードオフの場合がありますか?
まあ、あるかもしれないと思いますが、それは私が行っていた場所ではありません。重要なのは、メッセージの損失に直面した場合に設計を進めるために必要な堅牢性を設計に組み込む必要があるということです。
これがよく見えるのは、通知を伴うプルベースのモデルです。プロバイダーは、順序付けされた永続ストアにメッセージを書き込みます。消費者はストアからメッセージをプルし、独自の最高水準点を追跡します。プッシュ通知はレイテンシ削減デバイスとして使用されます-ただし、通知が失われた場合、コンシューマーが定期的なスケジュールでプルしているため、メッセージは引き続き(最終的に)フェッチされます(違いは、通知を受信するとプルがより早く発生することです) )。
Udi Dahanによる分散トランザクションなしの信頼性のあるメッセージング(すでに Andy によって参照されています)および Polyglot Data によるGreg Youngを参照してください。
私が正しく理解している場合、現在のフローは次のとおりです。
もしそうなら、私はデザインの最初の変更は、すべてのイベントでプールへの接続を返すコードを処理することを停止することだと思います。代わりに、DB接続の数と1対1の挿入スレッド/プロセスのプールを作成します。これらはそれぞれ専用のDB接続を保持します。
ある種の並行キューを使用して、これらのスレッドに並行キューからメッセージをプルして挿入させます。理論的には、接続をプールに戻したり、新しい接続を要求したりする必要はありませんが、接続が不良になった場合に備えて、処理を組み込む必要がある場合があります。スレッド/プロセスを強制終了して新しいものを開始するのが最も簡単な場合があります。
これにより、接続プールのオーバーヘッドが効果的に排除されます。もちろん、各接続で毎秒少なくとも1000 /接続イベントのプッシュを実行できる必要があります。同じテーブルで500の接続が機能していると、DBで競合が発生する可能性があるため、異なる数の接続を試すこともできますが、それはまったく別の問題です。考慮すべきもう1つのことは、バッチ挿入の使用です。つまり、各スレッドが多数のメッセージをプルし、それらを一度にすべてプッシュします。また、同じ行を更新しようとする複数の接続を避けてください。
あなたが説明する負荷は一定であると仮定します。それは解決するのがより難しいシナリオだからです。
また、Webアプリケーションプロセスの外部で、トリガーされた長時間実行されるワークロードを実行する方法があると仮定します。
ボトルネック(プロセスとPostgresデータベース間の待ち時間)を正しく特定したと仮定すると、これは解決すべき主要な問題です。ソリューションは、イベントを受け取った後、できるだけ早くイベントを読み取りたい他のクライアントとの整合性の制約を考慮する必要があります。
レイテンシの問題を解決するには、保存するイベントごとに発生するレイテンシの量を最小限に抑える方法で作業する必要があります。 これは、ハードウェアを変更したくない、または変更できない場合に達成する必要がある重要なことです。 PaaSサービスを利用していて、ハードウェアやネットワークを制御できない場合、イベントごとのレイテンシを削減する唯一の方法は、ある種のイベントの一括書き込みを使用することです。
イベントのキューをローカルに保存する必要があります。イベントのキューは、指定されたサイズに達した後、または一定の時間が経過した後、定期的にフラッシュされ、データベースに書き込まれます。プロセスは、ストアへのフラッシュをトリガーするためにこのキューを監視する必要があります。選択した言語で定期的にフラッシュされる同時キューを管理する方法については、人気のあるSerilogロギングライブラリの定期的なバッチシンクからの例がたくさんあるはずです これはC#の例です 。
このSO回答 は、Postgresでデータをフラッシュする最も速い方法を説明しています-バッチ処理ではキューをディスクに保存する必要があり、解決すべき問題がある可能性がありますHerokuの再起動時にディスクが消えたときにそこに。
別の答えはすでに言及しました[〜#〜] cqrs [〜#〜]、そしてそれは制約を解決する正しいアプローチです。各イベントが処理されるときに読み取りモデルをハイドレートしたい- メディエーターパターン は、イベントをカプセル化し、インプロセスの複数のハンドラーに配布するのに役立ちます。したがって、1つのハンドラーが、クライアントがクエリできるメモリ内の読み取りモデルにイベントを追加し、別のハンドラーが、最終的なバッチ書き込みのイベントをキューに入れることができます。
CQRSの主な利点は、概念的な読み取りモデルと書き込みモデルを分離することです。これは、1つのモデルに書き込み、別の完全に異なるモデルから読み取るというおかしな方法です。 CQRSからスケーラビリティのメリットを得るには、通常、各モデルがその使用パターンに最適な方法で個別に保存されるようにする必要があります。この場合、データの書き込みにトランザクションデータベースを使用しながら、読み取りを高速で一貫性のあるものにするために、集約読み取りモデル(Redisキャッシュ、または単にメモリ内など)を使用できます。
イベントはDB接続プールが処理できるよりも速く入ってくる
各プロセスが1つのデータベース接続を必要とする場合、これは問題です。各ワーカーが1つのデータベース接続のみを必要とし、各ワーカーが複数のイベントを処理できるワーカーのプールができるようにシステムを設計する必要があります。
メッセージキューはその設計で使用できます。イベントをメッセージキューにプッシュするメッセージプロデューサーと、ワーカー(コンシューマー)がキューからのメッセージを処理する必要があります。
他のクライアントはイベントを同時に読み取る必要があるかもしれません
この制約は、処理なしでデータベースに保存されたイベント(生イベント)の場合にのみ可能です。イベントがデータベースに保存される前に処理される場合、イベントを取得する唯一の方法はデータベースからです。
クライアントが生のイベントをクエリするだけの場合は、Elastic Searchなどの検索エンジンを使用することをお勧めします。クエリ/検索APIも無料で入手できます。
イベントがデータベースに保存される前にクエリを実行することが重要であると考えると、Elastic Searchのような単純なソリューションが機能するはずです。基本的にはすべてのイベントをそこに格納するだけで、データベースにコピーして同じデータを複製することはありません。
Elastic Searchのスケーリングは簡単ですが、基本的な構成でもパフォーマンスは非常に高くなります。
処理が必要な場合、プロセスはESからイベントを取得し、処理してデータベースに格納できます。この処理で必要なパフォーマンスレベルはわかりませんが、ESからのイベントのクエリとは完全に別のものです。固定数のワーカーとそれぞれが1つのデータベース接続を持つことができるので、とにかく接続の問題はありません。
1秒あたり1kまたは2kイベント(5KB)は、適切なスキーマとストレージエンジンがあれば、データベースにとってそれほど多くありません。 @eddyceで提案されているように、1つ以上のスレーブを持つマスターは、読み取りクエリを書き込みのコミットから分離できます。より少ないDB接続を使用すると、全体的なスループットが向上します。
他のクライアントはイベントを同時に読み取る必要があるかもしれません
これらのリクエストについては、読み取りスレーブへのレプリケーションラグがあるため、マスターデータベースからも読み取る必要があります。
私は(Percona)MySQLとTokuDBエンジンを使用して、非常に大量の書き込みを行いました。 LSMtreesに基づくMyRocksエンジンもあり、書き込み負荷に適しています。これらのエンジンとPostgreSQLの両方について、トランザクションの分離とコミット同期動作の設定があり、書き込み容量を劇的に増やすことができます。以前は、コミットされたものとしてdbクライアントに報告された1秒までの失われたデータを受け入れていました。他のケースでは、損失を避けるためにバッテリーでバックアップされたSSDがありました。
MySQLフレーバーのAmazon RDS Auroraは、ゼロコストのレプリケーションで6倍の書き込みスループットがあると主張されています(マスターとファイルシステムを共有するスレーブに似ています)。 Aurora PostgreSQLフレーバーには、異なる高度なレプリケーションメカニズムもあります。
Herokuをまとめてドロップします。つまり、集中型アプローチをドロップします。最大プール接続をピークにする複数の書き込みが、dbクラスターが発明された主な理由の1つであり、主に書き込みをロードしません。クラスター内の他のdbによって実行できる読み取り要求を持つdb(s)、さらにマスタースレーブトポロジで試してみます-他の誰かがすでに述べたように、独自のdbインストールを使用すると、全体を調整できますクエリの伝播時間が正しく処理されるようにするシステム。
幸運を