暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

一次 PostgreSQL 大量 COMMIT 卡顿的排障复盘:最终定位到虚拟块存储瞬时抖动

原创 瓶盖吃芥末 4天前
212

一次 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。

所以排查短时数据库抖动时:

一分钟平均值只能作为趋势证据,无法反映秒级峰值。

awaitaqu-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 本身。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论