在数据库日常使用中,我们常常会遇到一个有趣的现象:一个复杂的查询,第一次运行时可能需要数十秒,而紧接着第二次执行,却在瞬间完成。
这个"第二次更快"的魔法,其背后正是缓存机制在默默发挥作用。
对于 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 系列课程,循序渐进,从命令行到运维部署,一步到位。