用 pg_buffercache 和 pgfincore 彻底解剖 PostgreSQL 双缓存

在数据库日常使用中,我们常常会遇到一个有趣的现象:一个复杂的查询,第一次运行时可能需要数十秒,而紧接着第二次执行,却在瞬间完成。

这个"第二次更快"的魔法,其背后正是缓存机制在默默发挥作用。

对于 PostgreSQL 而言,它采用了一种被称为"双缓存(Double Caching)"的策略来加速数据访问。

然而,这种设计既是性能功臣,有时也让人迷惑:"我的服务器内存到底被谁占用?"、"shared_buffers 参数设置多大才最合适?"。

这些问题的根源,都指向我们对双缓存机制理解的深度。

本文将摒弃纯理论,以一份详尽的、可复现的实验手册为核心,带你一步步操作,用数据和事实说话。

我们将借助 pg_buffercache 和 pgfincore 这两个强大的工具,让你亲眼见证并彻底理解 PostgreSQL 的缓存机制,并将其与 Oracle、MySQL 的设计进行深度对比,最终让你拥有洞察数据库内存管理的锐利目光。

01

原理剖析:一场关于"信任"的

设计理念之争

在深入实验之前,我们必须理解 PostgreSQL 在缓存设计上的核心思想。

这不仅仅是技术选择,更是一场关于"是否信任操作系统"的设计理念之争,也直接回答了一个经典问题。

**经典疑问:**为何 PG 的 shared_buffers 只推荐 25%,而 Oracle/MySQL 却敢用 80%?

在使用 PostgreSQL 时,我们通常得到的建议是将 shared_buffers 设置为操作系统内存的 25%。

而经验丰富的 DBA 会发现,在 Oracle 中 SGA(系统全局区)或 MySQL 中innodb_buffer_pool_size 的大小,往往被推荐设置为物理内存的 70%-80%。

为何存在如此巨大的差异?答案就在于它们截然不同的缓存架构。

1

PostgreSQL 的双缓存协作模型:

"信任并利用操作系统"

PostgreSQL 的设计理念是与操作系统紧密协作。

它认为操作系统在文件系统管理和 I/O 调度方面已经做得足够好,数据库无需"重新发明轮子"。

因此,它构建了一个双层缓存体系:

1)PostgreSQL 共享缓冲区 (Shared Buffers)

这是 PG 自身管理的第一层缓存,用于存放最热的数据块。

它的效率最高,但容量有限。

2)操作系统页面缓存 (OS Page Cache)

这是 PG 依赖的第二层、也是更广阔的缓存。

PostgreSQL 通过标准的文件系统接口读写数据,这意味着所有 I/O 操作都会自然地被 OS Page Cache 缓存。

effective_cache_size 的角色

PostgreSQL 不仅被动地使用 OS 缓存,它还希望"知道"这层缓存有多大。

effective_cache_size 参数正是为此而生。它并不分配内存,而是作为一个提示(Hint),告诉 PostgreSQL 优化器:"你可以假设系统总共有这么大的内存可用于缓存数据(即 shared_buffers + 可用的 OS Page Cache)"。

这个信息至关重要,它会直接影响优化器的决策,例如在数据量较大时,是选择可能利用缓存的索引扫描,还是选择看似更昂贵但对顺序 I/O 友好的全表扫描。

因此,shared_buffers 设置为 25% 的逻辑就清晰了:我们必须为强大且高效的 OS Page Cache 留出充足的内存空间。

2

Oracle/MySQL 的单缓存模型:

"我比操作系统更专业"

与 PG 相反,Oracle 和 MySQL (InnoDB) 的设计理念是"我比操作系统更专业"。

它们认为通用操作系统的缓存算法无法理解数据库复杂的访问模式,因此选择绕过 OS Page Cache,实现自己的单层、一体化缓存。

实现机制

它们通过直接 I/O (Direct I/O) 技术,在读写数据时向操作系统明确传递 O_DIRECT 标志。

这个标志的含义是:"请不要把这次 I/O 的数据放入你的页面缓存,直接将数据在磁盘和我指定的内存地址(即数据库自身的缓存区)之间传输。"

