接続制限付きデータベースへの高頻度イベントの保存

私たちはサーバに入ってくる膨大な数のイベントに対処しなければならない状況を持っています。平均して毎秒約1000イベントです(ピークは最大2000)

問題

我々のシステムは Heroku でホストされており、比較的高価な Heroku Postgres DB 。接続プーリングを使用して、サーバーからDBに接続します。

イベントはDB接続プールが処理できるよりも早く到着します。

問題 we have is that events come faster than the connection pool can handle. By the time one connection has finished the network roundtrip from the server to the DB, so it can get released back to the pool, more than n additional events come in.

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

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

制約

他のクライアントは同時にイベントを読みたいと思うかもしれません

他のクライアントは、DBにまだ保存されていなくても、特定のキーを使用してすべてのイベントを読み取ることを継続的に要求します。

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

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

可能な解決策

サーバーにイベントをエンキューする

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

これは悪い考えです。

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

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

メッセージキュー( RabbitMQ のような)を使用することができると思います。そこにメッセージを送り続けます。もう一方の端には、DBにイベントを保存することだけを扱う別のサーバーがあります。

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

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

私達がしたもう一つの解決策は中央の "DBコーディネーター/ロードバランサー"で複数のデータベースを使うことです。イベントを受け取り次第 このコーディネーターは、メッセージを書き込むデータベースを1つ選択します。これにより、複数のHerokuデータベースを使用できるようになり、接続数の上限は500 xデータベースになります。

読み込みの問い合わせがあると、このコーディネータは各データベースに対して SELECT クエリを発行し、すべての結果をマージして読み込みを要求したクライアントに送り返します。

これは悪い考えです。

  • この考えは…ああ、オーバーエンジニアリングのようですね。同様に管理するのは悪夢かもしれません(バックアップなど)。構築と保守は複雑で、どうしても必要な場合を除き、 KISS 違反のように思えます。
  • 一貫性を犠牲にします。複数のDBにまたがってトランザクションを実行するのは、この考えに従うといけません。
