バッファ管理。タプルの追加

シンプルなINSERTを行ったときの呼び出しシーケンス。ページにタプルを追加するところまで。

(gdb) bt
#0  PageAddItem (page=0xb5835260 "", item=0x84e4d04 "J\002", size=28, offsetNumber=3, overwrite=0 '\0', is_heap=1 '\001') at bufpage.c:249
#1  0x080a2cf8 in RelationPutHeapTuple (relation=0xb561a2f8, buffer=101, tuple=0x84e4cf0) at hio.c:43
#2  0x0809dbb8 in heap_insert (relation=0xb561a2f8, tup=0x84e4cf0, cid=2, use_wal=1 '\001', use_fsm=1 '\001') at heapam.c:1841
#3  0x0818d787 in ExecInsert (slot=0x84e4960, tupleid=0x0, planSlot=0x84e4960, dest=0x84ad820, estate=0x84e4848) at execMain.c:1632
#4  0x0818d500 in ExecutePlan (estate=0x84e4848, planstate=0x84e4a20, operation=CMD_INSERT, numberTuples=0, direction=ForwardScanDirection, dest=0x84ad820) at execMain.c:1485
#5  0x0818b98c in ExecutorRun (queryDesc=0x84cbdb0, direction=ForwardScanDirection, count=0) at execMain.c:270
#6  0x0824ddce in ProcessQuery (plan=0x84ad7b0, params=0x0, dest=0x84ad820, completionTag=0xbfc4d8da "") at pquery.c:179
#7  0x0824f248 in PortalRunMulti (portal=0x84de3f8, isTopLevel=1 '\001', dest=0x84ad820, altdest=0x84ad820, completionTag=0xbfc4d8da "") at pquery.c:1242
#8  0x0824ea27 in PortalRun (portal=0x84de3f8, count=2147483647, isTopLevel=1 '\001', dest=0x84ad820, altdest=0x84ad820, completionTag=0xbfc4d8da "") at pquery.c:813
#9  0x0824939c in exec_simple_query (query_string=0x84ac870 "insert into ccc values(10);") at postgres.c:986
#10 0x0824d15f in PostgresMain (argc=4, argv=0x8454520, username=0x84544f8 "postgres") at postgres.c:3572
#11 0x08217982 in BackendRun (port=0x8467d18) at postmaster.c:3207
#12 0x08216f0a in BackendStartup (port=0x8467d18) at postmaster.c:2830
#13 0x08214928 in ServerLoop () at postmaster.c:1274
#14 0x08214335 in PostmasterMain (argc=1, argv=0x8451578) at postmaster.c:1029
#15 0x081b6707 in main (argc=1, argv=0x8451578) at main.c:188

heap_insert()

注目したい関数はheap_insert()。タプルを追加する処理を行う。
ページに追加する前に次のようにMVCCに必要な情報などを付け加えている。

tup->t_data->t_infomask &= ~(HEAP_XACT_MASK);
tup->t_data->t_infomask2 &= ~(HEAP2_XACT_MASK);
tup->t_data->t_infomask |= HEAP_XMAX_INVALID;
HeapTupleHeaderSetXmin(tup->t_data, xid);
HeapTupleHeaderSetCmin(tup->t_data, cid);
HeapTupleHeaderSetXmax(tup->t_data, 0);     /* for cleanliness */
tup->t_tableOid = RelationGetRelid(relation);

WALのログの作成も行っている。

以下は、heap_insert()から呼ばれる主な関数。

  • RelationGetBufferForTuple()
    • タプルを追加するためのバッファを取得する。
  • RelationPutHeapTuple()
    • 取得したバッファにタプルを追加する。
  • MarkBufferDirty()
    • バッファのフレーム(バッファ記述子)にダーティだとしるしをつける。
  • XLogInsert()
    • WALバッファにログを書き込む。
  • PageSetLSN()
    • ページにLSNをセットする。このLSNはXLogInsertの戻り値。
  • PageSetTLI()
    • ページにタイムラインIDをセットする。
  • CacheInvalidateHeapTuple()
    • キャッシュを無効にする。

宿題

  • タプル追加のためにバッファを取得するロジックはどうなっている?
  • ログの情報はどのようなもの?
  • LSNとは何か?その役割は?
  • タイムラインIDとは何か?
  • キャッシュを無効にすると際のロジックはどうなっている?

