最近、標準的なレシピを使用して方法を説明しました
#1. セクショニング
整理する方法と理由についての記事
「過ぎ去った日々のこと…」
当初、他の MVP と同様に、私たちのプロジェクトはかなり軽い負荷の下で開始されました。監視は XNUMX 台の最も重要なサーバーに対してのみ実行され、すべてのテーブルは比較的コンパクトでした...しかし時間が経つにつれて、監視されるホストの数は増加しました。などなど、そしてもう一度、次のいずれかを使って何かをしようとしました テーブルのサイズは 1.5TB, このまま生活を続けることは可能ですが、とても不便であることに気づきました。
当時はほぼ壮大な時代で、さまざまなバージョンの PostgreSQL 9.x が関連していたので、すべてのパーティショニングを「手動」で行う必要がありました。 テーブルの継承とトリガー 動的ルーティング EXECUTE
.
結果として得られたソリューションは、すべてのテーブルに変換できるほど汎用性があることが判明しました。
- 空の「ヘッダー」親テーブルが宣言されました。 必要なインデックスとトリガー.
- クライアントの視点からのレコードは「ルート」テーブルに作成され、内部的には ルーティングトリガー
BEFORE INSERT
レコードは必要なセクションに「物理的に」挿入されました。 そのようなものがまだ存在しない場合は、例外をキャッチしました... - …を使用して
親テーブルのテンプレートに基づいて作成されました 希望日限定のセクションデータを取得するときに、そのデータ内でのみ読み取りが実行されるようにします。CREATE TABLE ... (LIKE ... INCLUDING ...)
PG10: 最初の試み
しかし、継承によるパーティション化は、これまでアクティブな書き込みストリームや多数の子孫パーティションの操作にはあまり適していませんでした。 たとえば、必要なセクションを選択するためのアルゴリズムには次のような機能があったことを思い出してください。 二次複雑度、100 以上のセクションで動作するということは、あなた自身もその方法を理解しています...
PG10 では、この状況はサポートを実装することで大幅に最適化されました。
マニュアルを調べてみると、このバージョンのネイティブにパーティション化されたテーブルは次のとおりであることがわかりました。
- インデックスの説明はサポートされていません
- トリガーをサポートしていません
- 誰かの「子孫」になることはできません
- サポートしていません
INSERT ... ON CONFLICT
- セクションを自動的に生成できません
熊手で額に痛烈な打撃を受けた私たちは、アプリケーションを変更せずにそれを行うのは不可能であることに気づき、さらなる研究をXNUMXか月間延期しました。
PG10: セカンドチャンス
そこで、私たちは発生した問題を XNUMX つずつ解決し始めました。
- なぜならトリガーと
ON CONFLICT
まだあちこちでそれらが必要であることがわかったので、それらを解決するための中間段階を作成しました。 プロキシテーブル. - 「ルーティング」を廃止した トリガー内 - つまり、から
EXECUTE
. - 別々に取り出してくれました すべてのインデックスを含むテンプレート テーブルしたがって、それらはプロキシテーブルにも存在しません。
最後に、これまでの作業を経て、メイン テーブルをネイティブにパーティション分割しました。 新しいセクションの作成は依然としてアプリケーションの良心に任されています。
「のこぎり」辞典
他の分析システムと同様に、 「事実」と「カット」 (辞書)。 私たちの場合、この立場で彼らは次のように行動しました。
「事実」はすでに長い間日ごとにセクション化されていたため、古いセクションは冷静に削除しましたが、気にすることはありませんでした(ログ!)。 しかし、辞書に問題がありました...
たくさんあったというわけではありませんが、およそ 100TBの「事実」から2.5TBの辞書ができた。 このようなテーブルからは何も削除できず、適切な時間内に圧縮することもできず、テーブルへの書き込みは徐々に遅くなっていきました。
辞書のように...その中で、各エントリは XNUMX 回だけ表示される必要があります...これは正しいですが、!. 毎日別々の辞書! はい、これによりある程度の冗長性がもたらされますが、次のことが可能になります。
- 書き込み/読み取りが速くなります セクションサイズが小さいため
- メモリ消費量が少なくなる よりコンパクトなインデックスを使用することで
- 保存するデータを少なくする 古いものをすぐに削除できるため
複雑な対策全体の結果として CPU 負荷が最大 30%、ディスク負荷が最大 50% 減少:
同時に、負荷を軽減しながら、まったく同じ内容をデータベースに書き込み続けました。
#2. データベースの進化とリファクタリング
それで私たちは自分たちが持っているものに落ち着きました 毎日には独自のセクションがあります データ付き。 実は、 CHECK (dt = '2018-10-12'::date)
— パーティション化キーと、レコードが特定のセクションに分類される条件があります。
当社のサービスのすべてのレポートは特定の日付のコンテキストで構築されているため、「非パーティション化時」以降のレポートのインデックスはすべてのタイプです。 (サーバ、 日付、計画テンプレート), (サーバ、 日付、計画ノード), (日付、エラークラス、サーバー)、...
しかし今では彼らはあらゆるセクションに住んでいます あなたのコピー それぞれのインデックス...そして各セクション内 日付は定数です...今、私たちはそのようなインデックスのそれぞれにいることがわかります 定数を入力するだけです フィールドの XNUMX つとして使用すると、そのボリュームと検索時間の両方が増加しますが、結果は得られません。 彼らは熊手を放っておいてしまいました、おっと...
最適化の方向性は明らか - シンプル すべてのインデックスから日付フィールドを削除します パーティション化されたテーブル上。 ボリュームを考慮すると、ゲインは約 1TB/週!
ここで、このテラバイトは依然として何らかの方法で記録される必要があることに注意してください。 つまり、私たちも ディスクの負荷が軽減されるはずです! この写真は、私たちが XNUMX 週間かけて行った掃除から得られた効果をはっきりと示しています。
#3. ピーク負荷を「分散」する
ロードされたシステムの大きな問題の XNUMX つは、 冗長同期 それを必要としない一部の操作。 「気づかなかったから」ということもあれば、「そっちのほうが楽だった」ということもありますが、遅かれ早かれそれを取り除かなければなりません。
前の画像を拡大して、ディスクがあることを確認してみましょう。 二重振幅の負荷の下で「ポンプ」します 隣接するサンプル間では、このような数の操作では明らかに「統計的に」発生するはずがありません。
これは非常に簡単に達成できます。 すでにモニタリングを開始しています 約1000のサーバー、それぞれが別個の論理スレッドによって処理され、各スレッドは次のように、データベースに送信される蓄積された情報を特定の頻度でリセットします。
setInterval(sendToDB, interval)
ここでの問題は、まさに次のような事実にあります。 すべてのスレッドがほぼ同時に開始されます、そのため、送信時間はほとんどの場合「要点まで」一致します。 おっと #2...
幸いなことに、これは非常に簡単に修正できます。 「ランダムな」助走を追加する 時間別:
setInterval(sendToDB, interval * (1 + 0.1 * (Math.random() - 0.5)))
#4. 必要なものをキャッシュします
XNUMX 番目の従来の高負荷の問題は、 キャッシュがありません 彼はどこにいる できた であるために。
たとえば、計画ノード (これらすべて) の観点から分析できるようにしました。 Seq Scan on users
)しかし、すぐにそれらはほとんどの場合同じであると考えます-彼らは忘れていました。
いいえ、もちろん、データベースには再び何も書き込まれません。これにより、トリガーが切断されます。 INSERT ... ON CONFLICT DO NOTHING
。 ただし、このデータは依然としてデータベースに到達するため、不要です 競合をチェックするための読み取り しなければなりません。 おっと #3...
キャッシュを有効にする前と後で、データベースに送信されるレコード数の違いは明らかです。
これに伴うストレージ負荷の低下は次のとおりです。
合計で
「XNUMX 日あたりテラバイト」というと、ちょっと怖く聞こえます。 すべてを正しくやれば、これはただのことです 2^40 バイト / 86400 秒 = ~12.5MB/秒デスクトップのIDEネジでもしっかり固定されました。 🙂
しかし真剣に考えれば、日中の負荷が XNUMX 倍の「スキュー」であっても、最新の SSD の機能を簡単に満たすことができます。
出所: habr.com