核心优势

  • 消除双缓存冗余

内存效率最高,一份数据在内存中只有一份副本。

  • 减少数据拷贝与上下文切换

数据直接在用户态的数据库缓存和磁盘间传输,提升了 I/O 效率。

  • 更智能的缓存算法

数据库可以实现更精细的缓存策略,避免一次性大查询"污染"掉真正的热点数据。

正因为它们不依赖 OS Cache,所以它们敢于将大部分物理内存(如 80%)分配给自己的专用缓存区(Oracle 的 SGA 或 MySQL 的 InnoDB Buffer Pool),以期将尽可能多的数据保留在自己的掌控之下。

事情并非绝对

云时代的演进 值得注意的是,原生 PostgreSQL 不支持 Direct I/O 是其与 Oracle/MySQL 的一个本质差异。

但在云原生时代,这种界限正在变得模糊。

例如,亚马逊的 Aurora PostgreSQL 通过其创新的存储架构,实际上消除了双缓存,并且不使用传统的文件系统缓存,这表明即使在 PostgreSQL 生态内部,缓存策略也在不断演进。

02

实验手册:搭建你的观测平台

1

实验目的

通过两个独立的场景(小表和大表),精确观察并验证PostgreSQL在默认Buffered I/O模式下的"双缓存"现象,并理解其针对不同场景的缓存管理策略。

2

实验前准备

1) 安装并启用插件(如果尚未完成)

复制代码
CREATE EXTENSION pg_buffercache;
CREATE EXTENSION pgfincore;

2)获取基本信息

复制代码
SHOW shared_buffers; 
 shared_buffers 
----------------
 128MB       -- 记下这个值,它是我们判断"大/小"表的依据。
(1 row)
SHOW block_size;     
 block_size 
------------
 8192  -- 通常是 8192 字节。
(1 row)

3

场景一:小表实验(小于 shared_buffers)

目标

观察当一个表能完全放入shared_buffers时,双缓存的表现。

步骤 1: 创建并填充小表

创建一个大小明显小于shared_buffers的表。

复制代码
CREATE TABLE small_table AS
SELECT g AS id, md5(g::text) AS val
FROM generate_series(1, 1000000) g;

ANALYZE small_table;

-- 确认表大小
SELECT pg_size_pretty(pg_relation_size('small_table'));
 pg_size_pretty 
----------------
 66 MB
(1 row)

2)步骤 2: 【关键】执行清理,建立干净的"读前"环境

在数据准备好后、执行读查询前,我们进行彻底的清理,以确保接下来的缓存变化完全是由SELECT查询引起的。

1) 清空操作系统Page Cache (需要root权限):

复制代码
# 以 root 用户执行
echo 3 > /proc/sys/vm/drop_caches

2) 清空PostgreSQL Shared Buffers (通过重启PG服务实现)

复制代码
pg_ctl restart

步骤 3: 验证初始状态(缓存应为空)

重新连接数据库,确认small_table在两层缓存中都不存在。

复制代码
-- 检查 PG Shared Buffers
SELECT count(*) AS pg_cache_pages FROM pg_buffercache WHERE relfilenode = 'small_table'::regclass::oid;
 pg_cache_pages 
----------------
              0
(1 row)


-- 检查 OS Page Cache
SELECT pages_mem > 0 AS os_cache_hit FROM pgfincore('small_table');
 os_cache_hit 
--------------
 f
(1 row)

步骤 4: 执行读操作(全表扫描)

复制代码
-- 这会触发从磁盘读取数据
SELECT count(*) FROM small_table;

步骤 5: 观察并分析结果

立即检查两层缓存的状态。

复制代码
-- 1. 检查 PG Shared Buffers
SELECT pg_size_pretty(count(*) * 8192) AS size_in_pg_cache
FROM pg_buffercache WHERE relfilenode = 'small_table'::regclass::oid;
 size_in_pg_cache 
------------------
 768 kB
(1 row)

-- 2. 检查 OS Page Cache (假设 OS 页面大小 4KB)
SELECT pg_size_pretty(pages_mem * 4096) AS size_in_os_cache
FROM pgfincore('small_table');
 size_in_os_cache 