12
100%のイベントがデータベースに正しく挿入されていることが絶対に必要ですか?そうであれば、サーバーの再起動時に現在どのように問題を処理していますか?
追加された 著者 Walfrat,
だからあなたは100%のアベイラビリティを望みますが、同期はしません。それから私の賭けは最初にローカルでイベントを永続化し(例:ファイル)そして定期的にファイルをエクスポートすることです(これは30秒ごとのロックを避けるためにtmpファイルローリングかもしれません)。そのようなシステムの基本は、あなたがすべてを同時に持つことができるということです(損失なし、インスタントプロセス、パフォーマンスの維持)。あなたはあなたがあなたが必要とするものを手に入れるためにあなたが落とすことができるものを知っている必要があります(例えば:同期、または本当の0%の損失)。しかし、これはあなたがそれらを修正したものではないかもしれないあなたのシステムの要求に依存します。
追加された 著者 Walfrat,
あなたは本当にこのレートがピークなのか平均的なのかを明確にする必要があります。ピーク時には、1日のイベント数はいくつですか。
追加された 著者 JimmyJames,
「問題の頻度の高いイベントをクライアントからより遅いペースで発信することで緊急事態を解決しましたが、その頻度の高いイベントを処理する必要がある場合に、このシナリオを処理する方法を知りたいのです。」これがどのように問題を解決するのか私にはわかりません。あなたがあなたが平均して扱うことができる以上のものを得ているならば、クライアントを遅くしないことは彼らが継続的に処理される必要があるイベントのより深いバックログを構築していることを意味しませんか?
追加された 著者 JimmyJames,
あなたのボトルネックはどこにありますか?接続プールについて言及していますが、これは並列処理にのみ影響し、挿入あたりの速度には影響しません。 500の接続がある場合2000QPSでは、各クエリが250ms以内に完了する場合はこれでうまくいきます。これは長い時間です。なぜ15ms以上なのでしょうか。また、PaaSを使用すると、データベースハードウェアの拡張やプライマリデータベースの負荷を軽減するためのリードレプリカの使用など、重要な最適化の機会を放棄することになります。展開があなたの最大の問題でない限り、Herokuは価値がありません。
追加された 著者 amon,
@NicholasKyriakides適切なハードウェアは、ミクロの最適化ではありません。これはデータベースを拡張するための主な方法です。 1つのデータセンター内のネットワーク遅延はここでは無視できます、<1ms。エンタープライズグレードのSSDへの書き込みも1ms未満です。 1000トランザクションの場合、最低1k IOPSが必要です。 RAID-0は役に立ちますが、ハードディスクは提供できません。有能なシステム管理者は、これらすべてを正しく設定できるはずです。それでもあなたは問題を見ます。ソフトウェアコンポーネントに大きなパフォーマンス上の問題がある(これはDBのために除外されています)か、あなたのPaaSは本当に本当に悪いです。クラウドはパフォーマンスを犠牲にしています。
追加された 著者 amon,
ネットワークを介して送信する前に単一の要求にいくつかのイベントをパックすることは選択肢ではありませんか?私は同様の問題を解決するために、各クライアントに1回のリクエストで特定の時間枠内に発生したすべてのイベントを「パック」させ、10〜15秒ごとに送信します。それが選択肢であるならば、私にpingをください、そして私は完全な答えでそれを広げるつもりです。
追加された 著者 T. Sar,
接続プールが問題であることをどの程度正確に確認しましたか? @amonは彼の計算では正しいです。 500回の接続で select null を発行してみてください。接続プールが問題にならないことに気付くでしょう。
追加された 著者 user26009,
nullの選択に問題がある場合は、おそらくそのとおりです。それはすべての時間が費やされているところでは面白いでしょうが。それほど遅いネットワークはありません。
追加された 著者 user26009,
@amonボトルネックは確かに接続プールです。クエリ自体に ANALYZE を実行しましたが、問題ありません。また、接続プールの仮説をテストするためのプロトタイプを作成し、これが本当に問題であることを確認しました。データベースとサーバー自体は異なるマシン上にあるため、待ち時間が長くなります。また、どうしても必要な場合を除き、Herokuをあきらめたくはありません。デプロイについて心配しないことは、大きなプラスです。
追加された 著者 Nicholas Kyriakides,
...このシナリオでは、今回は "スロットルによる回避策"を使用することはできますが、もうすぐ終わりにはなりません。
追加された 著者 Nicholas Kyriakides,
@JimmyJames がクライアントの動作を遅くしないということは、処理が必要なイベントのより多くのバックログを継続的に作成しているということです。この場合ではありません。クライアントがスロットルして、そのイベントを低いペースで送信しました。そのイベントでは、そのペースで送信されるデータを必要する必要はありませんでしたが、持っているといいでしょう。私たちが常にそれらを持っている必要があるイベントがあります。現時点では、ユーザー数が少ないため、必要なイベントでも同じ問題が発生しますが、すぐに十分に機能するようになります。私は現在の問題を正確に解決していません...
追加された 著者 Nicholas Kyriakides,
私たちはそれを処理しませんでした。一時的な回避策としてイベントが発生しているペースを遅くしただけです。また、はイベントの100%がデータベースに正しく挿入されていることが絶対に必要です。はいといいえ;クライアントがサーバーにイベントを送信した場合、私はそれが他のクライアントによってすぐにそして2、3年後に読めるようになることを保証したいです。すぐにデータベースに挿入する必要はありませんが、提案されたソリューションはすべてフォールトトレラントになるはずです。
追加された 著者 Nicholas Kyriakides,
@JimmyJames質問を編集しました、それは平均です。
追加された 著者 Nicholas Kyriakides,
@usr私のテストハーネスは500接続ではなく50接続で実行されました。 SELECT NULL を実行しましたが、まだ問題があります。また、クエリで ANALYZE を実行しましたが、それらの時間は問題ないようです。私の質問の概念はまだ成立していますが、より正確なデータで更新します。私はまた、ネットワークを介して送信されるクエリのサイズを追加するのを忘れていました。これはかなり大きいものです(平均5KBまで)。
追加された 著者 Nicholas Kyriakides,
そうは言っても、私は現在の問題を解決するのに役立つ可能性のある微小な最適化があることを理解しています。私の問題にスケーラブルなアーキテクチャの解決策があるかどうか疑問に思います。
追加された 著者 Nicholas Kyriakides,
一般的なガイドラインとして、私は言います:あなたが使っている技術の限界に達したら、あなたは他の技術への切り替えを始める必要があります。
追加された 著者 Dominique,

6 答え

私はあなたが拒絶したアプローチをもっと注意深く探る必要があると思います

  • サーバーにイベントをエンキューする

私の提案は、 LMAXアーキテクチャについて公開されているさまざまな記事を読み始めることです。彼らは彼らのユースケースのために大量のバッチ処理をすることに成功した、そしてそれはあなたのトレードオフを彼らのもののように見えるようにすることは可能かもしれない。

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

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

分散システムでは、メッセージが失われることにかなり自信があると思います。シーケンスの障壁について慎重に検討することで、その影響の一部を軽減できる可能性があります(たとえば、イベントがシステムの外部で共有される前に、永続ストレージへの書き込みが行われるようにする)。

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

たぶん - 私はあなたのビジネスの境界を調べて、データを断片化するための自然な場所があるかどうかを確かめることになるでしょう。

データを失うことが許容できるトレードオフである場合がありますか?

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

