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

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

ご意見等がございましたら,@y_sira までお願いします.

前提知識

PostgreSQL を理解するためには,まず,リレーショナルデータベースの仕組みを理解する必要がある. 日本語であれば,お茶の水大学の増永良文氏による書籍リレーショナルデータベース入門が初学者に適している. リレーショナルデータベースで使われている要素技術の詳細については,日本語ではカバーしきれないため洋書を読むことになる. PostgreSQL の公式 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

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

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

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

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

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

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

前提

PostgreSQL のビルドには,以下のツールが必要になる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.2
  • Clang 9.0.0
  • GNU Make 4.2.1
  • GNU Readline 8.0.1
  • zlib 1.2.11
  • Git 2.24.1
  • Flex 2.6.4
  • Bison 3.5
  • Perl 5.30.0

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

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

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

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

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

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

ソースコードは 公式の Git リポジトリ から取得する. また,Git のタグで最新の安定版リリースである PostgreSQL 12.1 のソースコードに切り替える.

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

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

リグレッションテスト

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

make check

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

$ make check

(snipped)

=======================
 All 192 tests passed. 
=======================

make[1]: Leaving directory '/Users/sira/Projects/postgres/build/src/test/regress'

インストール

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

make install

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

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

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

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

データベースクラスタの初期化は 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 locale "en_US.UTF-8".
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".

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
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:

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

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

$ pg_ctl start
waiting for server to start....2019-12-18 03:53:48.123 JST [19808] LOG:  starting PostgreSQL 12.1 on x86_64-apple-darwin19.2.0, compiled by clang version 9.0.0 (tags/RELEASE_900/final), 64-bit
2019-12-18 03:53:48.131 JST [19808] LOG:  listening on IPv6 address "::1", port 5432
2019-12-18 03:53:48.132 JST [19808] LOG:  listening on IPv4 address "127.0.0.1", port 5432
2019-12-18 03:53:48.139 JST [19808] LOG:  listening on Unix socket "/tmp/.s.PGSQL.5432"
2019-12-18 03:53:49.065 JST [19890] LOG:  database system was shut down at 2019-12-18 03:47:18 JST
.2019-12-18 03:53:49.121 JST [19808] LOG:  database system is ready to accept connections
 done
server started

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

createdb test

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

$ psql test
psql (12.1)
Type "help" for help.

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

test=# 

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

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

$ pg_ctl stop  
waiting for server to shut down...2019-12-18 04:07:39.417 JST [30171] LOG:  received fast shutdown request
.2019-12-18 04:07:39.421 JST [30171] LOG:  aborting any active transactions
2019-12-18 04:07:39.422 JST [30171] LOG:  background worker "logical replication launcher" (PID 30227) exited with exit code 1
2019-12-18 04:07:39.422 JST [30222] LOG:  shutting down
2019-12-18 04:07:39.522 JST [30171] LOG:  database system is shut down
 done
server stopped

データベースの削除・データベースクラスタの削除・ビルド成果物の削除・PostgreSQL のアンインストール

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

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

dropdb test

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

rm -rf $PGDATA

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

make uninstall

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

make clean

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

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

エディタの設定

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

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

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

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

$ psql test
psql (12.1)
Type "help" for help.

test=#

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

$ ps ax | grep postgres
 1065   ??  Ss     0:00.14 /Users/sira/.local/pgsql/bin/postgres
 1084   ??  Ss     0:00.01 postgres: checkpointer   
 1087   ??  Ss     0:00.03 postgres: background writer   
 1088   ??  Ss     0:00.02 postgres: walwriter   
 1089   ??  Ss     0:00.67 postgres: autovacuum launcher   
 1090   ??  Ss     0:00.85 postgres: stats collector   
 1092   ??  Ss     0:00.02 postgres: logical replication launcher   
 1527   ??  TXs    0:00.16 postgres: sira test [local] idle
50921 s003  R+     0:00.01 grep --color=auto postgres

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

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

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

$ /usr/local/opt/llvm/bin/lldb
(lldb) process attach -p 1527
Process 1527 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
    frame #0: 0x00007fff70509896 libsystem_kernel.dylib`poll + 10