------------------
 46 MB
(1 row)

4

场景二:大表实验(大于 shared_buffers)

目标

观察当表的大小超出shared_buffers时,PG的智能缓存策略以及双缓存的表现。

步骤 1: 创建并填充大表

创建一个大小明显大于shared_buffers的表。

复制代码
CREATE TABLE big_table AS
SELECT g AS id, repeat('x', 200) AS pad
FROM generate_series(1, 2000000) g; -

ANALYZE big_table;

-- 确认表大小
SELECT pg_size_pretty(pg_relation_size('big_table'));
 pg_size_pretty 
----------------
 460 MB
(1 row)

步骤 2: 【关键】再次执行清理

与场景一相同,在执行读查询前,我们再次清理环境。

1)清空操作系统Page Cache

echo 3 > /proc/sys/vm/drop_caches

2)清空PostgreSQL Shared Buffers

重启PostgreSQL服务。

步骤 3: 验证初始状态(缓存应为空)

复制代码
-- 检查 PG Shared Buffers
SELECT count(*) AS pg_cache_pages FROM pg_buffercache WHERE relfilenode = 'big_table'::regclass::oid;
-- >> 预期结果: 0

-- 检查 OS Page Cache
SELECT pages_mem > 0 AS os_cache_hit FROM pgfincore('big_table');
-- >> 预期结果: false

步骤 4: 执行读操作(全表扫描)

复制代码
SELECT count(*) FROM big_table;

步骤 5: 观察并分析结果

立即检查两层缓存的状态。

复制代码
-- 1. 检查 PG Shared Buffers
SELECT pg_size_pretty(count(*) * 8192) AS size_in_pg_cache
FROM pg_buffercache WHERE relfilenode = 'big_table'::regclass::oid;
 size_in_pg_cache 
------------------
 2968 kB
(1 row)


-- 2. 检查 OS Page Cache
SELECT pg_size_pretty(pages_mem * 4096) AS size_in_os_cache
FROM pgfincore('big_table');
size_in_os_cache 
------------------
 459 MB
(1 row)

03

实验结果分析

双缓存(Double Buffering)指的不是"两份完全相同大小的缓存",而是"数据在两套独立的缓存系统中都可能存在"。

整理我们得到的结论:在share_buffer=128MB的情况下,执行全表扫描之后的cache情况:

接下来的分析,以小表 为例:small_table 的数据(大约65MB),既在 OS Cache 里有一份完整的拷贝,同时又在 PG Shared Buffers 里有另一份(尽管只是很小一部分),这就是双缓存。

1

为什么 PG Shared Buffers 里只有 768KB?

这是最关键的问题,也是你产生疑问的地方。

原因在于 SELECT count(*) 这个查询的执行方式和 PG 的缓存替换策略。

查询的本质

SELECT count(*) 是一个顺序扫描(Sequential Scan)操作。

PostgreSQL 只需要从头到尾"流式"地读取表中的每一个数据块(Block),检查行是否可见,然后计数。

它不需要把整张表的所有数据块都同时保留在内存里。

PG 的缓存策略(Ring Buffer for Scans)

对于大型表的顺序扫描,PostgreSQL 非常智能。

它知道如果把整个表都加载到 shared_buffers,会立刻"污染"掉缓存,把其他更有价值的热点数据(比如索引块)都冲刷出去。

  • 因此,它会使用一个小的**"环形缓冲区"(Ring Buffer)**策略来处理扫描。它只申请一小部分 shared_buffers(默认通常是 256KB,但可以动态调整)来作为这个扫描的"工作区"。

  • 想象一下:PG 从磁盘(经由 OS Cache)读取 Block 1,放到这个环形缓冲区的第1个位置;然后读取 Block 2,放到第2个位置... 当这个小环形缓冲区满了之后,再读取新的 Block 时,就会覆盖掉环里最旧的那个 Block。

  • 所以,当 count(*) 结束时,留在 shared_buffers 里的,仅仅是这张表被扫描到最后的那一小部分数据块。

