PostgreSQL 数据读取到共享缓冲区
在这篇文章中,我们将深入探讨 PostgreSQL 如何将磁盘上的数据文件读入到共享缓冲区中。
调试方法(可选)
如果你希望跟踪代码执行过程,可以使用以下调试方法:启动一个 SQL 终端(如 psql),执行 SELECT pg_backend_pid(); 获取当前后端进程的 PID,然后通过 gdb attach 到该进程。调试过程中可能会收到 SIGUSR1 和 SIGUSR2 信号,可以在 gdb 中屏蔽它们。
数据读取入口
当查询需要访问某个表的数据时,PostgreSQL 会将该表对应的数据块加载到共享缓冲区中。这一过程始于 ReadBuffer 函数(位于 src/backend/storage/buffer/bufmgr.c):
1
2
3
4
5
6
// src/backend/storage/buffer/bufmgr.c
Buffer
ReadBuffer(Relation reln, BlockNumber blockNum)
{
return ReadBufferExtended(reln, MAIN_FORKNUM, blockNum, RBM_NORMAL, NULL);
}
ReadBuffer 接收两个参数:
Relation reln:目标关系(表或索引)BlockNumber blockNum:需要读取的块号
该函数直接调用 ReadBufferExtended,后者提供了更丰富的控制参数(如分支号、读取模式、缓冲策略等)。
ReadBufferExtended
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/backend/storage/buffer/bufmgr.c
Buffer
ReadBufferExtended(Relation reln, ForkNumber forkNum, BlockNumber blockNum,
ReadBufferMode mode, BufferAccessStrategy strategy)
{
bool hit;
Buffer buf;
/*
* 拒绝读取非本会话的临时关系——因为我们无法看到其他会话的本地缓冲区。
*/
if (RELATION_IS_OTHER_TEMP(reln))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot access temporary tables of other sessions")));
/*
* 读取缓冲区,并更新统计计数器以反映缓存命中或未命中。
*/
pgstat_count_buffer_read(reln);
buf = ReadBuffer_common(RelationGetSmgr(reln), reln->rd_rel->relpersistence,
forkNum, blockNum, mode, strategy, &hit);
if (hit)
pgstat_count_buffer_hit(reln);
return buf;
}
该函数主要完成两项检查:
- 临时表访问检查:确保不会跨会话访问临时表(临时表的数据位于会话私有的本地缓冲区中)。
- 统计计数:记录缓冲区读取次数,并在命中时更新命中次数。
随后调用 ReadBuffer_common 执行实际的读取操作。
ReadBuffer_common
ReadBuffer_common 是读取逻辑的核心,其代码较长,我们分段分析。
函数签名与变量声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
static Buffer
ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
BlockNumber blockNum, ReadBufferMode mode,
BufferAccessStrategy strategy, bool *hit)
{
BufferDesc *bufHdr;
Block bufBlock;
bool found;
bool isExtend;
bool isLocalBuf = SmgrIsTemp(smgr);
*hit = false;
/* 确保有足够空间记录 buffer pin */
ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
isExtend = (blockNum == P_NEW);
TRACE_POSTGRESQL_BUFFER_READ_START(forkNum, blockNum,
smgr->smgr_rnode.node.spcNode,
smgr->smgr_rnode.node.dbNode,
smgr->smgr_rnode.node.relNode,
smgr->smgr_rnode.backend,
isExtend);
/* 如果调用者请求 P_NEW,则替换为正确的块号 */
if (isExtend)
{
blockNum = smgrnblocks(smgr, forkNum);
/* 如果关系已达到最大可能长度,则报错 */
if (blockNum == P_NEW)
ereport(ERROR,
(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("cannot extend relation %s beyond %u blocks",
relpath(smgr->smgr_rnode, forkNum),
P_NEW)));
}
关键点:
isLocalBuf:判断是否为临时表(使用本地缓冲区)。ResourceOwnerEnlargeBuffers:确保当前资源所有者(ResourceOwner)有空间记录即将增加的 buffer pin。这是为了在事务结束或回滚时能够正确释放 pin,防止缓冲区被永久固定在内存中。isExtend:判断是否为扩展关系(即请求新块)。若块号为P_NEW,则通过smgrnblocks获取当前文件长度,并将该块作为新块分配。
缓冲区查找与分配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
if (isLocalBuf)
{
bufHdr = LocalBufferAlloc(smgr, forkNum, blockNum, &found);
if (found)
pgBufferUsage.local_blks_hit++;
else if (isExtend)
pgBufferUsage.local_blks_written++;
else if (mode == RBM_NORMAL || mode == RBM_NORMAL_NO_LOG ||
mode == RBM_ZERO_ON_ERROR)
pgBufferUsage.local_blks_read++;
}
else
{
/*
* 查找缓冲区。如果请求的块不在内存中,则会设置 IO_IN_PROGRESS 标志。
*/
bufHdr = BufferAlloc(smgr, relpersistence, forkNum, blockNum,
strategy, &found);
if (found)
pgBufferUsage.shared_blks_hit++;
else if (isExtend)
pgBufferUsage.shared_blks_written++;
else if (mode == RBM_NORMAL || mode == RBM_NORMAL_NO_LOG ||
mode == RBM_ZERO_ON_ERROR)
pgBufferUsage.shared_blks_read++;
}
/* 此时我们不持有任何锁。 */
根据缓冲区类型(本地或共享)分别调用 LocalBufferAlloc 或 BufferAlloc。这两个函数均尝试在缓冲区池中查找目标块;若找到(found == true),则直接返回对应的缓冲区描述符,并更新相应的统计计数器(命中、写入或读取)。
BufferAlloc 的主要职责有两个:
- 检查请求的磁盘块是否已在内存中。
- 若不在,则分配一个空闲缓冲区(可能涉及淘汰旧页面),并初始化其描述符
bufHdr。
缓存命中处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* 如果已经在缓冲区池中,则直接返回 */
if (found)
{
if (!isExtend)
{
/* 仅需更新统计信息后退出 */
*hit = true;
VacuumPageHit++;
if (VacuumCostActive)
VacuumCostBalance += VacuumCostPageHit;
TRACE_POSTGRESQL_BUFFER_READ_DONE(forkNum, blockNum,
smgr->smgr_rnode.node.spcNode,
smgr->smgr_rnode.node.dbNode,
smgr->smgr_rnode.node.relNode,
smgr->smgr_rnode.backend,
isExtend,
found);
/*
* 在 RBM_ZERO_AND_LOCK 模式下,调用者期望返回时页面已被锁定。
*/
if (!isLocalBuf)
{
if (mode == RBM_ZERO_AND_LOCK)
LWLockAcquire(BufferDescriptorGetContentLock(bufHdr),
LW_EXCLUSIVE);
else if (mode == RBM_ZERO_AND_CLEANUP_LOCK)
LockBufferForCleanup(BufferDescriptorGetBuffer(bufHdr));
}
return BufferDescriptorGetBuffer(bufHdr);
}
若缓冲区已存在且不是扩展操作,则:
- 标记命中(
*hit = true)。 - 更新 Vacuum 相关统计(用于 autovacuum 成本计算)。
- 根据读取模式决定是否加锁(如
RBM_ZERO_AND_LOCK要求独占锁)。 - 最后返回缓冲区句柄。
扩展时的特殊情况
若在扩展关系时发现一个已存在且标记为 BM_VALID 的缓冲区,说明之前可能曾尝试读取超出 EOF 的块(例如由于内核 bug 或 zero_damaged_pages 设置)。此时会检查页面是否真的为全新(PageIsNew),若不是则报错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
* 我们仅在此角落情况下执行:试图扩展关系,但找到了一个已标记 BM_VALID 的缓冲区。
* 这可能是因为 mdread 不会抱怨超出 EOF 的读取(当 zero_damaged_pages 为 ON),
* 导致之前读取超出 EOF 的尝试留下了一个“有效”的零填充缓冲区。
* 不幸的是,在某些有 bug 的 Linux 内核中也可能出现这种情况,
* 其 lseek(SEEK_END) 的结果未能反映最近的写入。此时,已存在的缓冲区可能包含有效数据,
* 我们不希望覆盖它。因为合法情况应始终留下零填充的缓冲区,
* 所以若非 PageIsNew 则报错。
*/
bufBlock = isLocalBuf ? LocalBufHdrGetBlock(bufHdr) : BufHdrGetBlock(bufHdr);
if (!PageIsNew((Page) bufBlock))
ereport(ERROR,
(errmsg("unexpected data beyond EOF in block %u of relation %s",
blockNum, relpath(smgr->smgr_rnode, forkNum)),
errhint("This has been seen to occur with buggy kernels; consider updating your system.")));
/* 必须执行 smgrextend,否则内核不会保留该页面,下一次 P_NEW 可能会返回同一页面。
* 清除 BM_VALID 位,执行 BufferAlloc 未做的 StartBufferIO 调用,然后继续。
*/
if (isLocalBuf)
{
/* 仅需调整标志位 */
uint32 buf_state = pg_atomic_read_u32(&bufHdr->state);
Assert(buf_state & BM_VALID);
buf_state &= ~BM_VALID;
pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state);
}
else
{
/* 循环处理极小的可能性:有人在我们清除 BM_VALID 和 StartBufferIO 检查之间重新设置了它 */
do
{
uint32 buf_state = LockBufHdr(bufHdr);
Assert(buf_state & BM_VALID);
buf_state &= ~BM_VALID;
UnlockBufHdr(bufHdr, buf_state);
} while (!StartBufferIO(bufHdr, true));
}
}
缓存未命中:读取或扩展
若未找到缓冲区(found == false),则说明需要从磁盘读取数据(或扩展文件)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/*
* 走到这里意味着我们已经为页面分配了缓冲区,但其内容尚未有效。
* 如果是共享缓冲区,则其 IO_IN_PROGRESS 标志已被设置。
*/
Assert(!(pg_atomic_read_u32(&bufHdr->state) & BM_VALID)); /* 无需自旋锁 */
bufBlock = isLocalBuf ? LocalBufHdrGetBlock(bufHdr) : BufHdrGetBlock(bufHdr);
if (isExtend)
{
/* 新缓冲区用零填充 */
MemSet((char *) bufBlock, 0, BLCKSZ);
/* 全零页面不计算校验和 */
smgrextend(smgr, forkNum, blockNum, (char *) bufBlock, false);
/*
* 注意:我们*没有*在此处调用 ScheduleBufferTagForWriteback;
* 虽然我们本质上执行了一次写入。至少在 Linux 上,这样做会破坏“延迟分配”机制,
* 导致文件碎片增加。
*/
}
else
{
/*
* 读入页面,除非调用者打算覆盖它并只希望我们分配一个缓冲区。
*/
if (mode == RBM_ZERO_AND_LOCK || mode == RBM_ZERO_AND_CLEANUP_LOCK)
MemSet((char *) bufBlock, 0, BLCKSZ);
else
{
instr_time io_start,
io_time;
if (track_io_timing)
INSTR_TIME_SET_CURRENT(io_start);
smgrread(smgr, forkNum, blockNum, (char *) bufBlock);
if (track_io_timing)
{
INSTR_TIME_SET_CURRENT(io_time);
INSTR_TIME_SUBTRACT(io_time, io_start);
pgstat_count_buffer_read_time(INSTR_TIME_GET_MICROSEC(io_time));
INSTR_TIME_ADD(pgBufferUsage.blk_read_time, io_time);
}
/* 检查数据是否损坏 */
if (!PageIsVerifiedExtended((Page) bufBlock, blockNum,
PIV_LOG_WARNING | PIV_REPORT_STAT))
{
if (mode == RBM_ZERO_ON_ERROR || zero_damaged_pages)
{
ereport(WARNING,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("invalid page in block %u of relation %s; zeroing out page",
blockNum,
relpath(smgr->smgr_rnode, forkNum))));
MemSet((char *) bufBlock, 0, BLCKSZ);
}
else
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("invalid page in block %u of relation %s",
blockNum,
relpath(smgr->smgr_rnode, forkNum))));
}
}
}
- 扩展:将缓冲区清零并调用
smgrextend将空页面写入文件。 - 读取:调用
smgrread从磁盘读取数据到bufBlock,同时记录 I/O 耗时(若开启track_io_timing)。读取后使用PageIsVerifiedExtended验证页面完整性;若损坏,根据配置决定是报错还是清零页面。
标记缓冲区有效并返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
* 在 RBM_ZERO_AND_LOCK 模式下,在标记页面为有效之前获取缓冲区内容锁,
* 以确保其他后端在调用者初始化之前不会看到零填充的页面。
*/
if ((mode == RBM_ZERO_AND_LOCK || mode == RBM_ZERO_AND_CLEANUP_LOCK) &&
!isLocalBuf)
{
LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_EXCLUSIVE);
}
if (isLocalBuf)
{
/* 仅需调整标志位 */
uint32 buf_state = pg_atomic_read_u32(&bufHdr->state);
buf_state |= BM_VALID;
pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state);
}
else
{
/* 设置 BM_VALID,终止 I/O,并唤醒任何等待者 */
TerminateBufferIO(bufHdr, false, BM_VALID);
}
VacuumPageMiss++;
if (VacuumCostActive)
VacuumCostBalance += VacuumCostPageMiss;
TRACE_POSTGRESQL_BUFFER_READ_DONE(forkNum, blockNum,
smgr->smgr_rnode.node.spcNode,
smgr->smgr_rnode.node.dbNode,
smgr->smgr_rnode.node.relNode,
smgr->smgr_rnode.backend,
isExtend,
found);
return BufferDescriptorGetBuffer(bufHdr);
}
最后,根据缓冲区类型设置 BM_VALID 标志(本地缓冲区直接修改状态,共享缓冲区通过 TerminateBufferIO 完成),更新未命中统计,并返回缓冲区句柄。
总结
ReadBuffer 系列函数是 PostgreSQL 缓冲区管理的核心,其通过多层封装实现了:
- 缓存查找:优先从共享缓冲区或本地缓冲区中获取页面,避免不必要的磁盘 I/O。
- 缓存分配:若未命中,则分配一个缓冲区(可能触发页面淘汰)。
- 磁盘 I/O:通过
smgrread或smgrextend读取或扩展数据。 - 页面验证:确保读取的数据完整有效。
- 并发控制:根据读取模式加锁,保证多后端访问的一致性。
理解这一流程有助于我们深入掌握 PostgreSQL 的存储层工作原理。