postgresql-pure を開発しました
この記事は Haskell Advent Calendar 2019 の6日目の記事です。
postgresql-pure は Haskell の PostgreSQL ドライバー(クライアントライブラリー)で次のような目標で開発しました。
- マルチコア環境でのパフォーマンス向上
- 暗黙のロックを回避する
- マルチプラットフォーム対応
使用方法
簡単に使用方法を説明します。
下記のようなテーブルがあるとします。
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
まずは 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 固有の効率化について説明します。
PostgreSQL プロトコルには2つの問い合わせ方法があります。ひとつは簡易問い合わせで、もうひとつは拡張問い合わせです。簡易問い合わせには最小限の機能しかなくプリペアドステートメントやバイナリーフォーマット、結果を1レコードずつフェッチすることなどはサポートされていないので、このライブラリーでは拡張問い合わせを採用しました。
PostgreSQL プロトコルは TCP の上で動作し、複数のメッセージは結合してひとつの TCP ペイロードに格納することができます。
下記の図はメッセージをひとつずつ送信した場合を表しています。
メッセージを結合した場合は下記のようになります。
メッセージを結合することで送受信するデータ量を減らし、またシステムコールの回数も減らすことができます。
既存のドライバー
- postgresql-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;
環境は下記の通りです。
- クライアント
- CPU: Intel Xeon E5-2650L v2
- スレッド: 20
- OS: CentOS 7.5.1804
- CPU: Intel Xeon E5-2650L v2
- サーバー
- クライアントと同じ
- ネットワーク
- 2 × 10 Gbps(チーミング)
計測結果を下に示します。縦軸は秒間リクエスト数で横軸はスレッド数です。
このライブラリーも postgres-wire も約4スレッドまではほぼ同じパフォーマスです。それ以上になると postgres-wire が線形比例を下回っていくのに対し、このライブラリーはより線形に近くなっています。
(比較対象は開発途中でのベンチマークにおいておそかったものを除去しています。)
postgresql-pure は IIJ イノベーションインスティテュートの業務として作成されました。