你的 768 KB 就是这个"扫描尾巴"的大小。

OS Cache 的行为

与 PG 不同,操作系统的 Page Cache 没那么"智能"。

它的默认行为是:**只要有进程读取了文件,就把读过的数据都缓存起来,**以备下次使用。

  • 当 PostgreSQL 后台进程去读取 small_table 的文件时,Linux 内核忠实地把整个文件(约 46MB)都加载到了 Page Cache 中。

  • 所以,pgfincore 看到了完整的 46MB 缓存。

2

用一个更形象的比喻来解释

  • OS Page Cache 就像一个巨大的仓库。

当你要处理一批货物(small_table,65MB)时,仓库管理员会把整批货都从货车(磁盘)上卸下来,堆放在仓库里。

  • PG Shared Buffers 就像你面前的一张小工作台。

你不会把整个 46MB 的货物都堆在工作台上。你只会从仓库里一次拿几箱(几个 Block)放到工作台上,检查完(count(*)),就放到一边,再拿下一批。

  • 我的实验结果

1)pgfincore 检查了仓库,发现整批 46MB 的货都在里面。

2)pg_buffercache 检查了你的工作台,发现上面只剩下最后处理的那几箱货(768 KB)。

  • 双缓存

这批货物既在仓库里存了一份,又在工作台上过了一遍手(并且还留了点渣)。

这就是双缓存。

03

结论与最终启示

这份详尽的实验手册带我们得出了坚实的结论:

1)双缓存是真实存在的

通过 pg_buffercache 和 pgfincore,我们亲眼见证了同一份数据在两层缓存中的副本。

2)shared_buffers 的角色是动态的

当热数据小于它时,它是性能的加速器;当数据远大于它时,它变成了一个"滑动窗口",此时 OS 缓存成为性能的守护者。

3)shared_buffers 不是越大越好

过度分配会挤压 OS 缓存的空间,可能损害大查询的性能。

将其设置为物理内存的 25% 是一个合理的起点,关键是为强大的 OS 缓存留足空间。

4)数据库设计存在理念差异

PostgreSQL 的双缓存模型拥抱简单和可移植性,而 Oracle/MySQL 的单缓存模型追求极致的控制和效率。

通过亲手操作,我们不再是道听途说,而是真正掌握了 PostgreSQL 缓存机制的精髓。

下一次当你面对性能问题时,你将拥有更锐利的目光,能够准确判断瓶颈究竟是在 PG 的第一层防线,还是在 OS 的第二层防线,从而做出最精准的优化决策。

写在最后

实际上,多数数据库性能问题的根源,常常与操作系统层面的资源配置、性能瓶颈等因素密切相关。

如果你还想系统补齐 Linux 的实战短板,推荐看看刘峰老师的 Linux 系列课程,循序渐进,从命令行到运维部署,一步到位。

相关推荐
程序员南音4 小时前
基于Springboot + vue3实现的农产品系统
经验分享
程序员南音4 小时前
基于Springboot + vue3实现的家校合作平台
经验分享
叠叠乐6 小时前
蓝牙数据包从底层到应用层协议一层套一层
经验分享
Logic1017 小时前
一份系统化《Python爬虫教程》学习笔记:Python爬虫63个核心案例精讲(含反爬策略与源码剖析)
经验分享·爬虫·python·学习笔记·编程·软件开发
SunnyDays10118 小时前
如何使用 C# 冻结 Excel 行和列
经验分享
qq77788899 小时前
PDF转图片免费工具有哪些?永久无广告PDF批量导出高清JPG PNG格式软件推荐
经验分享
草莓熊Lotso9 小时前
Python 基础语法完全指南:变量、类型、运算符与输入输出(零基础入门)
运维·开发语言·人工智能·经验分享·笔记·python·其他
金海境科技9 小时前
【服务器数据恢复】勒索病毒加密导致金融机构EMC存储核心数据丢失数据恢复案例 - 金海境科技
经验分享
小白跃升坊9 小时前
软件服务类企业基于开源 AI CRM 的实践案例
经验分享·开源·crm·经典案例·客户关系管理系统·ai crm