KernelRanch
返回文章列表

冷缓存、热缓存与"假磁盘":同一台机器为什么能测出 10.9 GB/s 和 250 MB/s

2026-11-10 #数据库

数据库压测里最贵的错误不是调错参数,而是不知道数据从哪来。同一条 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. 四层缓存:你的读到底命中了谁

层名字典型带宽怎么绕开
L1PostgreSQL shared_buffers10+ GB/s让数据集远大于 shared_buffers
L2客户机 OS page cache数 GB/sdrop_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/sL2/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

缓存状态syncworker结论
热(WSL2,数据被宿主缓存兜住)707 ms470 msworker 快 1.5 倍
冷(VPS,1025MB 表 > 961MB 内存)3471–4116 ms3446–4113 ms无差异(<2%)

为什么?热缓存时"读"本身极快,瓶颈变成 I/O 提交路径(同步模式一次只能有一个在途请求,异步能重叠);冷盘时瓶颈是磁盘延迟,提交模型再怎么优化都没用。

如果你只在热缓存下测,会得出"需要开 worker"的结论;只在冷盘下测,会得出"改这个没用"的结论。两个都对,也都片面。

例二:PgBouncer 的 pool_mode

压测方式transactionsession结论
背靠背(无思考时间)1558.6 tps1544.9 tps差 0.9%,看不出区别
500 客户端 + 50ms 思考时间9865 tps394 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 个字段

缺任何一个,读者都无法判断你的数字属于哪一层:

  1. 表/索引大小,以及行数
  2. shared_buffers 与机器内存
  3. 数据集 ÷ 内存 的倍数
  4. 是否预热、预热方式
  5. 缓存清理手段(drop_caches?换机?没有?)
  6. 清理后 Cached 的实际值
  7. 重复次数与波动范围(不是单次结果)
  8. 失败事务数 / 是否发生换页(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 例二的参数语义

实测环境

环境规格用于
AWindows 11 + Docker Desktop 29.4.3(WSL2),32 vCPU / 31.9 GiB10.9 GB/s 的"假磁盘"、热缓存 io_method 对照、PgBouncer 池模式
BDebian 12 KVM VPS,1 vCPU / 960 MB / 25GB,内核 6.1250 MB/s 冷盘、冷盘 io_method 对照、命中率 0.2%
CUbuntu 24.04 KVM VPS,1 vCPU / 961 MB / 25GB,内核 6.8kswapd0/apt-check 的块 I/O 计数(bpftrace 0.20.2)

核验说明

  • 文中所有带宽均由"表大小 ÷ 实测耗时"计算得出,未使用任何推测值;
  • 热缓存与冷盘两组 io_method 对照分别为 3 次与 4 次重复,波动范围已在正文给出;
  • PgBouncer 的 25 倍差距来自 500 客户端 + 50ms 思考时间的一组实验;同一批测试中,另有一组 VPS 上的池模式数据被我判定无效并作废(重启命令未生效导致两轮都跑 transaction),未纳入这篇;
  • 未做的事:没有在物理机 + 独立 NVMe 上做第三组对照,因此"L3 缓存占比"仍属推断,而非实测分解。