PostgreSQL Advent Calendar 2019 の 18 日目の記事です.

Postgres のソースコードを読むための知識をまとめています. 今回の記事では,デバッグ可能な Postgres をビルドし,デバッガでソースコードリーディングを行うまでを紹介しています.

前提知識

Postgres を理解するためには,まず,リレーショナルデータベースの仕組みを理解する必要がある. 日本語であれば,お茶の水大学の増永良文氏による書籍リレーショナルデータベース入門が初学者に適している. リレーショナルデータベースで使われている要素技術の詳細については,日本語ではカバーしきれないため洋書を読むことになる. Postgres の公式 Wiki にもいくつか書籍が挙げられている1 が,個人的には CMU の Andy Pavlo 氏が講義の参考図書として挙げている Avi Silberschatz 氏らの Database System Concepts がおすすめである. 実践的な内容については,少し古いがソースコードレベルの解説が豊富な Jim Gray 氏の Transaction Processing が参考になる.

PostgreSQL の基礎知識

postgres
(backend process)
postgres<br>(backend process)
Client
Client
postgres
(backend process)
postgres<br>(backend process)
postgres
(server process)
postgres<br>(server process)
walwriter
walwriter
background writer
background writer
$PGDATA
(database cluster)
$PGDATA<br>(database cluster)
Shared Memory
Shared Memory
fork()
fork()
Connection request
Connection request
Query
Query
PostgreSQL Server
PostgreSQL Server

Postgres のアーキテクチャを頭に叩き込んでおく必要がある. 上の図は Postgres サーバーの主要なコンポーネントとそれらの関係を表している.

Postgres は,クライアントサーバーモデルに基づく共有メモリ型のアーキテクチャを採用している. サーバープロセス postgres は,クライアントからコネクション要求を受け取るとフォークしてバックエンドプロセス postgres を生成する. 生成されたバックエンドプロセスは,以降,そのクライアントとのやりとりを担う. クライアントは,バックエンドプロセスと通信してクエリを送信したり,結果を受け取ったりする.

Postgres のすべてのプロセス間で共有されるメモリ領域は,共有メモリと呼ばれる. 共有メモリは,主に低速なストレージへのアクセスを減らすバッファ (共有バッファ) の役割を持つ. ストレージや共有メモリのアクセスは,ページ2と呼ばれる単位 (通常 8 kB) で行う. ページサイズは,ディスクのブロックサイズを考慮し効率的にアクセスできるように決められている.

Postgres は,ストレージ上にデータベースクラスタと呼ばれる領域を持つ. これは,テーブルデータや WAL ファイルなど,Postgres が管理するデータベースのデータをすべて格納する領域である. Postgres を利用する際には,通常,環境変数 $PGDATA にデータベースクラスタの格納先ディレクトリを指定する. WAL (Write-ahead Logging) は,リカバリにおいて重要な概念で,トランザクションログと呼ばれる,トランザクションにより行われたデータの変更をログとしてストレージに書き出す手法である. クライアントからトランザクションを受け取ると,postgres は共有メモリ上の WAL バッファにトランザクションログを書き込む. WAL バッファの内容は,walwriter というバックグラウンドプロセスによりストレージに書き出されて永続化される. テーブルデータ自体の変更は,postgres が共有メモリ上で行い,最終的に background writer というバックグラウンドプロセスによってストレージに書き出されて永続化される.

デバッグ可能な Postgres のビルド

Postgres の内部構造をソースコードレベルで理解するために,デバッガで Postgres を解析する. ここでは,デバッグ可能な Postgres をソースコードからビルドしていく.

前提

Postgres のビルドには,以下のツールが必要になる3

  • GNU Make 3.80 以上
  • C99 準拠した ISO/ANSI C コンパイラ
  • GNU Readline
  • zlib

Git のソースコードからビルドする際は,上記に加えて以下のツールが必要になる3

  • Git
  • Flex 2.5.31 以上
  • Bison 1.875 以上
  • Perl 5.8.3 以上

今回は以下の環境を前提とする.

  • macOS Catalina 10.15.7
  • Clang 13.0.1
  • GNU Make 4.3
  • GNU Readline 8.1.2
  • zlib 1.2.11
  • Git 2.36.0
  • Flex 2.6.4
  • Bison 3.5
  • Perl 5.34.0

また,Postgres のインストール先のディレクトリとデータベースクラスタのディレクトリは以下のように設定する.

  • インストール先: $HOME/.local/pgsql4
  • データベースクラスタ: $HOME/.local/pgsql/data5

macOS 特有の設定として,System Integrity Protection (SIP) を無効化する必要がある6

