PostgreSQL ソースコードリーディング
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 の基礎知識
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 のインストール先のディレクトリとデータベースクラスタのディレクトリは以下のように設定する.
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
以上で,データベースサーバーの準備が整った.
クライアントプログラム psql
12 を使って作成したデータベース 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
-
PostgreSQL: Documentation: devel: 68.6. Database Page Layout ↩
-
PostgreSQL: Documentation: devel: 16.2. Requirements ↩ ↩2 ↩3
-
PostgreSQL: Documentation: devel: 18.2. Creating a Database Cluster ↩
-
PostgreSQL: Documentation: devel: 16.7. Platform-Specific Notes ↩
-
Documentation: devel: createdb. https://www.postgresql.org/docs/devel/app-createdb.html ↩
-
データベース名を省略した場合は,ユーザー名がデータベース名として利用される. ↩