纲要
PostgreSQL文件系统基础- 不支持裸设备(Raw Device)
- 支持的文件系统类型:
XFS、EXT3、EXT4、ZFS
- I/O 模型:Buffer I/O 与 Direct I/O
Buffer I/O(缓冲I/O):数据必经OS Page CacheDirect I/O(直接I/O):绕过OS Page CacheDouble Buffering(双缓存)问题PostgreSQL各版本的 I/O 演进:debug_io_direct(v16)、ReadStream(v17)、AIO+io_method(v18)
- 数据页(Data Page)结构
- 默认页大小:
8KB - 页布局:
PageHeaderData(24字节)→ItemIdData(行指针数组)→Free Space→Items(元组数据)→Special Space - 页头关键字段:
pd_lower、pd_upper、pd_special、pd_pagesize_version
- 默认页大小:
- 元组(Tuple)结构
HeapTupleHeaderData:t_xmin、t_xmax、t_ctid、t_infomask、t_infomask2、t_hoffMVCC可见性判断依赖t_xmin/t_xmaxt_ctid:元组的物理位置标识,索引扫描的核心依据t_infomask:元组属性标志位
- 数据页大小对性能的影响
OLTP场景:小数据页(如4KB)更优OLAP场景:大数据页(如16KB、32KB)更优
- 底层观测工具:
pageinspect扩展
PostgreSQL 文件系统基础
PostgreSQL 不支持裸设备(Raw Device) ,所有数据必须运行于具体的文件系统之上。这一设计决策意味着 PostgreSQL 依赖底层文件系统来管理数据的物理存储。常见的文件系统包括 XFS、EXT3、EXT4、ZFS 等,不同文件系统在批量加载、初始化耗时等场景下各有优劣。
由于 PostgreSQL 运行于文件系统之上,其 I/O 路径必然经过操作系统层面的文件系统缓存(OS Page Cache)。这是理解 PostgreSQL I/O 模型的核心前提。
Buffer I/O 与 Direct I/O
Buffer I/O(缓冲I/O)
PostgreSQL 当前(截至 v17)默认采用 Buffer I/O 模型。当数据库需要读取数据块时,I/O 路径如下:
这一模型意味着数据块可能同时存在于 PostgreSQL 的 shared_buffers 和 操作系统的 Page Cache 中。这种现象被称为 Double Buffering(双缓存) 。
双缓存带来的问题包括:
- 内存浪费:同一份数据在内存中存储两份副本
- 缓存管理复杂:操作系统刷脏页(Dirty Page)时可能影响数据库性能
- 内存压力下的不确定性:操作系统内存回收机制可能影响数据库缓存命中率
Direct I/O(直接I/O)
与 Buffer I/O 相对的是 Direct I/O,数据读写绕过 OS Page Cache,直接与存储设备交互。
Direct I/O 的优势在于消除双缓存,使内存管理更加高效。但 PostgreSQL 长期以来未采用 Direct I/O,主要原因是 OS Page Cache 在某些场景下提供了额外的保护层——例如数据库重启后,数据仍可能从 OS Page Cache 中快速读取,避免昂贵的磁盘 I/O。
PostgreSQL I/O 模型的演进
PostgreSQL 社区一直在 I/O 模型上进行持续迭代:
| 版本 | I/O 相关特性 | 说明 |
|---|---|---|
| v16 | debug_io_direct | 实验性 Direct I/O 支持,仅限开发测试 |
| v17 | ReadStream、向量化 I/O | 内部 API 重构,为 AIO 铺路 |
| v18 | AIO + io_method | 异步 I/O 子系统,支持 io_uring |
在 v16 中引入的 debug_io_direct 参数是一个开发者导向的实验性功能,其名称中的 debug_ 前缀意在警示用户不应在生产环境启用。该参数可设置为 wal、data 等值,分别控制 WAL 和数据文件的 Direct I/O 行为。
v17 引入的 ReadStream 框架实现了顺序扫描和 ANALYZE 的流式读取,属于内部 API 层面的重构。
v18 正式引入了异步 I/O(AIO)子系统,通过 io_method 参数控制:
worker:使用后台工作进程执行异步 I/O(默认)io_uring:使用 Linuxio_uring接口sync:同步 I/O(v18 之前的行为)
AIO 在 v18 中支持顺序扫描、位图堆扫描和 VACUUM 操作。当 AIO 与 Direct I/O 结合使用时,PostgreSQL 可直接与存储接口交互,充分发挥硬件传输能力。
数据页(Data Page)结构
页大小
PostgreSQL 默认数据页大小为 8KB,该值在编译时确定。编译时可选的页大小包括 1KB、2KB、4KB、8KB、16KB、32KB。
页布局
每个数据页包含五个部分:
| 组成部分 | 说明 |
|---|---|
PageHeaderData | 页头,24 字节,包含页的通用信息 |
ItemIdData | 行指针(Line Pointer)数组,每个 4 字节,指向实际元组 |
Free Space | 未分配的空闲空间 |
Items | 实际元组(Tuple)数据 |
Special Space | 索引访问方法专用空间,普通表为空 |
数据页的布局示意图:
关键设计:行指针从页头向后增长,元组数据从页尾向前增长。空闲空间位于两者之间。这种设计使得插入新元组时无需移动已有数据,只需在页头追加行指针、在页尾追加元组数据即可。
页头(PageHeaderData)
页头结构定义在 src/include/storage/bufpage.h 中,关键字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
pd_lsn | PageXLogRecPtr | 该页最近一次 WAL 记录的 LSN |
pd_checksum | uint16 | 页校验和(若启用数据校验) |
pd_flags | uint16 | 标志位 |
pd_lower | LocationIndex | 空闲空间起始偏移量 |
pd_upper | LocationIndex | 空闲空间结束偏移量 |
pd_special | LocationIndex | Special Space 起始偏移量 |
pd_pagesize_version | uint16 | 页大小和版本号 |
pd_prune_xid | TransactionId | 最旧未清理的 XMAX |
通过 pageinspect 扩展的 page_header() 函数可查看页头信息:
-- 创建测试表
CREATE TABLE test_page (id int, name text);
-- 插入测试数据
INSERT INTO test_page VALUES (1, 'hello'), (2, 'world');
-- 查看第 0 号数据页的页头
SELECT * FROM page_header(get_raw_page('test_page', 0));
行指针(ItemIdData)
每个行指针占 4 字节,包含元组的偏移量(offset)和长度(length)。行指针从页头区域向后分配,每个新插入的元组对应一个新的行指针。
元组(Tuple)结构
元组头(HeapTupleHeaderData)
每一行数据(元组)在页内存储时,前面附带一个元组头。元组头定义在 src/include/access/htup_details.h 中,关键字段包括:
| 字段 | 类型 | 说明 |
|---|---|---|
t_xmin | TransactionId | 插入该元组的事务 ID |
t_xmax | TransactionId | 删除/更新该元组的事务 ID |
t_ctid | ItemPointerData | 当前元组或新版本元组的物理位置 |
t_infomask | uint16 | 元组属性标志位 |
t_infomask2 | uint16 | 扩展标志位和属性数量 |
t_hoff | uint8 | 元组头长度(含可能的空值位图等) |
元组头通常占用 23 字节(多数架构上),之后是可选的空值位图(Null Bitmap)、可选的 OID 字段,最后是实际用户数据。
关键字段详解
t_xmin 与 t_xmax:PostgreSQL 的 MVCC 机制依赖这两个字段判断事务可见性。插入时设置 t_xmin 为当前事务 ID;删除或更新时,旧元组的 t_xmax 设置为当前事务 ID,同时插入新版本元组。
t_ctid:元组的物理位置标识符,格式为 (block_number, item_index)。例如 (0,1) 表示第 0 个数据页的第 1 个行指针。索引中存储的就是索引键值加对应的 t_ctid,索引扫描时通过 t_ctid 直接定位到数据页中的具体元组。
t_infomask:包含 16 种状态标志位,用于标识元组的各种属性。例如:
- 是否有空值字段
- 是否包含变长字段
t_xmin是否已提交/中止t_xmax是否已提交/中止
通过 pageinspect 观测元组
-- 查看堆页中的所有行指针和元组信息
SELECT lp, lp_off, lp_len, t_xmin, t_xmax, t_ctid, t_infomask, t_infomask2
FROM heap_page_items(get_raw_page('test_page', 0));
heap_page_items() 返回每一行的行指针信息(lp、lp_off、lp_len)和元组头信息(t_xmin、t_xmax、t_ctid、t_infomask 等)。
t_infomask 的二进制位可通过 pageinspect 的辅助函数解码:
-- 解码 infomask 标志位(需 PostgreSQL 17+)
SELECT t_infomask, t_infomask2,
pg_catalog.pg_decode_tuple_infomask(t_infomask) AS infomask_decoded
FROM heap_page_items(get_raw_page('test_page', 0));
数据页大小与性能
PostgreSQL 数据页大小(BLCKSZ)在编译时设定,默认 8KB。不同工作负载下,不同的页大小表现各异:
| 工作负载类型 | 推荐页大小 | 原因 |
|---|---|---|
| OLTP(交易型,频繁增删改查) | 较小(4KB、8KB) | 减少单次 I/O 的写放大,随机读写更高效 |
| OLAP(批量加载、大查询) | 较大(16KB、32KB) | 减少系统调用开销,顺序扫描吞吐量更高 |
注意:页大小在编译时确定,无法通过配置文件动态调整。生产环境中更换页大小需要重新编译 PostgreSQL。
pageinspect 扩展
pageinspect 是 PostgreSQL 官方提供的扩展,用于底层数据页的低级别检查。仅限超级用户使用。
启用扩展
CREATE EXTENSION pageinspect;
核心函数
| 函数 | 说明 |
|---|---|
get_raw_page(relname text, blkno bigint) | 读取指定表的主分支数据页 |
get_raw_page(relname text, fork text, blkno bigint) | 读取指定分支(main/fsm/vm)的数据页 |
page_header(page bytea) | 显示页头信息 |
page_checksum(page bytea, blkno bigint) | 计算页校验和 |
heap_page_items(page bytea) | 显示堆页的所有行指针和元组 |
fsm_page_contents(page bytea) | 显示 FSM 页的内部结构 |
综合示例
-- 1. 创建测试环境
CREATE EXTENSION IF NOT EXISTS pageinspect;
DROP TABLE IF EXISTS page_demo;
CREATE TABLE page_demo (
id serial PRIMARY KEY,
data text
);
-- 2. 插入数据
INSERT INTO page_demo (data)
SELECT 'row_' || generate_series
FROM generate_series(1, 100);
-- 3. 查看页头
SELECT * FROM page_header(get_raw_page('page_demo', 0));
-- 4. 查看行指针和元组(仅显示前5行)
SELECT lp, lp_off, lp_len, t_xmin, t_xmax, t_ctid, t_infomask
FROM heap_page_items(get_raw_page('page_demo', 0))
LIMIT 5;
-- 5. 查看页大小(编译时确定)
SHOW block_size;
核心技术总结
- PostgreSQL 依赖文件系统:不支持裸设备,所有数据读写必经 OS Page Cache(Buffer I/O 模型)。
- 双缓存是当前的主要限制:数据同时存在于
shared_buffers和 OS Page Cache,造成内存浪费和运维复杂性。 - I/O 模型持续演进:v16 引入实验性
debug_io_direct,v17 引入ReadStream框架,v18 正式引入 AIO 子系统(io_method)。Direct I/O + AIO 是未来的方向。 - 数据页是存储的基本单元:默认 8KB,包含页头、行指针、空闲空间、元组数据和特殊空间。行指针从前向后增长,元组从后向前增长。
- 元组头承载 MVCC 核心信息:
t_xmin/t_xmax决定可见性,t_ctid是物理定位的关键,t_infomask标识元组属性。 - 页大小影响性能:OLTP 场景适合小页,OLAP 场景适合大页。页大小在编译时确定,不可动态调整。
- pageinspect 是底层观测利器:提供
get_raw_page()、page_header()、heap_page_items()等函数,用于调试和理解底层存储结构。
API 速览
pageinspect 扩展 API
所属库:pageinspect(PostgreSQL 官方贡献扩展)
get_raw_page(relname text, blkno bigint) RETURNS bytea
- 读取指定表主分支(
mainfork)的原始数据页 - 参数:
relname— 表名;blkno— 页号(从 0 开始) - 返回:
bytea类型的原始页数据
get_raw_page(relname text, fork text, blkno bigint) RETURNS bytea
- 读取指定分支的数据页
- 参数:
fork—'main'(主数据)、'fsm'(空闲空间映射)、'vm'(可见性映射)、'init'(初始化分支)
page_header(page bytea) RETURNS record
- 解析页头信息,返回
lsn、checksum、flags、lower、upper、special、pagesize、version、prune_xid
heap_page_items(page bytea) RETURNS SETOF record
- 解析堆页的所有行指针和元组,返回
lp、lp_off、lp_flags、lp_len、t_xmin、t_xmax、t_ctid、t_infomask等字段
Demo 简单示例
以下是一个完整的 Node.js 示例,演示如何通过 pageinspect 扩展观测 PostgreSQL 数据页的底层结构。
运行说明
环境要求:
- Node.js 18+
- PostgreSQL 14+(已安装
pageinspect扩展) - npm 包:
pg
启动步骤:
# 1. 初始化项目
mkdir pg-pageinspect-demo && cd pg-pageinspect-demo
npm init -y
npm install pg
# 2. 创建 demo.js 文件(代码见下文)
# 3. 运行
node demo.js
代码说明
const { Client } = require('pg');
// PostgreSQL 连接配置
const client = new Client({
host: 'localhost',
port: 5432,
database: 'postgres',
user: 'postgres',
password: 'your_password'
});
async function runDemo() {
await client.connect();
console.log('✅ 已连接 PostgreSQL\n');
try {
// 1. 创建 pageinspect 扩展(如未安装)
await client.query('CREATE EXTENSION IF NOT EXISTS pageinspect;');
console.log('✅ pageinspect 扩展已就绪\n');
// 2. 创建测试表
await client.query(`
DROP TABLE IF EXISTS page_demo;
CREATE TABLE page_demo (
id serial PRIMARY KEY,
data text
);
`);
console.log('✅ 测试表 page_demo 已创建\n');
// 3. 插入 50 行数据(确保跨多个数据页)
await client.query(`
INSERT INTO page_demo (data)
SELECT 'row_' || generate_series
FROM generate_series(1, 50);
`);
console.log('✅ 已插入 50 行测试数据\n');
// 4. 查询数据页数量
const pageCountResult = await client.query(`
SELECT relpages
FROM pg_class
WHERE relname = 'page_demo';
`);
const pageCount = parseInt(pageCountResult.rows[0].relpages);
console.log(`📊 page_demo 表占用 ${pageCount} 个数据页\n`);
// 5. 遍历所有数据页,查看页头信息
console.log('--- 页头信息 ---');
for (let blkno = 0; blkno < pageCount; blkno++) {
const result = await client.query(`
SELECT lsn, checksum, flags, lower, upper, special, pagesize, version
FROM page_header(get_raw_page('page_demo', ${blkno}));
`);
const row = result.rows[0];
console.log(`Block ${blkno}: lower=${row.lower}, upper=${row.upper}, ` +
`special=${row.special}, pagesize=${row.pagesize}, version=${row.version}`);
}
// 6. 查看第 0 页的行指针和元组(前 5 行)
console.log('\n--- 第 0 页的行指针与元组(前5行) ---');
const itemsResult = await client.query(`
SELECT lp, lp_off, lp_len, t_xmin, t_xmax, t_ctid, t_infomask
FROM heap_page_items(get_raw_page('page_demo', 0))
LIMIT 5;
`);
console.table(itemsResult.rows);
// 7. 查看当前 block_size
const blockSizeResult = await client.query(`SHOW block_size;`);
console.log(`\n📌 当前 block_size: ${blockSizeResult.rows[0].block_size}`);
} catch (err) {
console.error('❌ 错误:', err.message);
} finally {
await client.end();
console.log('\n👋 连接已关闭');
}
}
runDemo();
PostgreSQL 原生指令对照:
-- 等价的原生 SQL 操作
CREATE EXTENSION pageinspect;
-- 查看页头
SELECT * FROM page_header(get_raw_page('page_demo', 0));
-- 查看行指针与元组
SELECT lp, lp_off, lp_len, t_xmin, t_xmax, t_ctid, t_infomask
FROM heap_page_items(get_raw_page('page_demo', 0));
-- 查看 block_size
SHOW block_size;
技术点总结
- pageinspect 扩展的安装与使用
get_raw_page()读取原始数据页page_header()解析页头结构heap_page_items()解析行指针与元组头- 通过
pg_class.relpages获取表的页数 - 理解
pd_lower/pd_upper与空闲空间的关系
官方文档
- PostgreSQL 官方文档: Database Page Layout
- PostgreSQL 官方文档: pageinspect
- PostgreSQL 官方文档: src/include/storage/bufpage.h
- PostgreSQL 18 发布公告
- PostgreSQL 18: 异步 I/O 子系统
参考链接
- PostgreSQL Wiki: Direct I/O
- Andres Freund: AIO in Postgres 18 and beyond
- Cybertec: PostgreSQL 18 AIO 性能测试
- TigerData: Double Buffering 解析
总结
本文系统梳理了 PostgreSQL 存储层的三大核心主题:文件系统与 I/O 模型、数据页结构和元组结构。PostgreSQL 当前(v17)采用 Buffer I/O 模型,存在双缓存问题;v16 起引入实验性 Direct I/O 支持,v18 正式推出 AIO 子系统,标志着 I/O 模型的重大演进。
数据页(默认 8KB)由页头、行指针、空闲空间、元组和特殊空间组成,行指针与元组从两端向中间增长。元组头中的 t_xmin/t_xmax 支撑 MVCC,t_ctid 是索引扫描的物理定位依据。pageinspect 扩展是观测底层存储结构的官方工具。理解这些底层原理,是做好 PostgreSQL 性能调优和运维的基础。
517



被折叠的 条评论
为什么被折叠?



