「数据库能扛多少 QPS」这个问题,唯一诚实的答案来自压测,而不是参数表。但压测的陷阱极多:客户端先成为瓶颈、数据全在缓存里、参数配得比生产宽松——任何一个都会让结论失效。pgbench 是 PostgreSQL 自带的标准压测工具,本文从方法论讲到实操,帮你得到可复现、可解释的数字。
核心认知:压测的目标不是「跑出一个大数字」,而是「找到系统在当前配置下的瓶颈位置,并验证改动是否真的有效」。
一、压测类型与方法论
1.1 四类测试的区别
基准测试(Benchmark) 固定负载,测系统的能力上限与配置差异
负载测试(Load) 逐步加压到目标业务量,验证是否满足 SLA
压力测试(Stress) 超过预期负载,找出崩溃点与降级行为
稳定性测试(Soak) 长时间中等负载运行,暴露内存泄漏与连接耗尽
选错类型是最常见的浪费:用基准测试的结论去承诺业务容量,或用压力测试的崩溃点去否定一个配置。
1.2 一个可用的压测流程
1. 明确问题:要验证什么?扩容?改参数?换硬件?
2. 定义指标:tps、P95 延迟、错误率、资源利用率
3. 构造数据:数据量与分布要接近生产
4. 预热:让缓存与连接池进入稳态
5. 执行:至少三轮,取中位数
6. 分析:结合数据库与系统两层指标定位瓶颈
7. 复现:记录完整配置,确保他人能重现
二、pgbench 基础
2.1 初始化与缩放因子
# 初始化:-s 是缩放因子,1 单位约等于 10 万行
pgbench -i -s 100 -F 90 -U postgres mydb
# 含义
# -s 100 -> pgbench_accounts 约 1000 万行,约 1.35GB
# -F 90 -> fillfactor = 90,减少更新时的页分裂
# -i 会重建 pgbench_accounts / branches / tellers / history
-- 确认数据量与分布
SELECT count(*) FROM pgbench_accounts; -- 期望 100 * 100000
SELECT count(*) FROM pgbench_branches; -- 期望 100
SELECT pg_size_pretty(pg_total_relation_size('pgbench_accounts'));
缩放因子决定了工作集能否放进内存。-s 太小会让整个数据集落在 shared_buffers 中,测出的是纯内存性能。
2.2 内置脚本
# tpcb-like:TPC-B 风格,含 SELECT + UPDATE + INSERT + UPDATE
pgbench -b tpcb-like -c 32 -j 8 -T 300 mydb
# simple-update:去掉 SELECT,纯 UPDATE
pgbench -b simple-update -c 32 -j 8 -T 300 mydb
# select-only:只读
pgbench -b select-only -c 32 -j 8 -T 300 mydb
# 混合比例(多脚本按权重)
pgbench -b select-only@8 -b tpcb-like@2 -c 32 -j 8 -T 300 mydb
-b 后的 @n 表示权重,上例即 80% 只读、20% 读写。
2.3 连接与并发
pgbench -c 32 -j 8 -T 300 mydb
-c 并发客户端数(相当于并发连接数)
-j 工作线程数,通常取 CPU 核数,且 j <= c
-T 持续秒数(与 -t 事务数二选一)
-c 才是压力来源,-j 只是客户端侧的并行度。-j 设得过大反而会因线程切换降低客户端吞吐。
三、自定义脚本
3.1 -f 与 \set
-- bench.sql
\set aid random(1, 100000 * :scale)
\set bid random(1, 1 * :scale)
\set delta random(-5000, 5000)
BEGIN;
UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid;
SELECT abalance FROM pgbench_accounts WHERE aid = :aid;
UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid = :bid;
INSERT INTO pgbench_history (tid, bid, aid, delta, mtime)
VALUES (:bid, :bid, :aid, :delta, CURRENT_TIMESTAMP);
END;
pgbench -f bench.sql -c 32 -j 8 -T 300 mydb
3.2 变量与 :scale
pgbench 内置变量::scale(缩放因子)、:client_id(客户端编号)、:random_seed。函数有 random(lo, hi)、random_exponential(lo, hi, p)、random_gaussian(lo, hi, p)。
random(1, :scale * 100000) 均匀分布,命中均匀
random_gaussian(1, 100000, 2.0) 高斯分布,模拟热点
random_exponential(1, 100000, 2.0) 指数分布,长尾
用 random_gaussian 或 random_exponential 模拟真实业务的热点分布,比均匀分布更接近生产。
3.3 事务与权重
-- 脚本内可写多个事务,用 \set 决定走哪条分支
\set r random(1, 100)
\if :r <= 80
SELECT abalance FROM pgbench_accounts WHERE aid = :aid;
\else
UPDATE pgbench_accounts SET abalance = abalance + 1 WHERE aid = :aid;
\endif
pgbench 也支持 --latency-limit 与 --rate 配合限流模式使用。
四、关键参数详解
pgbench -c 32 -j 8 -T 300 -M prepared -P 10 --rate 1000 \
--latency-limit 50 -n -U postgres mydb
-c, --client=NUM 并发客户端数
-j, --jobs=NUM 工作线程数
-t, --transactions=NUM 每客户端执行的事务数
-T, --time=NUM 持续秒数
-M, --protocol=MODE simple / extended / prepared(默认 simple)
-f, --file=FILENAME 自定义脚本
-b, --builtin=NAME 内置脚本
-n, --no-vacuum 跳过初始化前的 VACUUM
-P, --progress=NUM 每 N 秒打印进度
--rate=NUM 目标 tps,限流模式(泊松到达)
--latency-limit=NUM 超过此毫秒数的延迟不计入统计
--max-tries=NUM 失败事务的重试次数
-M 的选择很关键:
simple 每个事务一次简单查询协议,含解析开销
extended 扩展查询协议,每次重新解析
prepared 使用预备语句,跳过解析,最接近生产 ORM 的行为
测 ORM 场景应用 -M prepared;测朴素驱动可用 simple。三者 tps 可能相差 20% 以上。
五、结果解读
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 100
query mode: prepared
number of clients: 32
number of threads: 8
duration: 300 s
number of transactions actually processed: 4821356
latency average = 1.988 ms
latency stddev = 3.121 ms
initial connection time = 214.335 ms
tps = 16068.918250 (without initial connection time)
latency average是每个事务的平均延迟,不是查询延迟。initial connection time是建立全部连接的总耗时,不计入 tps,但连接建立慢往往暗示max_connections或认证配置有问题。tps是稳态吞吐,应与-P的进度输出对比,确认没有明显衰减。
# 拿到分位数(需要 --progress 之外的额外工具或解析)
pgbench -c 32 -j 8 -T 300 -P 10 --rate 1000 --latency-limit 50 mydb
# 输出中的 "number of transactions skipped" 表示超时被丢弃的事务数
六、避免压测误区
1. 客户端瓶颈:pgbench 自己 CPU 打满,测出的是客户端上限
-> 用多个客户端机器,或观察 pgbench 进程的 CPU 占用
2. 缓存假象:数据全在 shared_buffers 或 OS page cache 中
-> 用足够大的 -s,或压测前清空缓存(重启实例)
3. 参数宽松:生产用 fsync=on,压测却关了
-> 压测配置必须与生产一致
4. 只测只读:生产是读写混合,只读测试高估容量
-> 按真实读写比构造脚本
5. 忽略连接建立:连接池预热后测得的 tps 远高于冷启动
-> 报告要注明是否预热
6. 单轮结论:一次运行受随机波动影响
-> 至少三轮取中位数
-- 检查压测期间是否真的在读磁盘(blks_read 增长说明没全命中缓存)
SELECT blks_read, blks_hit,
round(100.0 * blks_hit / greatest(blks_hit + blks_read, 1), 2) AS hit_pct
FROM pg_stat_database WHERE datname = 'mydb';
七、瓶颈定位
7.1 pg_stat_statements
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
SELECT query, calls, mean_exec_time, total_exec_time,
rows, shared_blks_read, shared_blks_hit
FROM pg_stat_statements
ORDER BY total_exec_time DESC LIMIT 20;
压测后 shared_blks_read 高的语句说明没有命中缓存,是 IO 瓶颈来源;mean_exec_time 高的语句是 CPU 或锁瓶颈候选。
7.2 pg_stat_activity 与等待事件
-- 压测中另开一个会话采样
SELECT wait_event_type, wait_event, count(*)
FROM pg_stat_activity
WHERE state = 'active' AND backend_type = 'client backend'
GROUP BY 1, 2 ORDER BY 3 DESC;
LWLock / WALWriteLock 写 WAL 竞争,检查 checkpoint 与 synchronous_commit
Lock / transactionid 行锁或事务 ID 等待,检查长事务与热点行
IO / DataFileRead 磁盘读等待,缓存不足或索引缺失
Client / ClientRead 客户端处理慢,可能是客户端瓶颈
CPU(wait_event 为空) 纯计算,考虑优化 SQL 或加 CPU
7.3 系统层 iostat
iostat -x 1 10
# 关注 %util(设备饱和度)、await(平均 IO 等待毫秒)、r/s 与 w/s
vmstat 1
# 关注 r(运行队列长度,超过核数说明 CPU 饱和)、si/so(换页)
数据库层与系统层的指标必须对照看:数据库显示 IO 等待,iostat 显示 %util 接近 100%,才能确认是磁盘饱和。
八、环境隔离与可重复性
1. 压测机与数据库机分离,避免互相争抢 CPU
2. 关闭其他负载:备份、autovacuum 大表、监控采集降频
3. 固定配置:记录 postgresql.conf 的全部非默认项
4. 固定数据:同一份初始化数据与同一随机种子
5. 预热:先跑 30 秒预热,再开始计时
6. 记录硬件:CPU 型号核数、内存、磁盘类型与 RAID 级别
# 保存完整配置快照
psql -c "SELECT name, setting, unit FROM pg_settings WHERE source <> 'default';" \
> config-snapshot.txt
九、报告模板
=== PostgreSQL 压测报告 ===
日期 / 执行人:
目标问题:
[环境]
CPU / 内存 / 磁盘 / 文件系统:
PostgreSQL 版本 / 关键参数(shared_buffers, work_mem, fsync, wal_level):
pgbench 版本 / 客户端机器配置:
[数据集]
缩放因子 -s: 实际行数:
数据总量: 是否预热:
[负载]
脚本 / 并发 -c / 线程 -j / 时长 -T / 协议 -M:
读写比: 限流 --rate:
[结果]
tps(三轮): 中位数:
latency average / stddev:
错误 / 超时事务数:
[瓶颈分析]
数据库层: 系统层:
结论与建议:
常见问题(FAQ)
pgbench 的 tps 是不是数据库的真实上限
不是。它受客户端机器、网络、-j 设置与协议模式影响。若 pgbench 进程 CPU 打满,测出的是客户端上限而非数据库上限。判断方法是观察客户端 CPU 与数据库 CPU 谁先饱和,并用多台客户端机器施压。
缩放因子该取多大
原则是让数据集明显大于 shared_buffers,否则测的是纯内存性能。经验公式:-s 使数据量达到内存的 2 到 4 倍,且至少 10 倍于 shared_buffers。若要测缓存命中场景,则反过来让数据全部装进内存,但要明确说明这是内存内测试。
为什么压测结果远好于生产表现
常见原因有三:压测数据全在缓存、压测配置更宽松(fsync 关闭或 synchronous_commit = off)、压测负载分布均匀而生产有明显热点。让压测贴近生产需要在数据分布、参数配置、读写比例三方面都对齐。
-M prepared 与 simple 差多少
取决于语句复杂度与连接复用。简单语句差距约 5% 到 15%,复杂语句可达 20% 以上,因为 prepared 跳过了每次解析。生产中的 ORM 与连接池通常复用预备语句,因此测容量应用 -M prepared。
限流模式有什么用
--rate 让 pgbench 按泊松过程发送事务,模拟真实到达率而非「能多快跑多快」。它用于负载测试:设定目标 tps,观察在该速率下延迟是否达标。配合 --latency-limit 可以把超时事务排除出统计,得到「有效吞吐」。
压测时该不该开 autovacuum
该开,但要注意它可能干扰结果。生产环境 autovacuum 是开着的,压测时关闭会让表膨胀并测出偏高的 tps。建议保持开启,同时在报告中记录 autovacuum 的活动情况;若发现它在压测中频繁触发,说明表膨胀速度快,这本身就是一个需要报告的问题。
相关阅读
- PostgreSQL 性能调优 — 参数配置与资源配置
- PostgreSQL 监控与诊断体系 — 用视图定位瓶颈
- PostgreSQL 查询优化实战 — 慢查询分析与执行计划
- PostgreSQL 统计信息与 ANALYZE — 统计信息对计划的影响
- PostgreSQL 连接池 PgBouncer — 连接数对压测结果的影响
- PostgreSQL 专题导航
延伸阅读
- PostgreSQL 事务、隔离级别与锁 — 锁等待导致的延迟毛刺
- PostgreSQL 存储内部 — 页结构与缓存命中率
完整示例(一键复制)
# ========== 1. 初始化数据集 ==========
pgbench -i -s 100 -F 90 -U postgres mydb
psql -c "SELECT count(*) FROM pgbench_accounts;"
psql -c "SELECT pg_size_pretty(pg_total_relation_size('pgbench_accounts'));"
# ========== 2. 内置脚本基准测试(三轮取中位数) ==========
for i in 1 2 3; do
pgbench -b tpcb-like -c 32 -j 8 -T 300 -M prepared -P 30 mydb
done
# ========== 3. 混合读写(80% 只读) ==========
pgbench -b select-only@8 -b tpcb-like@2 -c 32 -j 8 -T 300 -M prepared mydb
# ========== 4. 限流负载测试 ==========
pgbench -f bench.sql -c 32 -j 8 -T 300 -M prepared \
--rate 1000 --latency-limit 50 -P 30 mydb
# ========== 5. 系统层采样(另开终端) ==========
iostat -x 1 30 > iostat.log &
vmstat 1 30 > vmstat.log &
-- ========== 6. 数据库层采样 ==========
SELECT blks_read, blks_hit,
round(100.0 * blks_hit / greatest(blks_hit + blks_read, 1), 2) AS hit_pct
FROM pg_stat_database WHERE datname = 'mydb';
SELECT wait_event_type, wait_event, count(*)
FROM pg_stat_activity
WHERE state = 'active' AND backend_type = 'client backend'
GROUP BY 1, 2 ORDER BY 3 DESC;
SELECT query, calls, mean_exec_time, shared_blks_read
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20;
-- ========== 7. 保存配置快照 ==========
SELECT name, setting, unit FROM pg_settings WHERE source <> 'default';
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。