リカバリ。WAL(Write Ahead Log)

WALとCLOG

ここ2、3日、WALとCLOGのどちらへの書き込みが終わったらトランザクションの持続性が確保されるのか?と悩んでいた。
結論としては、WAL。WALへの書き込みはfdatasync()で同期書き込みされるけど、CLOGへの書き込みは非同期だから(チェックポイントで同期されるから)。何で悩んだかというと、WALに同期書き込みして、CLOGへも同期書き込みして、それで完了という流れかもと思っていたから。

WALの書き込みは、Linuxの場合はfdatasync()で行われる。だからfdatasync()が返ってきた時点で持続性が確保されたといえる。以下は、コミット時の関数呼び出し順序をGDBで取得。fdatasync()はpg_fdatasync()の中で呼ばれる。

(gdb) bt
#0  pg_fdatasync (fd=36) at fd.c:299
#1  0x080c8bcf in issue_xlog_fsync () at xlog.c:6392
#2  0x080beeb4 in XLogWrite (WriteRqst={Write = {xlogid = 0, xrecoff = 6281584}, Flush = {xlogid = 0, xrecoff = 6281584}}, flexible=0 '\0', xlog_switch=0 '\0') at xlog.c:1614
#3  0x080bf2e9 in XLogFlush (record={xlogid = 0, xrecoff = 6281584}) at xlog.c:1741
#4  0x080b8a1c in RecordTransactionCommit () at xact.c:949
#5  0x080b93dd in CommitTransaction () at xact.c:1675
#6  0x080b9d3a in CommitTransactionCommand () at xact.c:2274
#7  0x0824b647 in finish_xact_command () at postgres.c:2322
#8  0x082493df in exec_simple_query (query_string=0x84ac870 "create table abc(a int);") at postgres.c:1017
#9  0x0824d15f in PostgresMain (argc=4, argv=0x8454520, username=0x84544f8 "postgres") at postgres.c:3572
#10 0x08217982 in BackendRun (port=0x8467d18) at postmaster.c:3207
#11 0x08216f0a in BackendStartup (port=0x8467d18) at postmaster.c:2830
#12 0x08214928 in ServerLoop () at postmaster.c:1274
#13 0x08214335 in PostmasterMain (argc=1, argv=0x8451578) at postmaster.c:1029
#14 0x081b6707 in main (argc=1, argv=0x8451578) at main.c:188

WALに書き込むには、記録すべき処理を行うところでXLogInsert()でまずWALバッファに書き、その後、確定するところでXLogFlush()をしバッファからディスクに書き込むという手順のよう。

  • しかし、新たな疑問。
    • CLOGは何のためにあるのか?単に古いWALを切り落とすための情報として使うだけ?
    • MVCCでトランザクションの可視性チェックでCLOGを参照する場合があるのはなぜ?

RM(Resource Manager)

PostgreSQL内のサブシステム?としてRMというのがある。RMの種類には次のようなものがある。

/*
 * Built-in resource managers
 *
 * Note: RM_MAX_ID could be as much as 255 without breaking the XLOG file
 * format, but we keep it small to minimize the size of RmgrTable[].
 */
#define RM_XLOG_ID              0
#define RM_XACT_ID              1
#define RM_SMGR_ID              2
#define RM_CLOG_ID              3
#define RM_DBASE_ID             4
#define RM_TBLSPC_ID            5
#define RM_MULTIXACT_ID         6
#define RM_HEAP2_ID             9
#define RM_HEAP_ID              10
#define RM_BTREE_ID             11
#define RM_HASH_ID              12
#define RM_GIN_ID               13
#define RM_GIST_ID              14
#define RM_SEQ_ID               15
#define RM_MAX_ID               RM_SEQ_ID

WALに書かれるものは、レコードのコミット情報だけだと思い込んでいたけど、実はRMごとにWALに書き出す。
たとえば、行を追加したり削除したりするときは「RM_HEAP_ID」のデータがWALに書き込まれ、CREATE TABLEしたときは「RM_SMGR_ID」がWALに書き込まれる。トランザクション関係は「RM_XACT_ID」。
何が行われたか記憶しておかなければいけないわけだから、レコードのコミット情報以外も記録しておくのは当たり前かも。

リカバリもRMごとに用意された処理が実行される。