libsystem_kernel.dylib`poll:
->  0x7fff70509896 <+10>: jae    0x7fff705098a0            ; <+20>
    0x7fff70509898 <+12>: movq   %rax, %rdi
    0x7fff7050989b <+15>: jmp    0x7fff7050468d            ; cerror
    0x7fff705098a0 <+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 0xb858c6 in process 1527
Process 1527 resuming
Process 1527 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000107b9aa8b postgres`exec_simple_query(query_string="SELECT 1;") at postgres.c:986:21
   983 	static void
   984 	exec_simple_query(const char *query_string)
   985 	{
-> 986 		CommandDest dest = whereToSendOutput;
   987 		MemoryContext oldcontext;
   988 		List	   *parsetree_list;
   989 		ListCell   *parsetree_item;

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

(lldb) thread backtrace 
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000107b9aa8b postgres`exec_simple_query(query_string="SELECT 1;") at postgres.c:986:21
    frame #1: 0x0000000107b9a198 postgres`PostgresMain(argc=1, argv=0x00007fb94e813328, dbname="test", username="sira") at postgres.c:4236:7
    frame #2: 0x0000000107ad0c80 postgres`BackendRun(port=0x00007fb93e600440) at postmaster.c:4437:2
    frame #3: 0x0000000107ad00c3 postgres`BackendStartup(port=0x00007fb93e600440) at postmaster.c:4128:3
    frame #4: 0x0000000107acf02a postgres`ServerLoop at postmaster.c:1704:7
    frame #5: 0x0000000107acc9d5 postgres`PostmasterMain(argc=1, argv=0x00007fb94e401cf0) at postmaster.c:1377:11
    frame #6: 0x00000001079cf209 postgres`main(argc=1, argv=0x00007fb94e401cf0) at main.c:228:3
    frame #7: 0x00007fff703c27fd libdyld.dylib`start + 1

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

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

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

(lldb) thread step-in
Process 41313 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step in
    frame #0: 0x0000000107b97743 postgres`pg_parse_query(query_string="SELECT 1;") at postgres.c:638:6
   635 	
   636 		TRACE_POSTGRESQL_QUERY_PARSE_START(query_string);
   637 	
-> 638 		if (log_parser_stats)
   639 			ResetUsage();
   640 	
   641 		raw_parsetree_list = raw_parser(query_string);
(lldb) thread step-out
Process 41313 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step out

Return value: (List *) $0 = 0x00007fb94e807ba8

    frame #0: 0x0000000107b9aaf9 postgres`exec_simple_query(query_string="SELECT 1;") at postgres.c:1037:17
   1034		 * Do basic parsing of the query or queries (this should be safe even if
   1035		 * we are in aborted transaction state!)
   1036		 */
-> 1037		parsetree_list = pg_parse_query(query_string);
   1038	
   1039		/* Log immediately if dictated by log_statement */
   1040		if (check_log_statement(parsetree_list))
(lldb) thread step-over
Process 1527 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x0000000107b9ab00 postgres`exec_simple_query(query_string="SELECT 1;") at postgres.c:1040:26
   1037		parsetree_list = pg_parse_query(query_string);
   1038	
   1039		/* Log immediately if dictated by log_statement */
-> 1040		if (check_log_statement(parsetree_list))
   1041		{
   1042			ereport(LOG,
   1043					(errmsg("statement: %s", query_string),

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

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

(lldb) frame variable 
(const char *) query_string = 0x00007fb94e806f18 "SELECT 1;"
(CommandDest) dest = DestRemote
(MemoryContext) oldcontext = 0x00007fb94e834e00
(List *) parsetree_list = 0x00007fb94e807ba8
(ListCell *) parsetree_item = 0x0000000107d7bf6f
(bool) save_log_statement_stats = false
(bool) was_logged = false
(bool) use_implicit_block = true
(char [32]) msec_str = "\x80\"Y??"

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

(lldb) frame variable *parsetree_list 
(List) *parsetree_list = {
  type = T_List
  length = 1
  head = 0x00007fb94e807b80
  tail = 0x00007fb94e807b80
}

長くなってしまったので,今日はここまで.

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