PostgreSQL 的文件格式

2020/12/31

doc

PostgreSQL 使用 “共享硬盘” 和 “共享内存” 的方式在单机上组成集群。尽管文档不断重复 “Cluster”,PostgreSQL 只能单机运行。

从文件层面 PostgreSQL 表现如下:

文件名作用
PG_VERSION版本号
base/NNN/PG_VERSION版本号
base/NNN/MMMNNN 库 MMM 表数据
base/NNN/MMM_vmNNN 库 MMM 表可见块
base/NNN/MMM_fsmNNN 库 MMM 表空闲块
base/NNN/MMM_initNNN 库 MMM 表初始化标记
current_logfiles日志文件当前写入
globalpg_global 库里的表类似 base/NNN
pg_commit_ts事务时间戳
pg_dynshmem动态共享内存
pg_multixact共享行锁
pg_notifypg_notify
pg_logical流复制相关,解码状态
pg_replslot流复制相关,复制槽
pg_serial事务相关
pg_snapshots已导出的快照
pg_stat统计信息
pg_stat_tmp临时统计信息
pg_subtrans子事务
pg_tblspc用于 tablespaces 的链接,指向 base/NNN
pg_twophasetwo phase commmit 相关
pg_walWAL/xlong 崩溃恢复相关
pg_xactxact/clog 事务信息
postgresql.auto.conf配置文件
postmaster.opts配置文件
postmaster.pid文件锁

对于 base/NNN 内的文件,文件名为表的 filenode。

后缀 _vm 为 VisibilityMap 后缀为 _fsm 为 FreeSapceMap 后缀为 _init 的为 InitFile。

当一个 PageFile 的大小突破 1G 时就会分裂成多个,命名规则为 NNN.1 NNN.2。

toast 数据与普通表无异。

PG_VERSION

一个字符串,对于 PostgreSQL 13 来说是 “13\n” (0x31330a)

PageFile

doc

每个 PageFile 存储一堆定长的 Page,长度编译时指定一般是 8KiB。

每个 Page 结构如下

ItemDescription
PageHeaderData24byte Header
ItemIdData数组,每个 item 对应 15bit offset + 2bit flag + 15 bit length = 32bit
Free space未分配的数据
Items实际数据
Special space特殊数据,普通表 0bit

所以 PostgreSQL 理论上支持的最大 Block Size 为 (64Ki - 1 ) Byte,实际上为 64KiB。

当 Page Size = 8KiB Tuple = 16Byte 时,Page 层面的空间利用率为 79.9%。

其中 PageHeaderData 如下

FieldTypeLengthDescription
pd_lsnXLogRecPtr8 bytesLSN: 修改此 Page 的 xlog 记录的下一个 byte
pd_tliuint162 bytes最后一次修改的 TimeLineID
pd_flagsuint162 bytesFlag bits
pd_lowerLocationIndex2 bytesfree space 开始的偏移量
pd_upperLocationIndex2 bytesfree space 结束的偏移量
pd_specialLocationIndex2 bytesspecial space 开始的偏移量
pd_pagesize_versionuint162 bytesPage size and layout version number information
pd_prune_xidTransactionId4 bytes最旧的以提交 xid (XMAX)

其中 pd_lower 为 ItemIdData 分配的开始,pd_upper 为 Items 分配的开始。

in page alloc

每个 Item (名为 HeapTupleHeaderData)格式如下

FieldTypeLengthDescription
t_xminTransactionId4 bytesinsert XID stamp
t_xmaxTransactionId4 bytesdelete XID stamp
t_cidCommandId4 bytessql 计数
t_xvacTransactionId4 bytesVACUUM 的 xid 和 t_cid 是个 union
t_ctidItemPointerData6 bytes行 ID
t_infomask2uint162 bytesnumber of attributes, flag
t_infomaskuint162 bytesflag
t_hoffuint81 byte用户数据偏移量

ctid = Block Number 4 byte + Offset Number 2 byte

PostgreSQL 为何又存储了一遍 Block ID? + 4 byte

Header 有 23 byte 大。如果只存两个 i64(16byte) 的话,空间利用率为 41%.

Timescale 是认真的么?

VisibilityMap

vm

PostgreSQL 没有说明文件格式,只给了一个小工具说明这个文件工作原理。

一个简单地无 Header 文件,使用一个简单地公式定位

#define HEAPBLK_TO_MAPBLOCK(x) ((x) / HEAPBLOCKS_PER_PAGE)
#define HEAPBLK_TO_MAPBYTE(x) (((x) % HEAPBLOCKS_PER_PAGE) / HEAPBLOCKS_PER_BYTE)
#define HEAPBLK_TO_OFFSET(x) (((x) % HEAPBLOCKS_PER_BYTE) * BITS_PER_HEAPBLOCK)

每个 Page 需要至少 2bit 来放 flag

不知道可见和冻结是什么意思。

当这个 page 插入第一个数据时 VISIBILITYMAP_ALL_VISIBLE 被清除

需要看一下 vacuum lazy 如何实现的。

FreeSapceMap

fsm

doc

负责快速找到一个空闲 Page。整体是一个二叉树。

每个 Page 对应 1 byte 空间,存 255 灰度图。

每个 FSM Page 是一个定长的二叉堆,大小与 Block Size 相同(包括 Block Header),默认为 8KiB。

树符合

$$ value = \max (left, right) $$

For example:

    4
 4     2
3 4   0 2    <- This level represents heap pages. 叶子节点保存 Block 的序号

整个 FSM 文件使用类似的树形结构。使用定高树,高度为 3。

分配方式为 $$y = n + (n / F + 1) + (n / F^2 + 1) + 1$$

$F$ 为每个 FMS Page 保存的 Block 数量

$n$ 为要寻找的 Block 序号

$y$ 为计算出的 FMS Page 序号

每次插入分配新空间时需要 3 次 pread 最坏情况一次全局锁的 pwrite,一次 8KiB O(lgN)

每个数据文件大于 1G 时会分裂,3 层的定高树足够用了。

InitFile

init

只是一个标记,当存在空表时 PostgreSQL crash 用这个标记来恢复。

WAL

WAL 与 XLOG 是一个东西。文档不清晰,要翻代码。

WAL 固定大小,16MiB 一个文件,文件依旧无头。

使用的概念上对于一个 TimeLineID 有一个无限长的文件,使用 64bit 的 offset 访问。

文件名格式为 %08X%08X%08X

创建或打开文件在 XLogFileInit 中,会循环调用 pwrite 写出一个 16MiB 的文件….

后续的写入和读取均使用 pread pwrite,没有用 smgr 的抽象,也都是不定长的随机 IO。

文件读取用 XLogReaderRoutine 里的虚函数 XLogPageRead 调用 pread 直接读。

写入从共享内存进入 XLogBackgroundFlush 里的 XLogWrite 里的 pwrite 直接写入。

XLog 有 Block Header Magic,默认情况同样为 8KiB XLOG_BLCKSZ。

引入 Block 大概是为了保存完整性。

XLog 格式大致为 [Header [XLog, XLog]] 每个都是不定长的。解析函数为 DecodeXLogRecord 组装函数为 XLogRecordAssemble

有空看完 doc

XACT

XACT 是事务可见性文件。

PostgreSQL 中每个事务会有一个 ID,这个 ID 对应定长的事务状态。

PostgreSQL 可以直接根据 ID 算出 offset 然后访问文件来判断事务是否完成。