コンテンツにスキップ

アーキテクチャ

pgmem は、PostgreSQL 18.3 を一度だけ WebAssembly にコンパイルし、それを事前に Go へ変換して、メモリ上のファイルシステムに対してシングルユーザーモードで動かしたものです。小さなブリッジがループバックの TCP ポートでそれを公開します。

クエリの実行経路クライアントからブリッジ、PostgreSQL バックエンド、メモリ上のファイルシステムへクエリが流れる図。1 つの ServerPostgreSQL ドライバDSN 経由の TCPGo: net.Pipe (Server.Dial)pgmem ブリッジ接続を多重化PostgreSQL単一セッションメモリ上の VFSデータ + WAL
接続方法が違っても、クエリは同じバックエンドに届きます。1 つの Server 内では接続がトランザクション境界で直列化され、並列性は独立したフォークから得ます。
テンプレートからフォークへテンプレートを一度準備してスナップショットを作り、上限付きのフォーク枠から独立したサーバーを起動する図。テンプレートマイグレーション + シード稼働中のサーバースナップショット固定したデータの状態MaxForksこのスナップショットの MaxForks 枠フォーク 1データの複製サーバー + URLフォーク 2データの複製サーバー + URLフォーク Nデータの複製サーバー + URLFork()枠が埋まると待機し、Close() で枠が空く
各フォークはスナップショットから独立したデータ領域を持ちます。Reset は元の状態を同じフォークへ戻すため、ポートとクライアント接続は維持されます。

各言語の実行形態: Go はライブラリをアプリケーションプロセス内で使います。Python、Java、Node.js は cmd/pgmem バイナリを起動し、ドライバは各サーバーの DSN へ接続します。JSON Lines と Node.js の制御ソケットはフォーク操作用で、クエリの通信経路ではありません。

レイヤ 中身
PostgreSQL のソース PGlite 版(electric-sql/postgres-pglite)。コミットとチェックサムを wasm/postgres-pglite.lock で固定し、ビルド時に wasm/patches.py でパッチを当てます。
WebAssembly モジュール wasm/build.sh が Emscripten で 1 つの静的モジュールにリンクします。ロード可能モジュールや拡張も動的ロードではなく静的にリンクします。OpenSSL、ICU、zlib は含みません。
生成された Go wasm2go がモジュールを純 Go のパッケージ internal/aot/pgaot に変換します。.wasm はビルドの中間生成物で、配布されるのは Go だけです。wasm ランタイムも cgo も要りません。
ホスト internal/host が、モジュールが import するシステムコール、時計、メモリ拡張、終了時の巻き戻しを実装します。乱数、CRC-32C、ハッシュ、pgcrypto の暗号は Go の cryptohash パッケージで動きます。
ファイルシステム internal/vfs はメモリ上の POSIX 風ファイルシステムで、データディレクトリ、WAL、share ファイルを保持します。ホストのディスクには何も書きません。
エンジン internal/engine は PGlite と同じやり方で postgres --single を動かします。プロトコルのバイト列はメモリ上のバッファを流れ、ereport(ERROR) の巻き戻しもポストマスターなしで処理します。
ブリッジ pgmem.go127.0.0.1(またはプロセス内の net.Pipe)で接続を受け、トランザクション境界で 1 つのセッションに多重化し、NOTIFY を LISTEN している接続に届けます。
埋め込みデータ internal/pgdata/pgdata.tar.zst はビルド時に作った initdb の出力です。これをファイルシステムに展開するだけなので、起動はおよそ 0.1 秒で済みます。
  1. ドライバは DSN を使ってループバック TCP で接続するか、Go のプロセス内ダイヤラを使って net.Pipe で接続します。
  2. ブリッジはどちらの接続からもスタートアップパケットを読みます。サーバーの別のデータベースが指定されていれば、先にそのデータベースでバックエンドを再起動します。存在しないデータベースは SQLSTATE 3D000 で拒否します。
  3. 接続は、メッセージのひとまとまりの間、BEGIN の後ならトランザクション全体の間、バックエンドを占有します。
  4. バイト列が、生成された Go の中の PostgreSQL メインループに渡されます。
  5. ページの読み出しや WAL の書き込みはメモリ上のファイルシステムに届きます。
  6. 応答が同じ接続を通って返ります。

スナップショットは、準備済みのサーバーで CHECKPOINT を実行し、メモリ上のデータディレクトリを丸ごと複製します。フォークはその像をもう一度複製し、新しいバックエンドを起動して、新しいポートで待ち受けます。リセットは動いているサーバーに対して同じことをその場で行います。ポートもクライアントの接続も残るので、アプリケーションが import 時に取得した URL はそのまま使えます。起動時に再生する WAL はほとんどないのでミリ秒で終わり、読み込み済みのエンジンのコードはプロセス内の全サーバーで共有されます。

フォークは完全なバックエンドです。互いに独立しているので書き込むテストも並列に走らせられますが、それぞれが自分のバッファキャッシュとデータディレクトリの複製を持ちます。同時に存在できる数は MaxForks で制限され、上限に達すると次の要求はどれかが閉じるまで待ちます。

Python、Java、Node.js のパッケージは、cmd/pgmem からビルドした単体バイナリ pgmem を同梱しています。ラッパーはこれを起動し、テンプレートの DSN が入った JSON を 1 行読み取り、snapshotforkclose の要求を JSON 行として標準入力に送ります。親プロセスが終了すると標準入力が閉じ、バイナリはすべてを終了します。テストが異常終了してもサーバーが残ることはありません。別プロセスで動く Node.js のテストワーカーは、代わりにループバックの制御ソケット経由でフォークやリセットを行います。ソケットの接続で作ったフォークは、その接続が切れると閉じます。

テストプロセスごと(pytest-xdist のワーカー、フォークされたテスト JVM)に 1 つずつバイナリが起動します。その中でのフォークの振る舞いは Go とまったく同じです。