これがよく見えるのは、通知を伴うプルベースのモデルです。プロバイダはメッセージを順序付けされた永続ストアに書き込みます。消費者は店からメッセージを引き出し、それ自身の最高水準点を追跡します。プッシュ通知は待ち時間を短縮する装置として使用されますが、通知が紛失しても、コンシューマが定期的にスケジュールを進めているため、メッセージは(最終的には)まだ取得されます。 )

See Reliable Messaging Without Distributed Transactions, by Udi Dahan (already referenced by Andy) and Polyglot Data by Greg Young.

11
追加された
分散システムでは、メッセージが失われることにかなり自信があると思います。本当に?データを失うことが許容できるトレードオフである場合がありますか?私はデータを失うこと=失敗という印象を受けました。
追加された 著者 Nicholas Kyriakides,
@NicholasKyriakides、通常は受け入れられないため、OPはイベントを発行する前に耐久性のあるストアに書き込む可能性を提案しました。 この記事このビデオ(Udi Dahan著)。彼は問題の詳細を述べています。
追加された 著者 Andy,

入力ストリーム

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

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

提案された解決策

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

  • All events are systematically published on a kafka topic
  • A consumer would subscribe to the events and store them to the database.
  • A query processor will handle the requests from the clients and query the DB.

これはあらゆるレベルで非常にスケーラブルです。

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

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

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

オプション1:dbクエリを補完するためのキャッシュの使用

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

デザインは次のようになります。

enter image description here

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

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

より良いアプローチ私見は二重のAPIを提供することでしょう(別々の消費者グループのメカニズムを使います):

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

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

変種

非常に大容量向けに設計されているため、私はkafkaを提案しました必要に応じてサーバーを再起動できるように、持続メッセージ付きの/ a>

RabbitMQでも同様のアーキテクチャを構築できます。ただし、永続的なキューが必要な場合は、

パフォーマンスが低下する可能性があります。また、私の知る限りでは、RabbitMQを使って同じメッセージを複数のリーダー(例:writer + cache)で同時に消費する唯一の方法はキューを複製する。したがって、より高いスケーラビリティはより高い価格でもたらされる可能性があります。

8
追加された
@NicholasKyriakides私は「他のクライアントが DBにまだ保存されていない場合でも特定のキーを持つすべてのイベントを継続的に読み取ることを要求しています。 "DBクエリ(" all ")を作成し、着信イベント(ここでは入力から直接供給される"キャッシュ "で処理されます)と結合する必要があるため、doubleを排除します。 "all"を使用した場合、単に "all new"を意味するのであれば、単純化できます。キャッシュなし、マージなし、DBからの読み取り、または新しいイベントの転送のいずれかです。
追加された 著者 Christophe,
はい。私が最初に考えたのは、ランダムな配布に行かないことです。クエリの処理負荷が増大する可能性があるためです(つまり、ほとんどの場合、両方の複数のDBに対するクエリ)。あなたは分散DBエンジン(例:Ignite?)を検討することもできます。しかし、情報に基づいた選択をするには、DBの使用パターン(他に何があるのか​​、どのくらいの頻度で問い合わせるのか、どのような種類のクエリがあるのか​​など)
追加された 著者 Christophe,
@NicholasKyriakidesありがとう! 1)私は単純にいくつかの独立したデータベースサーバーを考えていましたが、コマンドを効果的にディスパッチするために使用できる明確な分割方式(キー、地理など)を使用しました。 2)直感的に、おそらくKafkaは非常に高スループットはサーバーを再起動する必要がありますか?) RabbitMQが分散型シナリオに対して柔軟性があること、そして永続キューがパフォーマンスを低下させる</>
追加された 著者 Christophe,
分散データベースとはどういう意味ですか(たとえば、キーのグループによるサーバーの特殊化を使用する場合)。また、なぜRabbitMQではなくKafkaなのでしょうか。どちらを選ぶのか、特別な理由はありますか。
追加された 著者 Nicholas Kyriakides,
1)だからこれは私の複数のデータベースを使うのアイデアとよく似ていますが、ランダムに(またはラウンドロビンで)メッセージを各データベースに配布するべきではないと言っています。右?
追加された 著者 Nicholas Kyriakides,
私は思っていますが、なぜローカルキャッシュが必要なのでしょうか。複数のデータベース/ライターを使用するという全体的なアイデアは、イベントが瞬時に保存され、バックログがほとんど発生しないようにすることです。 DBから直接読み取らないのはなぜですか。
追加された 著者 Nicholas Kyriakides,
まだDBに保存されていなくても。私がここで意味しているのは、まだ書き込まれていないイベントのバックログが常に存在することを受け入れるソリューションが選択された場合、read-clientsもバックログイベントを取得したいということです。マルチDBの考え方は、バックログがないこと(理論上)=保存されていないDBイベントがないこと=キャッシュが不要であることをほとんど意味します。
追加された 著者 Nicholas Kyriakides,
たとえkafkaが非常に高いスループットをもたらすことができるとしても、それはおそらくほとんどの人のニーズを超えています。私は、kafkaとそのAPIを扱うことは私たちにとって大きな間違いであることがわかりました。 RabbitMQは愚かではありません、そしてそれはあなたがMQから期待するだろうインターフェースを持っています
追加された 著者 Ankit,

