コンテンツにスキップ

アーキテクチャ

pgmem は、PostgreSQL 18.3 を一度だけ WebAssembly にコンパイルし、それを事前に Go へ変換して、メモリ上のファイルシステムに対して、本物のクラスタ(postmaster と接続ごとのプロセス、それぞれが WebAssembly インスタンス)として動かしたものです。小さなブリッジが、既定ではループバックの TCP ポートで公開します。Unix domain socket も選べます。

クエリの実行経路クライアントからブリッジ、PostgreSQL バックエンド、メモリ上のファイルシステムへクエリが流れる図。1 つの ServerPostgreSQL ドライバDSN 経由の TCPGo: net.Pipe (Server.Dial)pgmem ブリッジ接続ごとにバックエンドプロセスPostgreSQLpostmaster + プロセスメモリ上の VFSデータ + WAL
接続方法が違っても経路は同じです。接続ごとに postmaster がバックエンドプロセスを起動し、ロックや通知は PostgreSQL 自身が共有メモリで処理します。
テンプレートからフォークへテンプレートを一度準備してスナップショットを作り、上限付きのフォーク枠から独立したサーバーを起動する図。テンプレートマイグレーション + シード稼働中のサーバースナップショット固定したデータの状態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 の crypto と hash パッケージで動きます。
ファイルシステム internal/vfs はメモリ上の POSIX 風ファイルシステムで、データディレクトリ、WAL、share ファイルを保持します。ホストのディスクには何も書きません。
エンジン internal/engine は postmaster とその子プロセスを動かします。PostgreSQL は EXEC_BACKEND でビルドされているので、各プロセスは独立した goroutine 上の新しいモジュールインスタンスです。internal/host がそれらの共有するカーネルの役を担います。pid とシグナル、全インスタンスの同一アドレスにマップされる System V 共有メモリ、セマフォ、接続を postmaster へ届けるソケットです。
ブリッジ pgmem.go が ループバック TCP、明示的に選んだ Unix domain socket、またはプロセス内の net.Pipe で接続を受け、postmaster に渡します。セッションのスタートアップパケット、プリペアドステートメント、LISTEN の集合を覚えておき、クラスタを再起動するリセットの後もクライアント接続が使い続けられるようにします。
埋め込みデータ internal/pgdata/pgdata.tar.zst はビルド時に作った initdb の出力です。これをファイルシステムに展開するだけなので、起動はおよそ 0.1 秒で済みます。
  1. ドライバは DSN を使ってループバック TCP または Unix socket で接続するか、Go のプロセス内ダイヤラを使って net.Pipe で接続します。
  2. ブリッジは接続を postmaster に渡し、postmaster がその接続のためのバックエンドプロセスを起動します。バックエンドはサーバーと同じようにスタートアップパケットを読んで認証します。
  3. バイト列はそのバックエンドのソケットへ流れ、生成された Go の中で PostgreSQL のメインループが動きます。
  4. ロック、通知、ほかのセッションとの関係は、共有メモリを通じた PostgreSQL 自身のものです。
  5. ページの読み出しや WAL の書き込みはメモリ上のファイルシステムに届きます。
  6. 応答が同じ接続を通って返ります。

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

フォークは完全なクラスタです。互いに独立しているので書き込むテストも並列に走らせられますが、それぞれが自分のバッファキャッシュと、自分だけのデータディレクトリを持ちます。ファイルの中身はスナップショットと共有され、書き込んだファイルだけがそのフォークの複製になります(copy-on-write)。同時に存在できる数は MaxForks で制限され、上限に達すると次の要求はどれかが閉じるまで待ちます。

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

Go でも、組み込みエンジンを import しない軽量クライアント pgmemprocess から同じバイナリを使えます。バイナリを別途インストールし、TCP または Unix socket 接続を選びます。fixture が準備済みの snapshot を作り、組み込み Go と共通のドライバ用 helper を渡します。

Unix モードはホスト上に専用の socket ディレクトリを作り、正常終了時に削除します。PostgreSQL のデータと WAL はメモリに保持します。強制終了した場合は socket のエントリが残ることがあります。

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