この記事は Haskell Advent Calendar 2019 の6日目の記事です。

hackage.haskell.org

postgresql-pure は Haskell の PostgreSQL ドライバー（クライアントライブラリー）で次のような目標で開発しました。

マルチコア環境でのパフォーマンス向上 暗黙のロックを回避する

マルチプラットフォーム対応 C ライブラリーの libpq への依存をなくして特に Windows でのビルドを容易にする 既存ライブラリーとしては postgres-wire が高速だがそれは Windows をサポートしていない pure Haskell 実装のため Eta などの環境へも移植しやすい可能性がある



使用方法

簡単に使用方法を説明します。

下記のようなテーブルがあるとします。

CREATE TABLE person ( id serial PRIMARY KEY, name varchar ( 255 ) NOT NULL ); INSERT INTO person (name) VALUES ( ' Ada ' );

このとき ghci で下記のように実行できます。

> : set - XOverloadedStrings > : set - XFlexibleContexts > : set - XDataKinds > : set - XTypeFamilies > : set - XTypeApplications > > import Database.PostgreSQL.Pure > import Data.Default.Class (def) > import Data.Int (Int32) > import Data.ByteString (ByteString) > import Data.Tuple.Only (Only (Only)) > import Data.Tuple.List.Only () > import Data.Tuple.Homotuple.Only () > > conn <- connect def > preparedStatementProcedure = parse "" "SELECT id, name FROM person WHERE id = $1" Nothing > portalProcedure <- bind @ _ @ 2 @ _ @ _ "" BinaryFormat BinaryFormat (parameters conn) (const $ fail "" ) (Only ( 1 :: Int32)) preparedStatementProcedure > executedProcedure = execute @ _ @ _ @ (Int32, ByteString) 0 (const $ fail "" ) portalProcedure > ((_, _, e, _), _) <- sync conn executedProcedure > records e [( 1 , "Ada" )]

重要な部分を抽出すると parse 、 bind 、 execute の順に呼びだし、最後に sync でサーバーに送信します。 parse ・ bind ・ execute は入出力のない関数であり、リクエストのビルダーと対応するレスポンスのパーサーを構築しています。そしてそのビルダーとパーサーを sync が使用して送受信を行ないます。 bind がモナド値を返すようになっているのは失敗する可能性があるためで、型は MonadFail m => m a となっており IO a ではありません。

型適用が所々に明記されていますが、これは ghci で実行しているので結果の使用部分からの推論ができないためで、実際のコードではほとんどの場合で型適用の明記は必要なくなります。

postgresql-pure では、タプルによる要素数の不一致を型検査で検出するインターフェース Database.PostgreSQL.Pure （上記の例）と、しないインターフェース Database.PostgreSQL.Pure.List と、HDBC 互換インターフェース Database.HDBC.PostgreSQL.Pure の3つを提供しています。

高速化

高速化に寄与した技術を説明します。

まずは Haskell 一般に関連するものについての技術です。

大きな byte string を何度も確保しない

送信と受信のたびに byte string を確保することを避けました。約 3 kB 以上のメモリーを確保すると暗黙のグローバルロックがかかります1。送受信のたびに確保するのをやめ、代わりにバッファとして確保した領域を何度も再利用するようにしました。手動によるメモリー管理のために bytestring パッケージの Data.ByteString.Internal.mallocByteString と network パッケージの Network.Socket.sendBuf と recvBuf を使用しました。

mallocByteString :: Int -> IO (ForeignPtr a) sendBuf :: Socket -> Ptr Word8 -> Int -> IO Int recvBuf :: Socket -> Ptr Word8 -> Int -> IO Int

接続時に mallocByteString で2つのバッファを確保します。送信するメッセージは Data.ByteString.Builder.Extra.BufferWriter によって構築し、受信するメッセージは Data.Attoparsec.parseWith でパースします。

シンボルには ShortByteString を使用する

LISP のシンボルのような短い文字列には ShortByteString を使用しましょう。 ShortByteString は ByteString よりもオーバーヘッドが少なく、またヒープフラグメンテーションを引き起こしません。 ShortByteString には length や index のような簡単な操作だけが提供され複雑な操作は提供されていません。このライブラリーではサーバーのパラメーターを保存するために使用しました。

次に PostgreSQL 固有の効率化について説明します。

PostgreSQL プロトコルには2つの問い合わせ方法があります。ひとつは簡易問い合わせで、もうひとつは拡張問い合わせです。簡易問い合わせには最小限の機能しかなくプリペアドステートメントやバイナリーフォーマット、結果を1レコードずつフェッチすることなどはサポートされていないので、このライブラリーでは拡張問い合わせを採用しました。

PostgreSQL プロトコルは TCP の上で動作し、複数のメッセージは結合してひとつの TCP ペイロードに格納することができます。

下記の図はメッセージをひとつずつ送信した場合を表しています。

メッセージをひとつずつ送信した場合

メッセージを結合した場合は下記のようになります。

メッセージを結合した場合

メッセージを結合することで送受信するデータ量を減らし、またシステムコールの回数も減らすことができます。

既存のドライバー

postgresql-libpq https://github.com/phadej/postgresql-libpq libpq の薄いラッパー

postgresql-simple https://github.com/phadej/postgresql-simple Hackage で一番ダウンロードされている postgresql-libpq に依存 プリペアドステートメントはサポートせず 簡易問い合わせ

HDBC-postgresql https://github.com/hdbc/hdbc-postgresql libpq を直接使用 擬似的なプリペアドステートメントをサポート クライアントで代入をする 簡易問い合わせ

hasql https://github.com/nikita-volkov/hasql postgesql-libpq に依存 拡張問い合わせ

postgresql-typed https://github.com/dylex/postgresql-typed libpq に非依存 コンパイル時にデータベースに接続し、問い合わせの PostgreSQL の型と Haskell の型に互換性があるか検査する

postgres-wire https://github.com/postgres-haskell/postgres-wire libpq に非依存 UNIX 系 OS のみサポート Hackage にアップロードされていない メッセージの結合をサポート 拡張問い合わせ



下記のような単純な定数値のみの問い合わせを秒間何回問い合わせられるかを計測しました。定数値を使用したのはサーバーがボトルネックにならないためです。

SELECT 2147483647 :: int4, 9223372036854775807 :: int8, 1234567890 . 0123456789 :: numeric, 0 . 015625 :: float4, 0 . 00024414062 :: float8, ' hello ' :: varchar , ' hello ' :: text, ' \xDEADBEEF ' :: bytea, ' 1000-01-01 00:00:00.000001 ' :: timestamp, ' 2000-01-01 00:00:00.000001+14:30 ' :: timestamptz, ' 0001-01-01 ' :: date , ' 23:00:00 ' :: time, true :: bool;

環境は下記の通りです。

計測結果を下に示します。縦軸は秒間リクエスト数で横軸はスレッド数です。

このライブラリーも postgres-wire も約4スレッドまではほぼ同じパフォーマスです。それ以上になると postgres-wire が線形比例を下回っていくのに対し、このライブラリーはより線形に近くなっています。

（比較対象は開発途中でのベンチマークにおいておそかったものを除去しています。）

ベンチマーク結果

postgresql-pure は IIJ イノベーションインスティテュートの業務として作成されました。