一次 PostgreSQL 大量 COMMIT 卡顿的排障复盘:最终定位到虚拟块存储瞬时抖动
环境:PostgreSQL 12.x
场景:生产数据库短时间出现大量 SQL、DML 和 COMMIT 同时变慢
结论:高度疑似虚拟块设备后端存储瞬时 I/O latency spike
说明:本文已对主机名、IP、账号、业务库名、目录、业务表名等信息做脱敏处理。
声明:报告整理与排版由AI辅助整理
1. 故障现象
某生产 PostgreSQL 实例在晚高峰期间出现一次短时间性能抖动。
数据库日志中同时出现了大量:
- 普通 SELECT;
- INSERT;
- UPDATE;
- COMMIT;
执行耗时普遍达到:
7 ~ 15 秒
其中比较典型的是大量 COMMIT:
COMMIT duration: 15xxx ms COMMIT duration: 14xxx ms COMMIT duration: 12xxx ms COMMIT duration: 10xxx ms ...
更加异常的是:
大量不同会话的 COMMIT 几乎集中在同一毫秒级时间窗口完成。
例如日志表现为:
20:21:13.619 20:21:13.620 20:21:13.630 20:21:13.631
不同事务前面分别等待了 7 秒、10 秒、12 秒、15 秒,但最终却在极短时间内集中返回。
这类现象通常意味着:
多个数据库会话 ↓ 等待同一个公共资源 ↓ 公共资源恢复 ↓ 大量事务同时被释放
因此排查方向不能只盯某一条 SQL,而应该优先检查:
WAL 存储 同步复制 checkpoint OS I/O 虚拟化平台
2. 第一判断:是不是同步复制拖慢 COMMIT
首先检查:
SHOW synchronous_commit;
SHOW synchronous_standby_names;
结果类似:
synchronous_commit = on synchronous_standby_names = ''
这里非常关键。
synchronous_commit = on 表示事务提交需要等待本地 WAL 满足持久化要求。
但是:
synchronous_standby_names = ''
说明没有配置同步 standby。
因此 COMMIT 的主要链路是:
COMMIT ↓ 生成 WAL ↓ 写 pg_wal ↓ WAL flush / fsync ↓ 本地存储持久化 ↓ 返回客户端
而不是:
COMMIT ↓ 等待同步备库 ACK
所以:
SyncRep / 同步复制方向基本排除。
排查重点转向本地 WAL I/O 和底层存储。
3. CPU 是否出现瓶颈
通过 sar -u 回看故障时间窗口,CPU 大致如下:
%user ≈ 28% %system ≈ 2% %iowait ≈ 2%~3% %steal ≈ 0 %idle ≈ 67%
服务器为多核环境。
从这些指标看:
CPU 并没有打满
同时:
%steal ≈ 0
说明虚拟机也不存在明显的 CPU 被宿主机抢占问题。
因此:
CPU 瓶颈基本可以排除。
4. run queue 也没有明显异常
继续查看:
sar -q
结果类似:
runq-sz ≈ 6~7 load1 ≈ 5~6
在十几核 CPU 环境中,该 run queue 并不算严重。
如果是典型 CPU 饱和,往往会看到:
runq-sz >> CPU 数量
本次不存在这种现象。
所以:
CPU 调度也不是主要矛盾。
5. 真正异常出现在磁盘 I/O
继续查看:
sar -d -p
故障前一分钟:
DEV aqu-sz await %util vdb 0.04 0.57 11% vdd 0.04 0.48 7% data-lv 0.19 0.69 17%
表现正常。
但是覆盖故障的一分钟:
DEV aqu-sz await %util vdb 8.76 38.62 35% vdd 0.21 1.66 8% data-lv 10.11 35.79 32%
变化非常明显。
5.1 vdb 延迟暴涨
await: 0.57 ms ↓ 38.62 ms
约增加:
68 倍
5.2 vdb 队列明显堆积
aqu-sz: 0.04 ↓ 8.76
约增加:
200 多倍
这说明问题并不是单纯某几个 I/O 稍微变慢,而是:
底层 I/O 请求出现了明显排队。
5.3 同期另一块盘基本正常
另一个虚拟块设备:
vdd
同期:
await: 0.48 ms ↓ 1.66 ms
总体正常。
这说明异常并不是所有数据盘同步恶化,而主要集中在:
vdb
这一点对缩小故障范围非常重要。
6. 为什么 sar 只有 38ms,却能让 COMMIT 卡 15 秒
这里非常容易产生误判。
sar 当前是分钟级采样。
例如:
20:22:01
这一行实际上统计的是:
20:21:01 ↓ ↓ 这一整分钟 ↓ 20:22:01
但数据库真正明显异常的时间可能只有:
20:21:06 ~ 20:21:13
也就是说可能出现:
20:21:01 ~ 20:21:06 正常 20:21:06 ~ 20:21:13 严重 I/O stall 20:21:13 ~ 20:22:01 恢复正常
最后整个 60 秒平均下来:
await = 38ms
并不代表瞬时峰值只有 38ms。
所以排查短时数据库抖动时:
一分钟平均值只能作为趋势证据,无法反映秒级峰值。
但 await 和 aqu-sz 同时出现数量级变化,已经足以说明这一分钟确实发生过明显 I/O 排队和延迟。
7. 数据盘结构继续往下追
文件系统挂载类似:
/app ↓ LVM Logical Volume
通过:
lvs --segments
可以看到数据卷实际由两块虚拟盘线性拼接:
data-lv ├── 前约 2TiB → /dev/vdb └── 后约 1.5TiB → /dev/vdd
这里用的是:
linear
而不是:
stripe
这个差别很重要。
LVM linear 的含义是:
逻辑卷前半部分 → vdb 逻辑卷后半部分 → vdd
新增第二块盘只是:
扩容容量
不会自动做到:
I/O 均衡 数据均衡 热点均衡
原来已经存在于 vdb 的文件不会因为后来扩容而自动搬迁到 vdd。
8. PostgreSQL 的 WAL 到底在哪块盘
数据库目录位于:
/app/.../data
pg_wal 也没有单独挂载或软链接:
/app/.../data/pg_wal
因此:
PGDATA ↓ /app pg_wal ↓ /app
两者都位于同一个 LVM。
但 /app 同时跨 vdb 和 vdd,因此还需要继续确认:
WAL 的物理 extent 实际位于哪块底层盘。
9. 用 XFS extent + LVM mapping 定位 WAL 物理设备
首先查看当前 WAL:
SELECT pg_walfile_name(pg_current_wal_lsn());
然后:
xfs_bmap -v /app/.../pg_wal/<WAL_FILE>
得到类似:
BLOCK-RANGE: 1152149336..1152182103
再结合:
dmsetup table
可以得到 LVM 分界:
0 ~ 4299030527 → vdb 4299030528 以后 → vdd
由于:
1152149336 < 4299030528
因此当前 WAL segment 明确位于:
vdb
10. 对 WAL segment 进行批量抽查
继续对 pg_wal 目录中的 WAL segment 批量执行 xfs_bmap。
抽查结果类似:
WAL_SEGMENT_001 1172465368..1172498135 vdb WAL_SEGMENT_002 1172498136..1172530903 vdb WAL_SEGMENT_003 1172530904..1172563671 vdb WAL_SEGMENT_004 1172563672..1172596439 vdb ...
连续抽查的大量 WAL segment 均落在:
vdb
而且 extent 位置远低于 vdb/vdd 的分界点。
因此可以比较有把握地判断:
当前 PostgreSQL WAL segment 池主要物理位于 vdb。
考虑 PostgreSQL 会 recycle WAL segment,这对判断故障期间 WAL 的实际存储位置具有较强参考意义。
11. 到这里证据链已经基本形成
数据库侧:
普通 SELECT INSERT UPDATE COMMIT ↓ 同时出现 7~15 秒延迟
大量 COMMIT:
20:21:13.xxx 集中完成
配置:
synchronous_commit = on synchronous_standby_names = ''
说明:
COMMIT 依赖本地 WAL flush
存储侧:
vdb await 0.57ms → 38.62ms vdb aqu-sz 0.04 → 8.76
而:
vdd 基本正常
文件布局:
pg_wal ↓ /app ↓ LVM ↓ 实际 WAL extent ↓ vdb
因此完整故障链可以推导为:
vdb 后端存储瞬时延迟 ↓ I/O queue 堆积 ↓ ┌──────┴──────┐ │ │ ▼ ▼ 数据文件 I/O WAL I/O │ │ ▼ ▼ 查询/DML变慢 WAL flush变慢 │ ▼ COMMIT等待 │ 底层 I/O 恢复 │ ▼ 大量事务集中完成
12. 为什么普通 SELECT 也会慢
这也是判断“不是单纯 COMMIT/WAL 问题”的一个关键点。
PostgreSQL 的:
PGDATA
同样位于 /app。
因此如果 vdb 后端存储抖动:
不仅 pg_wal 受影响
落在 vdb 上的数据文件访问也会同时受到影响。
于是就可能出现:
DataFileRead DataFileWrite WALWrite WALSync
同时不同程度变慢。
这解释了为什么事故期间不仅有 COMMIT 慢,还有:
普通 SELECT INSERT UPDATE
同步变慢。
13. 为什么大量 COMMIT 会同时恢复
PostgreSQL 存在 group commit 机制。
可以简单理解为:
事务 A ─┐ 事务 B ─┤ 事务 C ─┤ 事务 D ─┤ ↓ WAL flush ↓ 存储卡顿 │ │ 等待 │ ▼ 存储恢复 ↓ WAL flush 完成 ↓ 多个事务集中返回
因此:
多个不同事务 等待时间不同 却在同一个毫秒级窗口完成
并不奇怪。
反而说明:
多个事务很可能共同依赖一个被阻塞的 WAL 持久化条件。
14. 内核为什么没有报错
继续检查:
journalctl -k dmesg
事故窗口没有发现:
I/O timeout I/O error reset abort filesystem error device-mapper error
这并不和存储抖动结论冲突。
因为:
存储 latency spike
和:
设备 timeout / reset
完全是两个级别的问题。
如果后端存储只是:
I/O 本来 1ms 突然变成几百毫秒甚至几秒 但最终仍然成功返回
Linux 内核完全可能不记录任何:
I/O error timeout reset
所以:
没有 dmesg 错误,只能排除明显硬故障,不能排除性能抖动。
15. 虚拟化环境是最后一层关键
设备类型为:
virtio_blk
说明 Guest OS 看到的 vdb 并不是物理盘。
完整路径其实是:
PostgreSQL ↓ XFS ↓ LVM ↓ vdb ↓ virtio_blk ↓ Hypervisor ↓ 后端云盘 / SAN / Ceph / 分布式存储
Guest OS 只能确认:
vdb latency 上升 vdb queue 上升
但是看不到:
- 宿主机 I/O;
- 云盘限速;
- 存储 QoS;
- 后端卷 latency;
- Ceph/SAN 节点异常;
- 存储网络抖动;
- 数据迁移;
- snapshot;
- backup;
- rebalance。
因此最终根因闭环必须由平台侧继续确认。
16. checkpoint 是否是根因
事故发生时数据库正处于 checkpoint 周期中。
但上一轮 checkpoint 日志显示:
sync 时间很短
并没有直接证据表明 checkpoint 本身卡住。
因此比较合理的判断是:
checkpoint 可能在故障时段增加了后台 I/O,但当前没有足够证据把 checkpoint 本身定为根因。
更准确的关系可能是:
checkpoint ↓ 增加后台 I/O ↓ 底层存储本身发生 latency spike ↓ 故障表现被放大
因此 checkpoint 更适合作为:
潜在放大因素
而不是:
确定根因
17. 最终结论
综合:
- PostgreSQL 日志;
- COMMIT 执行时间;
- COMMIT 集中恢复时间;
synchronous_commit;synchronous_standby_names;- CPU;
- run queue;
sar -d;- LVM mapping;
- XFS extent;
- WAL 物理位置;
可以形成如下结论:
本次 PostgreSQL 系统性性能抖动,高度疑似由虚拟块设备 vdb 对应的后端存储发生短时 I/O latency spike 引起。
该异常导致:
数据文件 I/O 变慢 + WAL flush 延迟
进一步造成:
SELECT / DML / COMMIT 同时出现 7~15 秒级延迟
当存储恢复后:
大量事务在极短时间内集中完成
与事故日志完全吻合。
18. 为什么这里只能写“高度疑似”
数据库侧和 Guest OS 已经基本闭环:
SQL慢 ↓ COMMIT慢 ↓ vdb latency明显升高 ↓ WAL 位于 vdb
但是 Guest OS 看不到:
vdb 背后的真正物理存储
所以最后仍然应该让虚拟化或存储平台检查故障窗口:
故障前后约 20~30 秒
重点检查:
Read latency Write latency Flush latency fsync latency Queue depth IOPS Throughput QoS Throttling 宿主机 I/O 存储池状态 存储节点异常 卷迁移 快照 备份 rebalance
尤其注意:
不要只问“有没有 I/O error”,而要查 latency。
这是排查此类故障时非常容易被忽略的一点。
19. 后续优化建议
19.1 OS I/O 监控改成秒级
一分钟 sar 对十秒级故障太粗。
建议增加:
1s / 5s / 10s
级别采集。
重点指标:
await r_await w_await aqu-sz IOPS throughput
19.2 增加 PostgreSQL wait_event 采集
故障时直接看:
SELECT
pid,
state,
wait_event_type,
wait_event,
query
FROM pg_stat_activity
WHERE state <> 'idle';
重点观察:
WALWrite WALSync DataFileRead DataFileWrite SyncRep Lock
如果当时能抓到:
大量 WALSync / WALWrite
证据会更加直接。
19.3 考虑将 pg_wal 与数据文件分离
当前:
PGDATA pg_wal
位于同一逻辑卷。
意味着一次存储抖动可以同时影响:
数据页访问 + 事务提交
可以评估:
PGDATA → 数据卷 pg_wal → 独立低延迟 WAL 卷
这样既有利于性能隔离,也更利于故障定位。
19.4 注意 LVM linear 扩容的性能含义
LVM linear:
vdb + vdd
只是把容量拼起来。
它不会:
自动均衡历史数据 自动均衡 WAL 自动均衡热点
因此:
新盘很空 旧盘仍然很热
是完全可能的。
这次 WAL segment 大量位于 vdb,就是一个典型例子。
20. 类似问题的快速排查 SOP
遇到:
大量 SQL 同时慢 大量 COMMIT 10s+ 多个会话同时恢复
建议按下面顺序:
1)先看数据库等待
SELECT
pid,
wait_event_type,
wait_event,
state,
query
FROM pg_stat_activity
WHERE state <> 'idle';
2)确认同步复制
SHOW synchronous_commit;
SHOW synchronous_standby_names;
3)实时看磁盘
iostat -xmd 1
重点:
await w_await aqu-sz %util
4)看 CPU
vmstat 1 sar -u sar -q
5)把 PGDATA 一直追到底层设备
findmnt lsblk lvs dmsetup
不要只停留在:
/app
一定要继续查:
/app ↓ LVM ↓ vdb/vdd
6)必要时确认 WAL extent
XFS:
xfs_bmap -v WAL_FILE
结合:
dmsetup table
可以把 WAL 精确映射到底层虚拟盘。
7)最后让平台侧查真正后端存储
提供:
精确故障时间 虚拟机 virtio 设备 await queue WAL 所在设备
让平台侧继续查:
宿主机 云盘 SAN Ceph QoS throttling storage latency
21. 总结
这次故障最大的启示不是“磁盘慢了”,而是:
大量 COMMIT 在同一时间集中恢复,本身就是识别公共资源等待的重要信号。
进一步结合:
synchronous_commit WAL 位置 sar I/O LVM XFS extent virtio
可以把问题从:
数据库慢
一步步缩小到:
WAL flush ↓ 特定 LVM extent ↓ 特定 virtio 块设备 ↓ 后端存储 latency spike
这类故障如果只看 SQL,很容易误判成:
SQL优化问题 锁问题 checkpoint问题
真正有效的方法,是把 PostgreSQL 等待链路和 OS I/O 链路放在一起分析。
一句话总结
当 PostgreSQL 出现大量 COMMIT 10 秒级卡顿并在同一毫秒级时间窗口集中恢复时,要高度警惕 WAL 所在底层存储出现瞬时 I/O stall,而不应只盯 SQL 本身。