ソースコードの取得とビルド

ソースコードを取得し,ビルドするまでの手順は以下のとおりである.

git clone git://git.postgresql.org/git/postgresql.git
cd postgresql
mkdir build
cd build
../configure --prefix=$HOME/.local/pgsql --enable-debug --enable-cassert CC=/usr/local/opt/llvm/bin/clang CFLAGS=-O0
make -j12

ソースコードは 公式の Git リポジトリ から取得する.

configure スクリプトにデバッグ用のオプションをいくつか指定して Makefile を生成する3--prefix オプションでインストール先のディレクトリを $HOME/.local/pgsql に指定し,--enable-debug オプションで Clang に -g オプションを渡して完全なデバッグ情報を付加7--enable-cassert オプションで実行時のエラーチェックを有効化している. コンパイラに渡すフラグは,デバッガでステップ実行した際の実行順序をソースコードに記述されている順序と整合させるために CFLAGS=-O0 としている. パフォーマンスとデバッグのしやすさのバランスを取る場合には CFLAGS=-Og を指定するとよい1

make にはビルドを並列実行するために -j オプションを渡している. -j の後に続けて整数を指定することで並列実行数を指定することができる. 今回は getconf _NPROCESSORS_ONLN で得られたプロセッサ数が 12 であったため,その値を採用している.

リグレッションテスト

リグレッションテストは,ビルドした Postgres が正常に動作することを確認する一連のテストである8. 次のコマンドを入力してリグレッションテストを実施する.

make check

最後の出力が以下のようになっていれば,テストは正常に完了している.

$ make check

(snipped)

=======================
 All 214 tests passed. 
=======================

インストール

make コマンドで Postgres のインストールを行う. ここで,インストール先のディレクトリは configure--prefix に指定した $HOME/.local/pgsql となる.

make install

次に,Postgres のバイナリがインストールされているディレクトリにパスを通す. データベースクラスタのディレクトリを表す環境変数 $PGDATA も設定しておく.

echo 'PATH=$HOME/.local/pgsql/bin:$PATH
LD_LIBRARY_PATH=$HOME/.local/pgsql/lib
PGDATA=$HOME/.local/pgsql/data
export PATH
export LD_LIBRARY_PATH
export PGDATA' >> ~/.bashrc
source ~/.bashrc

以上で Postgres のインストールが完了した.

データベースクラスタの初期化とデータベースの作成

データベースクラスタの初期化は pg_ctl コマンド9で行う. $PGDATA で設定したディレクトリと別のディレクトリにデータベースクラスタを作成したい場合は,-D オプションでディレクトリを指定する. 今回はすでに $PGDATA$HOME/.local/pgsql/data に設定しているため,オプションを省略している.

$ pg_ctl init
The files belonging to this database system will be owned by user "sira".
This user must also own the server process.

The database cluster will be initialized with this locale configuration:
  provider:    libc
  LC_COLLATE:  C
  LC_CTYPE:    UTF-8
  LC_MESSAGES: C
  LC_MONETARY: C
  LC_NUMERIC:  C
  LC_TIME:     C
The default database encoding has accordingly been set to "UTF8".
initdb: could not find suitable text search configuration for locale "UTF-8"
The default text search configuration will be set to "simple".

Data page checksums are disabled.

creating directory /Users/sira/.local/pgsql/data ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting default time zone ... Asia/Tokyo
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok

initdb: warning: enabling "trust" authentication for local connections
hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb.

Success. You can now start the database server using:

    /Users/sira/.local/pgsql/bin/pg_ctl -D /Users/sira/.local/pgsql/data -l logfile start

次に,pg_ctl コマンド9でサーバープロセスを起動する.

$ pg_ctl start
waiting for server to start....2022-04-27 22:33:25.839 JST [57076] LOG:  starting PostgreSQL 15devel on x86_64-apple-darwin19.6.0, compiled by Homebrew clang version 13.0.1, 64-bit
2022-04-27 22:33:25.842 JST [57076] LOG:  listening on IPv6 address "::1", port 5432
2022-04-27 22:33:25.842 JST [57076] LOG:  listening on IPv4 address "127.0.0.1", port 5432
2022-04-27 22:33:25.842 JST [57076] LOG:  listening on Unix socket "/tmp/.s.PGSQL.5432"
2022-04-27 22:33:25.845 JST [57079] LOG:  database system was shut down at 2022-04-27 22:32:35 JST
2022-04-27 22:33:25.848 JST [57076] LOG:  database system is ready to accept connections
 done
server started

