2021年3月28日日曜日

外部テーブルに対する一括挿入の高速化 - PostgreSQL 14でコミットされた機能の先行紹介

はじめに

現在Amit Kapila氏が率いるグローバルPostgreSQL開発チームは、富士通のPostgreSQLへの取り組みの一環として、コミュニティーへ継続的な貢献を行っています。私はこのチームの一員として、世界中の才能ある情熱的なコミュニティーメンバーとともに、PostgreSQLの発展に積極的に取り組んでいます。

私たちのチームは、メンバーが取り組んだ機能やパッチに焦点を当て、ブログ記事を発信していきます。この記事では、PostgreSQL 14にコミットされた「外部テーブルへの一括挿入」について、それに取り組んだ理由と、刺激的な性能向上の成果をご紹介します。

なぜこの開発に取り組んだか?

私たちのチームは、PostgreSQLのスケーラビリティや性能の向上に注力しています。大きな目標の1つは、読み書きスケールアウトを実現することです。これにより、PostgreSQLを適用できるワークロードをさらに広げたいと考えています。

スケールアウトのための複数サーバー間のやりとりには、外部データラッパー(FDW)機構に基づくpostgres_fdwを用いるアイデアが、コミュニティでは有望であり現実的だと考えられています。なぜなら、postgres_fdwは長年にわたり、数多くの開発者による改善を積み重ねてきているからです。

そのようなときに私たちは、コミッターであるTomas Vondra氏がpostgres_fdwによる一括行挿入を速くしようとしているのを見ました。彼が言うには、FDWを使ってシャード化したデータベースへの挿入が遅いという問題を、顧客からよく報告されるそうです。

あるユーザーもPostgreSQLのバグ報告用メーリングリストに、同様の問題を報告しました。その報告では、Google Cloud上でpostgres_fdwを使ってシャード化したデータベースに対し、INSERT SELECT文で2,000万行を挿入するのに、1時間50分もかかったといいます。シャード化しない場合は8分で完了したのに比べると、14倍も遅いということです。

遅い原因は、ネットワークのレイテンシと通信回数です。現在のFDW機構では、外部データラッパーが1行ずつ外部テーブルに行を挿入します。そのたびに、遠隔のサーバーとの往復通信が生じるのです。

Tomas Vondra氏たちはpostgres_fdwのみを変更し、まとめて複数の行を外部サーバーに送る方法を志向していました。しかし、executorを変更せずに済ませる方法を模索している間に、開発が3か月ほど止まっていたのです。

どのような機能を開発したか?

そこで私は、次のような提案をしました。外部サーバーに複数行を一括して送るというアイデアは、Tomas Vondra氏のものと同じです。

  • FDWインタフェースに、一括行挿入のための関数を追加する。具体的には、BeginForeignBatchInsert, ExecForeignBatchInsert, EndForeignBatchInsert, GetForeignBatchSize。
  • ExecForeignBatchInsert()は行の配列と行数を受け取り、それらの行を一括して外部サーバーに送る。
  • executorは複数の行を蓄積し、まとめてFDWに渡す。蓄積する行数は、GetForeignBatchSize()でFDWから取得する。
  • postgres_fdwは、渡された行配列を使って「INSERT ... VALUES (row 1), (row 2), ..., (row n)」文を組み立てて、外部サーバーでそれを実行する。
  • postgres_fdwの外部サーバーと外部テーブルの設定に、batch_sizeオプションを追加する。これは、外部テーブルごとに一括して挿入する行数を指定する。

実に素直でシンプルですよね?

これにより、INSERT SELECT文だけでなく、複数行を指定したINSERT VALUES文も速くなります。アプリケーションを変更する必要はありません。

上記のインタフェースは、oracle_fdwやmysql_fdwといった、他のデータベース用のFDWでも素直に実装できるはずです。

どれだけ速くなったか?

まず、単純なテーブルに対するデータ挿入を測定してみました。対象テーブルは1つのint型のプライマリキー列と、1つのtext型の列を持ちます。

別のテーブルからそのテーブルに、INSERT SELECT 文で100万件の行を挿入するのにかかった時間は次のとおりです。このとき、postgres_fdwのbatch_sizeパラメータの設定値は100です。つまり、一度に最大で100行を外部サーバーに送ります。

  • FDWなしのローカルテーブル: 6.1秒
  • FDWを使ったリモートテーブル(改善前): 125.3秒
  • FDWを使ったリモートテーブル(改善後): 11.1秒

すばらしい!11倍も速くなりました。

次に、同じ列からなる対象テーブルを8つのハッシュパーティションに分割しました。同様に、INSERT SELECT 文で100万行を挿入するのにかかった時間は次のとおりです。

  • FDWなしのローカルテーブル: 8.6秒
  • FDWを使ったリモートテーブル(改善前): 113.7秒
  • FDWを使ったリモートテーブル(改善後): 12.5秒

これもよいですね!9倍の速度向上です。

なお、この測定では、外部サーバーを同じホストに配置しました。「ping localhost」で測定されるネットワーク・レイテンシは34マイクロ秒です。これほど小さなレイテンシでも大きな性能向上が見られたということは、クラウドなどより大きなネットワーク・レイテンシを伴う環境では、この機能の効果はさらに大きいでしょう。

さらなる高速化への期待

この機能により、1つの外部テーブルへのデータ追加が劇的に速くなりました。ただ、これは単一のCPUを使った逐次処理の高速化です。

一方、私の同僚のGreg Nancarrow氏は、複数のCPUを使ってINSERT SELECT文を並列に処理できるようにする開発に取り組んでいます。また別の開発者たちは、CREATE TABLE AS SELECT(CTAS)文を並列化しようとしています。

これらを組み合わせることで、シャード化されたOLTPデータベースへのデータ移行や、データウェアハウスへのETL処理がさらに高速化する可能性に期待します。今後、またよいお知らせができることを楽しみにしています。

謝辞

この機能が比較的早くコミットされたことについて、Tomas Vondra氏やAmit Langote氏をはじめとする開発者の方々に深く感謝いたします。彼らの素早く熱心なフィードバックのおかげで、この機能を実現できたと考えています。