この度、データエンジニアとして新しい仕事を始めることになった。色々と学ぶべきことが多く、有給消化期間中に何を勉強するか非常に迷ったが、あらゆるデータ基盤製品の基礎となっているPostgresをしっかり学ぶことを選んだ。理由なのだが
- MVCC、他のデータ基盤製品にも共通する概念が多く詰まっており、レバレッジが利きやすい
- 現在OSS RDBMSの主流の製品として広く使われており、Postgres自体の知識も生きやすい
- C言語を使った低レイヤの操作に対して理解することで、コンピューターアーキテクチャーへの理解も深まりやすい
早速わたしが何をやったか、どのような手順で実行したかを記していきたい
C言語の学習兼ミニRDBMS実装
こちらにリポジトリを公開している。https://github.com/wara714/my_pg
PostgresはCで書かれているので、この言語を読めなければ、Postgresの中身を理解することは出来ない。
ただ、全く全部の文法を覚える必要はなくて、下記が理解できれば十分と考える
- for, if, whileの実装
- 関数の実装
- 変数・構造体の定義方法
- 関数ポインタ(関数のポインタを構造体に含めることで、クラスメソッドのような形で、振る舞いを定義できる)
- typedefの定義
- ポインタと、ポインタを使用した関数の定義(特に A->Bのような書き方が多用される)
- ビットの操作
Postgresは、いかにメモリの無駄をなくし、高速でデータにアクセスするかが求められているので、コアな実装部分はメモリの操作になってくる。そのため、ポインタを使用して、データが格納されているメモリアドレスに直接アクセスするなどの操作が必要になる。よって、ポインタの概念は超重要と捉えていただきたい。
勉強方法なのだが、下記の順番で実装をして、その中で学んでいった
- mini Postgres
- ユーザーがコマンドを叩いてプログラムと対話するための機能( postgres> だけが表示されている画面)
- コマンド解析機能
- 構造体にデータを保持させる機能
- 構造体にinsertする機能(idとvalueのみ)
- insertしたデータをselectする機能
- データをファイルに保存する機能
- B木の挿入をテストするプログラム
- ビット操作をテストするプログラム
ポイントとしては、AIの活用である。と言ってもAIにぶん投げて全部書かせるのでは意味がない。わたしは「具体的な実装はこちらでやるので、答えを出さずに、ヒントと調べるためのキーワードだけを教えて」とプロンプトで入力していた。やはり自分の力で実装する時間は一番力が伸びるので、おすすめである。
Postgresコードの読解
※下記のコード部分は、postgresのコミットハッシュ4bfd0f1b769eca345072ea4da646a0d46a49960c時点のコードに基づいている。
Postgresは膨大なコードを持っており、これを片っ端から全部読むのは不可能である。おすすめは、ユーザーに可能な限り近いところから、メモリに一番近いところまでの最短経路を読みに行くこと。即ち、①ユーザーがSQLを入力する→②構文解析して計画を立てる→③Executor(SQLの実行プログラム)が起動する→④メモリへの書き込み の③④だけを読みに行くということである。まずは③④に絞ることで、全体像を効率よく掴める。
ここだけ取り出すのであれば実は非常に読む箇所は少ないのである。読むコードは下記になる。
- src/backend/executor/nodeModifyTable.cのExecInsert関数→1262行目付近の下記コード(2のtableam.hの関数を参照している)
/* insert the tuple normally */
table_tuple_insert(resultRelationDesc, slot,
estate->es_output_cid,
0, NULL);
- src/include/access/tableam.h 1387行目付近の下記コード(関数ポインタで定義されたtuple_insertを参照している)
static inline void
table_tuple_insert(Relation rel, TupleTableSlot *slot, CommandId cid,
int options, BulkInsertStateData *bistate)
{
rel->rd_tableam->tuple_insert(rel, slot, cid, options,
bistate);
}
- src/include/access/tableam.h 510行目付近の下記コード(TableAmRoutine構造体の定義)
void (*tuple_insert) (Relation rel, TupleTableSlot *slot,
CommandId cid, int options,
BulkInsertStateData *bistate);
- src/backend/access/heap/heapam_handler.c 2654行目付近の下記コード(TableAmRoutine構造体を返す、heapam_methods関数の定義。ここの、tuple_insertというパラメーターには、heapam_tuple_insertという関数の定義をそのまま入れている。これを3で参照しているという形である)
.tuple_insert = heapam_tuple_insert,
5.src/backend/access/heap/heapam_handler.c 243行目付近の下記コード(heapam_tuple_insertの中身)
static void
heapam_tuple_insert(Relation relation, TupleTableSlot *slot, CommandId cid,
int options, BulkInsertState bistate)
{
bool shouldFree = true;
HeapTuple tuple = ExecFetchSlotHeapTuple(slot, true, &shouldFree);
/* Update the tuple with table oid */
slot->tts_tableOid = RelationGetRelid(relation);
tuple->t_tableOid = slot->tts_tableOid;
/* Perform the insertion, and copy the resulting ItemPointer */
heap_insert(relation, tuple, cid, options, bistate);
ItemPointerCopy(&tuple->t_self, &slot->tts_tid);
if (shouldFree)
pfree(tuple);
}
6.src/backend/access/heap/heapam.cの2194行目(バッファキャッシュにタプルを書き込む。実際のディスク書き込みは、WALとチェックポイントを通じて非同期に行われる。ちなみにこの下のコード部分ではXLogxxxという関数を多数実行しているが、ここはWALのバッファへの書き込みをしているようである)
RelationPutHeapTuple(relation, buffer, heaptup,
(options & HEAP_INSERT_SPECULATIVE) != 0);
7.src/backend/access/heap/hio.cの61行目(いよいよ !!! EREPORT(ERROR) IS DISALLOWED HERE !!! Must PANIC on failure!!! みたいなコメントも出てきて、重要度が露骨に出てきた。PageAddItemでデータの書き込みを行っている。)
offnum = PageAddItem(pageHeader, tuple->t_data, tuple->t_len, InvalidOffsetNumber, false, true);
8.src/include/storage/bufpage.hの478行目(PageAddItemの定義。別の関数の定義を流用しているようである)
#define PageAddItem(page, item, size, offsetNumber, overwrite, is_heap) \
PageAddItemExtended(page, item, size, offsetNumber, \
((overwrite) ? PAI_OVERWRITE : 0) | \
((is_heap) ? PAI_IS_HEAP : 0))
9.src/backend/storage/page/bufpage.cの193行目(ようやくたどり着いたが、ここがメモリ書き込みの実体の一つである。memcpyでメモリにデータを書き込んでいる)
memcpy((char *) page + upper, item, size);
...というかなり長い道のりであったが、メモリへの書き込みまでを見ることが出来た。このあとも更に処理工程が色々続くのと、条件分岐や付随機能など色々飛ばしてしまったが、ユーザーが操作しているレイヤから低レイヤへの橋渡し部分が見られたので、一旦良いであろう。ここから肉付けとして別の分岐等を読んでいく所存だ。個人的に面白かった発見としては、データというのは特定の構造体で定義されたパラメーターに入れるのではなくて、固定構造体の後ろに可変長データを詰めるレイアウトになっているということ。低レイヤならではという挙動で非常に面白かった。
他にも色々読まなければならないところは多いため、なにか発見があれば投稿していく。
まだまだ知識が浅いので間違っている部分等あればご指摘いただけると幸いである。