サーバーの起動が完了したら,createdb コマンド10を実行してテスト用のデータベース test を作る11

createdb test

以上で,データベースサーバーの準備が整った. クライアントプログラム psql12 を使って作成したデータベース test に接続して適当なクエリを実行してみよう.

$ psql test
psql (15devel)
Type "help" for help.

test=# SELECT 1;
 ?column? 
----------
        1
(1 row)

test=# 

クライアントプログラムを終了するには,\q と入力するか,Ctrl+D を入力する.

サーバープロセスを終了するには,以下のコマンドを入力する.

$ pg_ctl stop
waiting for server to shut down....2022-04-27 22:35:22.082 JST [57076] LOG:  received fast shutdown request
2022-04-27 22:35:22.082 JST [57076] LOG:  aborting any active transactions
2022-04-27 22:35:22.083 JST [57076] LOG:  background worker "logical replication launcher" (PID 57082) exited with exit code 1
2022-04-27 22:35:22.083 JST [57077] LOG:  shutting down
2022-04-27 22:35:22.084 JST [57077] LOG:  checkpoint starting: shutdown immediate
2022-04-27 22:35:22.119 JST [57077] LOG:  checkpoint complete: wrote 802 buffers (4.9%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.017 s, sync=0.017 s, total=0.036 s; sync files=185, longest=0.001 s, average=0.001 s; distance=4358 kB, estimate=4358 kB
2022-04-27 22:35:22.123 JST [57076] LOG:  database system is shut down
 done
server stopped

データベースの削除・データベースクラスタの削除・ビルド出力の削除・Postgres のアンインストール

必要に応じて以下を行う.

テスト用のデータベース test を削除するには,次のコマンドを実行する.

dropdb test

$PGDATA にあるデータベースクラスタを削除するには,ディレクトリを素直に削除すればよい.

rm -rf $PGDATA

make install でインストールした Postgres をアンインストールには次のコマンドを実行する.

make uninstall

ビルドで生成されたファイルは次のコマンドで削除することができる.

make clean

Postgres ソースコードリーディング

今回はクライアントから送信されたクエリがバックエンドプロセスに渡され.パースされる部分までを読む.

エディタの設定

Postgres のソースコードを読む際のエディタは Vim か Emacs のどちらかを利用することが推奨されている. それぞれのエディタの設定は ここ にまとまっている.

ソースコードを読む際はタグジャンプを駆使する. Vim なら src/tools/make_ctag,Emacs なら src/tools/make_etags にあるスクリプトを実行してタグインデックスを作成しておく.

LLDB にバックエンドプロセスの制御を移す

Postgres サーバーを起動して,psql コマンドからテスト用のデータベースに接続する.

$ psql test
psql (15devel)
Type "help" for help.

test=# 

このとき,Postgres サーバーは接続してきたクライアント専用のバックエンドプロセスをフォークして生成する. Postgres に関連するプロセスを ps コマンドを使って調べてみると,親プロセス (1065) とバックエンドプロセス (1527) の他に walwriter (1088) や background writer (1087) などのバックグラウンドプロセスが動作していることが確認できる.

$ pgrep -fil postgres 
57146 /Users/sira/.local/pgsql/bin/postgres
57147 postgres: checkpointer  (/Users/sira/.local/pgsql/bin/postgres)
57148 postgres: background writer  (/Users/sira/.local/pgsql/bin/postgres)
57150 postgres: walwriter  (/Users/sira/.local/pgsql/bin/postgres)
57151 postgres: autovacuum launcher  (/Users/sira/.local/pgsql/bin/postgres)
57152 postgres: logical replication launcher  (/Users/sira/.local/pgsql/bin/postgres)
57161 postgres: sira test [local] idle (/Users/sira/.local/pgsql/bin/postgres)

psql コマンドで接続しているバックエンドプロセスのプロセス ID を調べるには,次の関数を使う.

test=# SELECT * FROM pg_backend_pid();
 pg_backend_pid 
----------------
          57161
(1 row)

次に,LLDB を開いて PID を指定してバックエンドプロセスにアタッチする.

$ /usr/local/opt/llvm/bin/lldb
(lldb) process attach -p 57161
Process 57161 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
    frame #0: 0x00007fff69944766 libsystem_kernel.dylib`kevent + 10
libsystem_kernel.dylib`kevent:
->  0x7fff69944766 <+10>: jae    0x7fff69944770            ; <+20>
    0x7fff69944768 <+12>: movq   %rax, %rdi
    0x7fff6994476b <+15>: jmp    0x7fff69940629            ; cerror_nocancel
    0x7fff69944770 <+20>: retq
Executable module set to "/Users/sira/.local/pgsql/bin/postgres".
Architecture set to: x86_64h-apple-macosx-.

これでバックエンドプロセスの制御を LLDB に移すことができ,デバッグが可能になった.

クエリをパースするまでの流れ

バックエンドプロセスが受け取った SQL の処理を実際に開始するのは src/backend/tcop/postgres.c で定義されている exec_simple_query 関数からである. そこで,LLDB を使ってこの関数にブレークポイントをつける.

(lldb) breakpoint set --name exec_simple_query 
Breakpoint 1: where = postgres`exec_simple_query + 43 at postgres.c:986:21, address = 0x0000000107b9aa8b

次に,SQL プロンプトから SQL を送信する. 今回は,以下の単純なクエリを送信することとする.

test=# SELECT 1;

LLDB でブレークポイントにヒットするまで処理を継続する.

(lldb) thread continue 
Resuming thread 0x7d6334 in process 57161
Process 57161 resuming
Process 57161 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x000000010535db4d postgres`exec_simple_query(query_string="SELECT 1;") at postgres.c:994:21
   991 	static void
   992 	exec_simple_query(const char *query_string)
   993 	{
-> 994 		CommandDest dest = whereToSendOutput;
   995 		MemoryContext oldcontext;
   996 		List	   *parsetree_list;
   997 		ListCell   *parsetree_item;

バックトレースを確認すると,exec_simple_query 関数がどのように呼び出されているかを知ることができる.

(lldb) thread backtrace 
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x000000010535db4d postgres`exec_simple_query(query_string="SELECT 1;") at postgres.c:994:21
    frame #1: 0x000000010535d20a postgres`PostgresMain(dbname="test", username="sira") at postgres.c:4542:7
    frame #2: 0x0000000105266672 postgres`BackendRun(port=0x00007f9a8c504a30) at postmaster.c:4491:2
    frame #3: 0x0000000105265c5c postgres`BackendStartup(port=0x00007f9a8c504a30) at postmaster.c:4219:3
    frame #4: 0x0000000105264a8c postgres`ServerLoop at postmaster.c:1793:7
    frame #5: 0x000000010526235f postgres`PostmasterMain(argc=1, argv=0x00007f9a8c405e50) at postmaster.c:1465:11
    frame #6: 0x000000010513f10e postgres`main(argc=1, argv=0x00007f9a8c405e50) at main.c:202:3
    frame #7: 0x00007fff697fecc9 libdyld.dylib`start + 1

ステップ実行して,クライアントから送信したクエリがパースされるところまでを確認する.

(lldb) thread step-over
Process 57161 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x000000010535db56 postgres`exec_simple_query(query_string="SELECT 1;") at postgres.c:998:35
   995 		MemoryContext oldcontext;
   996 		List	   *parsetree_list;
   997 		ListCell   *parsetree_item;
-> 998 		bool		save_log_statement_stats = log_statement_stats;
   999 		bool		was_logged = false;
   1000		bool		use_implicit_block;
   1001		char		msec_str[32];
(lldb)  
Process 57161 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x000000010535db64 postgres`exec_simple_query(query_string="SELECT 1;") at postgres.c:999:8
   996 		List	   *parsetree_list;
   997 		ListCell   *parsetree_item;
   998 		bool		save_log_statement_stats = log_statement_stats;
-> 999 		bool		was_logged = false;
   1000		bool		use_implicit_block;
   1001		char		msec_str[32];
   1002	
(lldb)  
Process 57161 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x000000010535db68 postgres`exec_simple_query(query_string="SELECT 1;") at postgres.c:1006:23
   1003		/*
   1004		 * Report query to various monitoring facilities.
   1005		 */
-> 1006		debug_query_string = query_string;
   1007	
   1008		pgstat_report_activity(STATE_RUNNING, query_string);
   1009	
(lldb)  
Process 57161 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x000000010535db73 postgres`exec_simple_query(query_string="SELECT 1;") at postgres.c:1008:40
   1005		 */
   1006		debug_query_string = query_string;
   1007	
-> 1008		pgstat_report_activity(STATE_RUNNING, query_string);
   1009	
   1010		TRACE_POSTGRESQL_QUERY_START(query_string);
   1011	
(lldb)  
Process 57161 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x000000010535db81 postgres`exec_simple_query(query_string="SELECT 1;") at postgres.c:1010:2
   1007	
   1008		pgstat_report_activity(STATE_RUNNING, query_string);
   1009	
-> 1010		TRACE_POSTGRESQL_QUERY_START(query_string);
   1011	
   1012		/*
   1013		 * We use save_log_statement_stats so ShowUsage doesn't report incorrect
(lldb)  
Process 57161 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x000000010535db86 postgres`exec_simple_query(query_string="SELECT 1;") at postgres.c:1016:6
   1013		 * We use save_log_statement_stats so ShowUsage doesn't report incorrect
   1014		 * results because ResetUsage wasn't called.
   1015		 */
-> 1016		if (save_log_statement_stats)
   1017			ResetUsage();
   1018	
   1019		/*
(lldb)  
Process 57161 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x000000010535db95 postgres`exec_simple_query(query_string="SELECT 1;") at postgres.c:1026:2
   1023		 * one of those, else bad things will happen in xact.c. (Note that this
   1024		 * will normally change current memory context.)
   1025		 */
-> 1026		start_xact_command();
   1027	
   1028		/*
   1029		 * Zap any pre-existing unnamed statement.  (While not strictly necessary,
(lldb)  
Process 57161 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x000000010535db9a postgres`exec_simple_query(query_string="SELECT 1;") at postgres.c:1034:2
   1031		 * statement and portal; this ensures we recover any storage used by prior
   1032		 * unnamed operations.)
   1033		 */
-> 1034		drop_unnamed_stmt();
   1035	
   1036		/*
   1037		 * Switch to appropriate context for constructing parsetrees.
(lldb)  
Process 57161 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x000000010535db9f postgres`exec_simple_query(query_string="SELECT 1;") at postgres.c:1039:37
   1036		/*
   1037		 * Switch to appropriate context for constructing parsetrees.
   1038		 */
-> 1039		oldcontext = MemoryContextSwitchTo(MessageContext);
   1040	
   1041		/*
   1042		 * Do basic parsing of the query or queries (this should be safe even if
(lldb)  
Process 57161 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x000000010535dbb2 postgres`exec_simple_query(query_string="SELECT 1;") at postgres.c:1045:34
   1042		 * Do basic parsing of the query or queries (this should be safe even if
   1043		 * we are in aborted transaction state!)
   1044		 */
-> 1045		parsetree_list = pg_parse_query(query_string);
   1046	
   1047		/* Log immediately if dictated by log_statement */
   1048		if (check_log_statement(parsetree_list))

クライアントから送信した query_string="SELECT 1;"pg_parse_query という関数に渡されている. pg_parse_query 関数に入り,関数内の処理をすべて実行する.

(lldb) thread step-in
Process 57161 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step in
    frame #0: 0x000000010535a34c postgres`pg_parse_query(query_string="SELECT 1;") at postgres.c:596:2
   593 	{
   594 		List	   *raw_parsetree_list;
   595 	
-> 596 		TRACE_POSTGRESQL_QUERY_PARSE_START(query_string);
   597 	
   598 		if (log_parser_stats)
   599 			ResetUsage();
(lldb) thread step-out
Process 57161 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step out
Return value: (List *) $0 = 0x00007f9a8c80f3a8

    frame #0: 0x000000010535dbbb postgres`exec_simple_query(query_string="SELECT 1;") at postgres.c:1045:17
   1042		 * Do basic parsing of the query or queries (this should be safe even if
   1043		 * we are in aborted transaction state!)
   1044		 */
-> 1045		parsetree_list = pg_parse_query(query_string);
   1046	
   1047		/* Log immediately if dictated by log_statement */
   1048		if (check_log_statement(parsetree_list))

戻り値として 0x00007f9a8c80f3a8 という List 型のポインタを得,それが変数 parsetree_list に格納された.

現在のステップにおける変数の内容を確認する.

(lldb) frame variable 
(const char *) query_string = 0x00007f9a8c80e720 "SELECT 1;"
(CommandDest) dest = DestRemote
(MemoryContext) oldcontext = 0x00007f9a8c81a800
(List *) parsetree_list = 0x00007f9a8c80f3a8
(ListCell *) parsetree_item = 0x00007f9a8c80e720
(bool) save_log_statement_stats = false
(bool) was_logged = false
(bool) use_implicit_block = true
(char [32]) msec_str = "`#\xeb\xea\xfe\U0000007f"

parsetree_list の内容は次のように確認できる.

(lldb) frame variable *parsetree_list 
(List) *parsetree_list = {
  type = T_List
  length = 1
  max_length = 5
  elements = 0x00007f9a8c80f3c0
  initial_elements = {}
}

Postgres のソースコードはこのようにして読み進めていけばよい.

後始末は以下のとおり.

(lldb) continue
Process 57161 resuming
(lldb) detach
Process 57161 detached
(lldb) quit 
test=# \q