私が正しく理解していれば、現在の流れは次のとおりです。

  1. 受信とイベント(HTTP経由で想定しますか)
  2. プールからの接続を要求します。
  3. イベントをDBに挿入する
  4. プールへの接続を解除します。

もしそうなら、私は設計への最初の変更はあなたの均等な取り扱いコードがあらゆるイベントでプールへの接続を返すことをやめることになると思います。代わりに、DB接続数と1対1の挿入スレッド/プロセスのプールを作成してください。これらはそれぞれ専用のDB接続を保持します。

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

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

6
追加された

仮定

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

また、私はあなたがあなたのWebアプリケーションプロセスの外で引き起こされた、長期に渡るワークロードを実行する何らかの方法があると仮定するつもりです。

溶液

Assuming that you have correctly identified your bottleneck - latency between your process and the Postgres database - that is the primary problem to solve for. The 溶液 needs to account for your consistency 拘束 with other clients wanting to read the events as soon as practicable after they are received.

待ち時間の問題を解決するには、イベントごとに保存される待ち時間を最小限に抑えるように作業する必要があります。 これは、ハードウェアを変更する意思がない、または変更できない場合に達成する必要がある重要なことです。 PaaSサービスを利用していてハードウェアやネットワークを制御できないことを考えると、イベントごとの待ち時間を減らす唯一の方法は、ある種のイベントの一括書き込みを行うことです。

一定のサイズに達するか、または経過時間の経過後に、定期的にフラッシュされてdbに書き込まれるイベントのキューをローカルに格納する必要があります。プロセスはストアへのフラッシュをトリガーするためにこのキューを監視する必要があります。選択した言語で定期的にフラッシュされる並行キューを管理する方法については、たくさんの例があります - これは、人気のあるSerilogロギングライブラリの定期バッチシンクからのC#の例です。This SO answer describes the fastest way to flush data in Postgres - although it would require your batching store the queue on disk, and there is likely a problem to be solved there when your disk disappears upon reboot in Heroku.

拘束

Another answer has already mentioned CQRS, and that is the correct approach to solve for the 拘束. You want to hydrate read models as each event is processed - a Mediator pattern can help encapsulate an event and distribute it to multiple handlers in-process. So one handler may add the event to your read model that is in-memory that clients can query, and another handler can be responsible for queuing the event for its eventual batched write.

CQRSの主な利点は、概念的な読み書きモデルを切り離すことです。これは、あるモデルに書き込みを行い、別のまったく異なるモデルから読み込むのには好都合です。 CQRSからスケーラビリティの恩恵を受けるには、通常、各モデルがその使用パターンに最適な方法で別々に格納されるようにします。この場合、データの書き込みにトランザクションデータベースを使用しながら、読み取りの高速性と一貫性を確保するために、集約読み取りモデル(たとえば、Redisキャッシュ、または単にメモリ内)を使用できます。

5
追加された

イベントはDB接続プールが処理できるよりも早く到着します

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

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

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

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

クライアントが生のイベントをクエリしたいだけの場合は、Elastic Searchなどの検索エンジンを使用することをお勧めします。あなたは無料でquery/search APIを手に入れるでしょう。

イベントをデータベースに保存する前にクエリすることが重要であると思われることを考えると、Elastic Searchのような単純な解決策でうまくいくはずです。あなたは基本的にただそれにすべてのイベントを保存し、それらをデータベースにコピーすることによって同じデータを複製することはしません。

Elastic Searchのスケーリングは簡単ですが、基本的な構成でも非常に高性能です。

処理が必要な場合、プロセスはESからイベントを取得し、それらを処理してデータベースに格納することができます。この処理からどの程度のパフォーマンスレベルが必要かはわかりませんが、ESからのイベントのクエリとはまったく別のものになります。固定数のワーカーとそれぞれ1つのデータベース接続を持つことができるので、とにかく接続の問題はありません。

3
追加された

herokuをまとめてドロップする、つまり、集中型のアプローチを削除することにします。最大プール接続をピークにする複数の書き込みが、dbクラスタが発明された主な理由の1つですさらに、他の誰かが述べたように、あなた自身のdbインストールが全体を調整することを可能にするであろう - クラスタ内の他のdbによって実行される読み込み要求を持つdb(s)クエリ伝播時間が正しく処理されるようにするためのシステム。

がんばろう

1
追加された