冷缓存、热缓存与"假磁盘":同一台机器为什么能测出 10.9 GB/s 和 250 MB/s
数据库压测里最贵的错误不是调错参数,而是不知道数据从哪来。同一条 SELECT sum(...) 在全表扫描上,我测到过 10.9 GB/s 和 250 MB/s——差 40 倍。前者不是磁盘快,是缓存根本没清掉。
0. 先看两组原始数字
实验 A(Windows 11 + Docker Desktop / WSL2,32 vCPU、31.9 GiB 内存)
表大小: 5124 MB (pgbench_accounts, 4000 万行)
shared_buffers: 128 MB
每次扫描前: sync; echo 3 > /proc/sys/vm/drop_caches
实测: 470 ms
5124MB ÷ 0.47s ≈ 10.9 GB/s。
实验 B(Debian 12 KVM VPS,1 vCPU、961 MB 内存)
表大小: 1025 MB (pgbench_accounts, 800 万行)
shared_buffers: 64 MB
每次扫描前: sync; echo 3 > /proc/sys/vm/drop_caches
清缓存后 Cached: 120 MB
实测: 3450–4120 ms
1025MB ÷ 4.1s ≈ 250 MB/s。
两台机器都是 PostgreSQL 18.6,都是全表扫描,都执行了 drop_caches。40 倍差距来自一个事实:A 的数据其实还在,B 的不在了。
1. 四层缓存:你的读到底命中了谁
| 层 | 名字 | 典型带宽 | 怎么绕开 |
|---|---|---|---|
| L1 | PostgreSQL shared_buffers | 10+ GB/s | 让数据集远大于 shared_buffers |
| L2 | 客户机 OS page cache | 数 GB/s | drop_caches(有效) |
| L3 | 虚拟化层缓存(VHDX/qcow2 宿主缓存、存储卷缓存、overlay 上层) | 数 GB/s | 客户机内几乎无法清理 |
| L4 | 物理盘 | SSD 200–2000 MB/s,HDD 100–200 MB/s | —— |
A 实验的 10.9 GB/s 只可能来自 L1–L3。事后分析,最可能是 L3:Windows 宿主对 VHDX 文件的缓存——drop_caches 跑在 WSL2 的 Linux 内核里,只能清 L2,清不到 Windows 那一侧的缓存。
这就是"假磁盘":你以为在测盘,其实在测宿主机的内存。
数量级速查(用来自查你的数字是否可信):
| 实测带宽 | 数据几乎肯定来自 |
|---|---|
| > 5 GB/s | 内存(L1/L2/L3),不可能是磁盘 |
| 1–5 GB/s | L2/L3 或 NVMe 阵列 |
| 200–1000 MB/s | 单块 SSD(SATA/NVMe 单盘) |
| 50–200 MB/s | 网络盘、HDD、限流的云盘 |
2. 三个数字就能判断"数据从哪来"
① 算术校验(最快)
表大小 ÷ 耗时 = 带宽。超过 5 GB/s 就直接怀疑缓存,不要往下分析参数。
② Cached 的前后对比
# 清缓存前后各看一次
awk '/^Cached:/{printf "Cached=%.0fMB\n", $2/1024}' /proc/meminfo
sync; echo 3 > /proc/sys/vm/drop_caches
awk '/^Cached:/{printf "Cached=%.0fMB\n", $2/1024}' /proc/meminfo
实验 A 的实测:Cached 7931028 kB → 558604 kB(约 7.9GB → 0.55GB),看起来"清掉了"。但这仍然只证明 L2 被清了——L3 的宿主缓存不在这个计数里,这正是 A 事后仍跑出 10.9 GB/s 的原因。
实验 B 的实测:清完 Cached = 120 MB,而表有 1025MB,物理上不可能全部缓存,所以 4.1s 是可信的冷盘数字。
判据:(数据集大小) > (内存 + 你能确认被清掉的缓存),否则不能声称"冷"。
③ 数据库自己给的证据
EXPLAIN (ANALYZE, BUFFERS)
SELECT sum(abalance) FROM pgbench_accounts;
看 Buffers: shared hit=... read=...:read 才是真正向 OS 要页的数量。再配合 pg_stat_statements:
SELECT calls, round(mean_exec_time::numeric,2) AS mean_ms,
round(100.0*shared_blks_hit/nullif(shared_blks_hit+shared_blks_read,0),1) AS hit_pct
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 5;
我在两次实验里看到的 hit_pct:
- 热缓存场景:排序查询
hit_pct = 19.8%(还在大量读,但已有一部分命中) - 冷盘场景:同一条排序查询
hit_pct = 0.2%,temp_blks_written = 616510
同一个查询、同一条 SQL,命中率 19.8% vs 0.2%,对应完全不同的耗时解释。
3. 为什么 drop_caches 经常"清了个寂寞"
三个真实原因,按踩坑频率排序:
① 虚拟化层缓存不在你的控制范围
WSL2、VMware、VirtualBox、部分云盘的宿主机缓存,客户机内没有接口可以清。你能做的只有:
- 让数据集远大于宿主机内存(例如 > 宿主机 RAM 的 1.5 倍);
- 换一台物理机/独占型实例;
- 或明确承认"这是热缓存数据",并在报告里写清楚。
② 容器里清缓存是"全机行为"
在容器里执行 echo 3 > /proc/sys/vm/drop_caches 清的是整台宿主机的页缓存。这会拖慢同机的其它服务(数据库、代理、Web),也可能被安全策略禁止(需要 --privileged)。在共享机器上这是危险操作,我在测试机上全程用 nice -n 19 ionice -c3 限流并提前告知机主。
③ 清完立刻测,缓存又被"上一次探测"填回来了
drop_caches 之后你还要启动客户端、连库、解析 SQL,这些动作会读元数据与索引首页。正确顺序是:清缓存 → 立即执行被测语句 → 记录,而不是清完先跑个 select count(*) 热身。
4. 代价:同一组参数,冷热给出相反结论
这是最有价值的一组证据——同一份配置,缓存状态不同,结论完全相反。
例一:PostgreSQL 18 的 io_method
| 缓存状态 | sync | worker | 结论 |
|---|---|---|---|
| 热(WSL2,数据被宿主缓存兜住) | 707 ms | 470 ms | worker 快 1.5 倍 |
| 冷(VPS,1025MB 表 > 961MB 内存) | 3471–4116 ms | 3446–4113 ms | 无差异(<2%) |
为什么?热缓存时"读"本身极快,瓶颈变成 I/O 提交路径(同步模式一次只能有一个在途请求,异步能重叠);冷盘时瓶颈是磁盘延迟,提交模型再怎么优化都没用。
如果你只在热缓存下测,会得出"需要开 worker"的结论;只在冷盘下测,会得出"改这个没用"的结论。两个都对,也都片面。
例二:PgBouncer 的 pool_mode
| 压测方式 | transaction | session | 结论 |
|---|---|---|---|
| 背靠背(无思考时间) | 1558.6 tps | 1544.9 tps | 差 0.9%,看不出区别 |
| 500 客户端 + 50ms 思考时间 | 9865 tps | 394 tps | 差 25 倍 |
同一个中间件、同一份配置。差别是"客户端有没有空闲"——空闲时间被 session 模式绑定在服务端连接上,于是连接池退化成 20 条串行通道(20 / 0.051s ≈ 392 tps,与实测 394 吻合)。
这和缓存是同一类错误:你测的不是被测对象,而是被隐藏的等待。
5. 一份可直接用的冷/热对照脚本
#!/usr/bin/env bash
# 用法: ./scan_bench.sh "SELECT sum(abalance) FROM pgbench_accounts" 3
SQL="${1:?需要一条 SQL}"
ROUNDS="${2:-3}"
PSQL="psql -U postgres -d postgres -Atc"
for cold in 1 0; do
label=$([ "$cold" = 1 ] && echo "冷缓存" || echo "热缓存")
echo "=== $label ==="
for i in $(seq 1 "$ROUNDS"); do
if [ "$cold" = 1 ]; then
sync; echo 3 > /proc/sys/vm/drop_caches
cached=$(awk '/^Cached:/{printf "%.0f", $2/1024}' /proc/meminfo)
else
cached="-"
fi
t0=$(date +%s%3N)
$PSQL "$SQL" >/dev/null
t1=$(date +%s%3N)
echo "round$i: $((t1-t0)) ms Cached=${cached}MB"
done
done
判读:
- 冷、热两组的耗时差 < 20% → 你的数据集对内存来说太小,这轮测的是缓存,不是 I/O;把数据量放大或换机器。
- 热缓存组出现 GB/s 级带宽 → 有 L3 缓存没清掉,报告里要标注"热缓存"。
- 冷缓存组里
Cached仍接近数据量 → 清缓存失败,或数据集太小。
6. 报告压测结果需要说明的 8 个字段
缺任何一个,读者都无法判断你的数字属于哪一层:
- 表/索引大小,以及行数
shared_buffers与机器内存- 数据集 ÷ 内存 的倍数
- 是否预热、预热方式
- 缓存清理手段(
drop_caches?换机?没有?) - 清理后
Cached的实际值 - 重复次数与波动范围(不是单次结果)
- 失败事务数 / 是否发生换页(
kswapd0活动)
第 8 条容易被忽略:我用 eBPF 在一台 961MB 的机器上抓块 I/O,发现除了 dd 之外,kswapd0 提交了 144 次 I/O、apt-check 提交了 1588 次。换页和无关进程的 I/O 会直接污染你的"数据库慢"归因。
7. 六个错误
| 错误 | 实际情况 | 正确做法 |
|---|---|---|
| "我数据集 1GB,内存 32GB,够冷了" | 32GB 内存会把 1GB 全缓存 | 数据集要大于内存(且考虑宿主缓存) |
"我跑了 drop_caches,所以是冷读" | 只能清 guest,L3 清不到 | 用算术校验带宽,>5GB/s 就不要说冷 |
| "GB/s 说明我的盘很快" | 那是内存 | 10.9 GB/s 只能是缓存 |
| "只跑一次,结果 470ms" | 单次结果无法区分冷热 | 至少冷/热各 3 次,看波动 |
| "容器里清缓存很安全" | 清的是整台宿主机 | 共享机器上避免,或先告知机主并限流 |
| "参数调了 1.5 倍提升" | 可能只是热缓存 | 冷热两组都测,再下结论 |
8. 小结
压测数字的可信度只取决于一件事:数据当时在哪一层存储。
先做算术校验。表大小除以耗时,超过 5 GB/s 就不必再分析参数了,那是内存的速度,不是盘的速度。
然后冷热各测一遍。io_method 在热缓存下差 1.5 倍,在冷盘下差不到 2%。只测其中一种,结论必然是片面的。
最后把缓存状态写进报告。没有"数据集/内存倍数、清理手段、清理后 Cached"这三项,数字就不能采信——包括我上一版自己的数字。
数据来源与检索时间
方法论部分引用官方文档(检索时间 2026-11-10),全部数字来自 2026-11-09 至 11-10 的实测。
| 编号 | 来源 | URL | 用于 |
|---|---|---|---|
| [1] | PostgreSQL 18 文档《19.4 Resource Consumption》(shared_buffers、effective_io_concurrency 等默认值) | https://www.postgresql.org/docs/18/runtime-config-resource.html | §1 四层缓存中的 L1 参数、§4 例一的参数含义 |
| [2] | PostgreSQL 18 文档《F.32 pg_stat_statements》(shared_blks_hit/shared_blks_read 语义) | https://www.postgresql.org/docs/18/pgstatstatements.html | §2 ③ 命中率计算 |
| [3] | bpftrace 官方 README(探针类型与"开销极小"定位) | https://raw.githubusercontent.com/bpftrace/bpftrace/master/README.md | §6 第 8 条(用 eBPF 发现 kswapd0/apt-check 的 I/O) |
| [4] | PgBouncer 官方文档(pool_mode、default_pool_size 默认值) | https://www.pgbouncer.org/config.html | §4 例二的参数语义 |
实测环境
| 环境 | 规格 | 用于 |
|---|---|---|
| A | Windows 11 + Docker Desktop 29.4.3(WSL2),32 vCPU / 31.9 GiB | 10.9 GB/s 的"假磁盘"、热缓存 io_method 对照、PgBouncer 池模式 |
| B | Debian 12 KVM VPS,1 vCPU / 960 MB / 25GB,内核 6.1 | 250 MB/s 冷盘、冷盘 io_method 对照、命中率 0.2% |
| C | Ubuntu 24.04 KVM VPS,1 vCPU / 961 MB / 25GB,内核 6.8 | kswapd0/apt-check 的块 I/O 计数(bpftrace 0.20.2) |
核验说明
- 文中所有带宽均由"表大小 ÷ 实测耗时"计算得出,未使用任何推测值;
- 热缓存与冷盘两组
io_method对照分别为 3 次与 4 次重复,波动范围已在正文给出; - PgBouncer 的 25 倍差距来自 500 客户端 + 50ms 思考时间的一组实验;同一批测试中,另有一组 VPS 上的池模式数据被我判定无效并作废(重启命令未生效导致两轮都跑
transaction),未纳入这篇; - 未做的事:没有在物理机 + 独立 NVMe 上做第三组对照,因此"L3 缓存占比"仍属推断,而非实测分解。