アーキテクチャ
pgmem は、PostgreSQL 18.3 を一度だけ WebAssembly にコンパイルし、それを事前に Go へ変換して、メモリ上のファイルシステムに対してシングルユーザーモードで動かしたものです。小さなブリッジがループバックの TCP ポートでそれを公開します。
各言語の実行形態: 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 の crypto と hash パッケージで動きます。 |
| ファイルシステム | internal/vfs はメモリ上の POSIX 風ファイルシステムで、データディレクトリ、WAL、share ファイルを保持します。ホストのディスクには何も書きません。 |
| エンジン | internal/engine は PGlite と同じやり方で postgres --single を動かします。プロトコルのバイト列はメモリ上のバッファを流れ、ereport(ERROR) の巻き戻しもポストマスターなしで処理します。 |
| ブリッジ | pgmem.go が 127.0.0.1(またはプロセス内の net.Pipe)で接続を受け、トランザクション境界で 1 つのセッションに多重化し、NOTIFY を LISTEN している接続に届けます。 |
| 埋め込みデータ | internal/pgdata/pgdata.tar.zst はビルド時に作った initdb の出力です。これをファイルシステムに展開するだけなので、起動はおよそ 0.1 秒で済みます。 |
クエリの通り道
Section titled “クエリの通り道”- ドライバは DSN を使ってループバック TCP で接続するか、Go のプロセス内ダイヤラを使って
net.Pipeで接続します。 - ブリッジはどちらの接続からもスタートアップパケットを読みます。サーバーの別のデータベースが指定されていれば、先にそのデータベースでバックエンドを再起動します。存在しないデータベースは SQLSTATE 3D000 で拒否します。
- 接続は、メッセージのひとまとまりの間、
BEGINの後ならトランザクション全体の間、バックエンドを占有します。 - バイト列が、生成された Go の中の PostgreSQL メインループに渡されます。
- ページの読み出しや WAL の書き込みはメモリ上のファイルシステムに届きます。
- 応答が同じ接続を通って返ります。
スナップショットとフォーク
Section titled “スナップショットとフォーク”スナップショットは、準備済みのサーバーで CHECKPOINT を実行し、メモリ上のデータディレクトリを丸ごと複製します。フォークはその像をもう一度複製し、新しいバックエンドを起動して、新しいポートで待ち受けます。リセットは動いているサーバーに対して同じことをその場で行います。ポートもクライアントの接続も残るので、アプリケーションが import 時に取得した URL はそのまま使えます。起動時に再生する WAL はほとんどないのでミリ秒で終わり、読み込み済みのエンジンのコードはプロセス内の全サーバーで共有されます。
フォークは完全なバックエンドです。互いに独立しているので書き込むテストも並列に走らせられますが、それぞれが自分のバッファキャッシュとデータディレクトリの複製を持ちます。同時に存在できる数は MaxForks で制限され、上限に達すると次の要求はどれかが閉じるまで待ちます。
ほかの言語から
Section titled “ほかの言語から”Python、Java、Node.js のパッケージは、cmd/pgmem からビルドした単体バイナリ pgmem を同梱しています。ラッパーはこれを起動し、テンプレートの DSN が入った JSON を 1 行読み取り、snapshot、fork、close の要求を JSON 行として標準入力に送ります。親プロセスが終了すると標準入力が閉じ、バイナリはすべてを終了します。テストが異常終了してもサーバーが残ることはありません。別プロセスで動く Node.js のテストワーカーは、代わりにループバックの制御ソケット経由でフォークやリセットを行います。ソケットの接続で作ったフォークは、その接続が切れると閉じます。
テストプロセスごと(pytest-xdist のワーカー、フォークされたテスト JVM)に 1 つずつバイナリが起動します。その中でのフォークの振る舞いは Go とまったく同じです。