sql >> データベース >  >> RDS >> PostgreSQL

PostgreSQLベースのアプリケーションパフォーマンス:レイテンシと隠れた遅延

    ゴールドフィールズパイプライン、SeanMac(Wikimedia Commons)による

    PostgreSQLベースのアプリケーションのパフォーマンスを最適化しようとしている場合は、おそらく通常のツールに焦点を当てています: EXPLAIN(BUFFERS、ANALYZE) pg_stat_statements auto_explain log_statement_min_duration 、など。

    log_lock_waitsとのロックの競合を調査している可能性があります 、チェックポイントのパフォーマンスの監視なども行います。

    しかし、ネットワーク遅延について考えましたか ?ゲーマーはネットワーク遅延について知っていますが、それがアプリケーションサーバーにとって重要だと思いましたか?

    レイテンシが重要

    一般的なクライアント/サーバーのラウンドトリップネットワークの待ち時間は、0.01ミリ秒(ローカルホスト)から約0.5ミリ秒のスイッチドネットワーク、5ミリ秒のWiFi、20ミリ秒のADSL、300ミリ秒の大陸間ルーティング、さらには衛星やWWANリンクなどの範囲です。 。

    ささいなSELECT サーバー側の実行には0.1ms程度かかる場合があります。些細なINSERT 0.5msかかる場合があります。

    アプリケーションがクエリを実行するたびに、サーバーが成功/失敗、場合によっては結果セット、クエリメタデータなどで応答するのを待つ必要があります。これにより、少なくとも1つのネットワークラウンドトリップ遅延が発生します。

    小規模で単純なクエリを使用している場合、データベースがアプリケーションと同じホスト上にない場合、ネットワークの待ち時間はクエリの実行時間に比べて大きくなる可能性があります。

    多くのアプリケーション、特にORMは、ロットを実行する傾向があります。 非常に単純なクエリの。たとえば、Hibernateアプリが遅延フェッチされた @OneToManyを使用してエンティティをフェッチしている場合 1000の子アイテムとの関係では、n + 1選択の問題のおかげで、おそらく1001のクエリが実行されます。つまり、おそらく待機するだけでネットワークの往復待ち時間の1000倍を費やしていることになります。 。 左結合フェッチができます それを避けるために…しかし、その後、結合で親エンティティを1000回転送し、重複排除する必要があります。

    同様に、ORMからデータベースにデータを入力する場合は、おそらく数十万の些細な INSERTを実行していることになります。 s…そして、サーバーが問題ないことを確認するのを一人一人待っています。

    クエリの実行時間に焦点を合わせてそれを最適化するのは簡単ですが、簡単な INSERT INTO ... VALUES ...でできることはたくさんあります。 。いくつかのインデックスと制約を削除し、トランザクションにバッチ処理されていることを確認してください。これでほぼ完了です。

    すべてのネットワーク待機を取り除くのはどうですか? LAN上でも、数千を超えるクエリが追加され始めます。

    コピー

    遅延を回避する1つの方法は、 COPYを使用することです。 。 PostgreSQLのCOPYサポートを使用するには、アプリケーションまたはドライバーがCSVのような行のセットを生成し、それらを連続したシーケンスでサーバーにストリーミングする必要があります。または、サーバーにアプリケーションにCSVのようなストリームを送信するように依頼することもできます。

    いずれにせよ、アプリはCOPYを他のクエリとインターリーブすることはできず、コピー挿入は宛先テーブルに直接ロードする必要があります。一般的なアプローチは、コピーです。 一時テーブルに挿入し、そこから INSERT INTO ... SELECT ...を実行します。 、 UPDATE ... FROM .... DELETE FROM ... USING ... 、などを使用して、コピーしたデータを使用してメインテーブルを1回の操作で変更します。

    これは、独自のSQLを直接作成する場合に便利ですが、多くのアプリケーションフレームワークとORMはそれをサポートしていません。さらに、単純な INSERTを直接置き換えることしかできません。 。アプリケーション、フレームワーク、またはクライアントドライバーは、 COPYに必要な特別な表現の変換を処理する必要があります。 、必要なタイプのメタデータ自体を検索します。

    行う注目すべきドライバー COPYをサポート libpq、PgJDBC、psycopg2、およびPg gemが含まれますが、必ずしもそれらの上に構築されたフレームワークとORMである必要はありません。)

    PgJDBC –バッチモード

    PostgreSQLのJDBCドライバーには、この問題の解決策があります。 8.4以降のPostgreSQLサーバーに存在するサポートと、JDBCAPIのバッチ機能に依存してバッチを送信します。 サーバーへのクエリの数は、バッチ全体が正常に実行されたことを確認するために1回だけ待機します。

    まあ、理論的には。実際には、いくつかの実装上の課題がこれを制限しているため、バッチはせいぜい数百のクエリのチャンクでしか実行できません。また、ドライバーは、結果がどれだけ大きくなるかを事前に把握できる場合にのみ、バッチチャンクで結果行を返すクエリを実行できます。これらの制限にもかかわらず、 Statement.executeBatch()の使用 リモートデータベースインスタンスの一括データ読み込みなどのタスクを実行しているアプリケーションのパフォーマンスを大幅に向上させることができます。

    これは標準のAPIであるため、複数のデータベースエンジンで動作するアプリケーションで使用できます。たとえば、HibernateはJDBCバッチ処理を使用できますが、デフォルトでは使用しません。

    libpqとバッチ処理

    他のほとんど(すべて?)のPostgreSQLドライバーはバッチ処理をサポートしていません。 PgJDBCはPostgreSQLプロトコルを完全に独立して実装しますが、他のほとんどのドライバーは内部でCライブラリ libpqを使用します。 これはPostgreSQLの一部として提供されています。

    libpq バッチ処理はサポートしていません。非同期のノンブロッキングAPIがありますが、クライアントは一度に1つのクエリしか「実行中」にすることができません。別のクエリを送信するには、そのクエリの結果が受信されるまで待機する必要があります。

    PostgreSQLのサーバー バッチ処理は問題なくサポートされており、PgJDBCはすでにそれを使用しています。そこで、 libpqのバッチサポートを作成しました そしてそれを次のPostgreSQLバージョンの候補として提出しました。クライアントを変更するだけなので、受け入れられた場合でも、古いサーバーに接続するときに処理が高速化されます。

    libpqの作成者や上級ユーザーからのフィードバックに本当に興味があります。 libpqのベースのクライアントドライバーと開発者 ベースのアプリケーション。試してみたい場合は、PostgreSQL9.6beta1にパッチを適用してください。ドキュメントは詳細で、包括的なサンプルプログラムがあります。

    パフォーマンス

    RDSやHerokuPostgresのようなホスト型データベースサービスは、この種の機能が役立つ場所の良い例だと思いました。特に、私たち自身のネットワークからそれらにアクセスすることは、実際にどれだけの遅延が害を及ぼす可能性があるかを示しています。

    ネットワーク遅延が約320msの場合:

    • バッチ処理なしの500挿入: 167.0s
    • バッチ処理による500個の挿入: 1.2s

    …これは120倍以上高速です。

    通常、アプリサーバーとデータベース間の大陸間リンクを介してアプリを実行することはありませんが、これはレイテンシの影響を強調するのに役立ちます。 localhostへのUNIXソケットを介しても、10000挿入で50%以上のパフォーマンスの向上が見られました。

    既存のアプリのバッチ処理

    残念ながら、既存のアプリケーションのバッチ処理を自動的に有効にすることはできません。アプリは、一連のクエリを送信してから結果を要求するという、わずかに異なるインターフェースを使用する必要があります。

    特にノンブロッキングモードとselect()を使用している場合は、非同期libpqインターフェイスをすでに使用しているアプリを適応させるのはかなり簡単です。 / poll() / epoll() / WaitForMultipleObjectsEx ループ。同期libpqを使用するアプリ インターフェイスにはさらに変更が必要です。

    他のクライアントドライバーのバッチ処理

    同様に、クライアントドライバー、フレームワーク、およびORMは、通常、バッチ処理を使用できるようにするために、インターフェイスと内部の変更が必要になります。すでにイベントループと非ブロッキングI/Oを使用している場合は、変更がかなり簡単なはずです。

    PythonやRubyなどのユーザーがこの機能にアクセスできるようにしたいと思っているので、誰が興味を持っているのか知りたいです。これができると想像してみてください:

    import psycopg2
    conn = psycopg2.connect(...)
    cur = conn.cursor()
    
    # this is just an idea, this code does not work with psycopg2:
    futures = [ cur.async_execute(sql) for sql in my_queries ]
    for future in futures:
        result = future.result  # waits if result not ready yet
        ... process the result ...
    conn.commit()
    

    非同期のバッチ実行は、クライアントレベルで複雑にする必要はありません。

    COPYが最速です

    実用的なクライアントがまだCOPYを好むべき場所 。これが私のラップトップからのいくつかの結果です:

    inserting 1000000 rows batched, unbatched and with COPY
    batch insert elapsed:      23.715315s
    sequential insert elapsed: 36.150162s
    COPY elapsed:              1.743593s
    Done.
    
    の挿入

    作業をバッチ処理すると、ローカルのUNIXソケット接続でも驚くほど大幅なパフォーマンスの向上が得られます…。しかし、コピー 両方の個別のインサートアプローチをほこりの中ではるかに後ろに残します。

    COPYを使用する 。

    画像

    この投稿の画像は、西オーストラリア州パース近郊のムンダリングウィアーから内陸(砂漠)のゴールドフィールズまでのゴールドフィールズ給水スキームのパイプラインです。終了するのに非常に時間がかかり、その設計者であり主要な支持者であるC. Y. O’Connorが、就役する12か月前に自殺したほどの激しい批判を受けていたため、これは適切です。地元の人々はしばしば(誤って)彼が後に死んだと言います パイプラインは、水が流れていないときに建設されました。パイプラインプロジェクトが失敗したと誰もが思っていたので、時間がかかったからです。それから数週間後、注がれた水を出します。


    1. MySQLで列のパーセンテージを計算する方法

    2. Oracleを使用してDjangoで自動テストを作成する際のORA-65096エラーの修正

    3. OracleID列とselectに挿入

    4. SQLServerExpressとExpresslocaldb