ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

PostgreSQL性能排查:一文掌握wait_event等待事件分析与实战

PostgreSQL性能排查:一文掌握wait_event等待事件分析与实战 PostgreSQL 慢下来的时候我习惯第一时间打开pg_stat_activity看wait_event。这个字段的全称是等待事件它会直接告诉你每个后端进程此刻到底卡在哪里——是在等锁、等磁盘 IO、等 WAL 刷盘还是在等客户端响应。项目上线这几年我排查过上百次数据库抖动超过一半的结论不是靠慢 SQL 日志猜出来的而是靠 wait_event 一锤定音。这篇东西适合所有在用 PostgreSQL 或者正准备上 PG 的人不管你是 DBA、后端开发还是运维只要遇到过“数据库明明活着但查询就是不动”的诡异场景wait_event 就是那个帮你打开黑盒的钥匙。先说个前提我默认你已经有一个跑起来的 PostgreSQL 实例版本最好在 13 以上太低版本的事件字段会少很多。没装过的话随便找个安装教程把 PG 装好再回来看这篇文章效果更佳。1. 先把 wait_event 是什么搞明白1.1 从 pg_stat_activity 开始PostgreSQL 把每个后端进程的实时状态暴露在pg_stat_activity系统视图里其中两个字段和等待事件直接相关wait_event_type表示等待事件的大类wait_event表示具体的等待点。你可以直接跑这条 SQL 看当前正在等待的会话SELECT pid, usename, state, wait_event_type, wait_event, query FROM pg_stat_activity WHERE state active AND wait_event IS NOT NULL ORDER BY query_start;看起来很简单对吧但很多人第一次看到输出就懵了因为wait_event里的值五花八门什么ClientRead、DataFileRead、transactionid、buffer_mapping都有。我举个例子你立刻就懂了。开第一个会话执行SELECT pg_sleep(60);然后第二个会话查pg_stat_activity你会看到这个会话的wait_event_type是Timeoutwait_event是PgSleep。这说明它并不是卡住了而是主动睡了 60 秒。再换一个经典场景会话 ABEGIN; UPDATE t SET num num 1 WHERE id 1; -- 不提交会话 BUPDATE t SET num num 2 WHERE id 1;此时查pg_stat_activity会话 B 的wait_event_type通常是Lockwait_event是transactionid。意思是它在等一个事务结束因为同一行的更新被事务 A 锁住了。这两个例子合在一起就是 wait_event 的核心价值它回答了一个查询“当前这一瞬间到底在等什么”。慢 SQL 日志只能告诉你“花了多久”执行计划只能告诉你“它打算怎么跑”但 wait_event 能告诉你“它现在为什么跑不动”。1.2 为什么 wait_event 比慢 SQL 更值得关注很多人的排查习惯是数据库变慢了先去翻慢查询日志找到执行时间最长的 SQL然后对着表加索引、改 SQL。这个思路没错但在某些场景下完全无效。比如一个UPDATE语句已经跑了好几分钟你去 explain analyze它可能输出的是一个极其简单的计划成本极低。因为这条语句并不是真的在执行而是被别的事务锁住了它在等待。这种情况下加索引没用改 SQL 也没用你必须找到那个持有锁不释放的事务。而 wait_event 会直接告诉你它卡在锁等待上pg_blocking_pids()能一步到位告诉你凶手是谁。再比如一个查询频繁出现DataFileRead事件说明它一直在从磁盘读数据块那就应该查缓存命中率、查是不是全表扫描而不是盲目加内存。从 Oracle 换过来的人可能觉得这个有点眼熟它确实有点像v$session_wait里的event但 PG 这个视图更简单、更直白定位效率更高。2. wait_event 的分类体系别被几十种事件吓住2.1 WaitEventType 的几大类PostgreSQL 的等待事件数量很多尤其是新版本光官方文档里的表格就好几十行。但你不用全都背下来先记住几个大类就够了。以 PG 13 之后的版本为例wait_event_type主要包含这些类型类型含义常见场景Activity进程处于某种“活动性等待”通常是在等待调度或客户端交互空闲连接、等客户端发 SQLIO等待磁盘/存储 IO 完成读数据文件、写 WAL、临时文件读写IPC进程间通信包括部分 latch、管道、共享内存信号后台进程同步、等待通知Lock数据库层面的重量级锁表锁、行锁、事务锁等待LWLock轻量级锁保护内存里的共享结构buffer 映射、WAL 插入缓冲等BufferPin页面 pin 等待物理访问数据块时短暂等待热点数据页并发访问Timeout主动的定时等待pg_sleep()、定时任务Client与客户端网络交互相关较新版本向客户端写结果时阻塞Extension扩展自定义的等待事件第三方扩展注册的事件Recovery恢复过程中的等待备份恢复相关恢复 WAL、归档拉取一定要分清Lock和LWLock的区别。Lock是事务级的重量级锁可能被一个事务长时间持有比如未提交的UPDATE会一直占着行锁后面的事务只能排队。而LWLock是保护内存结构的轻量级锁正常情况下持有时间极短但如果频繁出现LWLock等待说明某个共享数据结构成了热点背后往往牵连着高并发压力或配置问题。2.2 常见事件速查表下面这张表我日常排查时用得最多整理在这里方便你直接抄wait_event所属大类含义优先处理方向ClientReadActivity / Client等待客户端发送下一条 SQL常出现在空闲连接和 idle in transaction检查连接池与事务是否长期不提交ClientWriteActivity / Client向客户端发送结果时网络缓冲满客户端消费慢检查结果集大小、客户端处理速度、网络带宽DataFileReadIO从表或索引的数据文件读块到共享缓冲区查缓存命中率、执行计划、索引情况DataFileWriteIO写数据文件通常集中在后台进程关注检查点频率、IO 负载DataFileExtendIO表文件需要扩展新块并发插入场景常见关注扩展锁BufFileRead / BufFileWriteIO临时文件读写排序或 hash join 时发生检查 work_mem、是否触发磁盘排序WALWrite / WALWriteSyncIOWAL 日志写盘或 fsync检查存储延迟、提交频率、checkpoint 参数Lock 下的 relation / transactionid / tupleLock重量级锁等待具体看 pg_locks 的 locktype用 pg_blocking_pids 找阻塞源头buffer_mappingLWLock共享缓冲区映射表竞争热点页、shared_buffers 配置、全表扫描并发WALInsertLockLWLock多个事务同时向 WAL 缓冲插入记录高并发提交、批量提交优化ProcArrayLockLWLock事务快照/事务数组相关竞争长事务、高并发事务开始和结束PgSleepTimeout主动 sleep属于正常的等待检查应用是否故意限速或跑测试AutoVacuumMain / BgWriterHibernateActivity后台进程在等待下一次调度唤醒一般是正常现象不用处理这张表不用背用多了自然就记住了。关键是看到某个事件时你能知道它大概属于哪一类然后去正确的方向排查而不是抓瞎。2.3 注意版本差异PostgreSQL 的等待事件在版本之间经常有调整同一个名字在不同版本里归属可能不同。例如 PG 14 之后对 IO 类事件做了相当大程度的细化很多原先笼统的事件被拆成了更具体的名字PG 13 对wait_event_type的分类也做过一次重新整理。所以你在网上搜到一张 9.6 时代的事件对照表拿到 PG 15 上就可能对不上号。我在排查时会直接查当前版本的官方文档搜索wait_event相关章节对照着看字段含义这是最保险的做法。尤其是你写监控告警的时候不要硬编码事件名尽量按wait_event_type做第一层分类再对具体wait_event做第二层处理这样版本升级后告警规则不至于大面积失效。3. 高频等待事件实战排查3.1 ClientRead / ClientWrite连接在“摸鱼”还是“憋大招”很多人一看到ClientRead就觉得数据库不行了其实完全不是。ClientRead最常见的含义是后端进程正在等待客户端发送下一条 SQL。也就是说连接是空闲的。pg_stat_activity里大量state idle的连接wait_event 基本都是ClientRead。这是正常状态说明应用开了连接池连接挂在那里待命。但有一种ClientRead需要警惕state idle in transaction。这表示连接在一个未结束的事务里等待客户端下一条指令事务既不提交也不回滚。这个状态很危险因为事务持有的锁不会释放如果它锁住了一张表或者一行记录其他会话就只能排队等。我见过不少生产事故就是应用代码里开事务之后忘了提交连接池里的连接全部变成idle in transaction然后业务查询全部卡死在锁等待上。定位方法很简单SELECT state, wait_event, count(*) FROM pg_stat_activity GROUP BY 1, 2 ORDER BY 3 DESC;如果idle in transaction的数量很多优先检查应用代码的事务管理或者设置合理的idle_in_transaction_session_timeout把超时事务自动杀掉。ClientWrite则是另一种情况服务端想把结果数据写回客户端但网络缓冲区满了客户端消费不过来。常见于一次查询返回超大结果集而应用端又一行一行慢慢 fetch。遇到这种事件光看数据库意义不大要去看应用怎么消费结果集、网络链路是否拥堵、一次取数是否必要必要时减少返回行数或者改用分批处理。3.2 IO 类等待DataFileRead / BufFileRead / WALWriteIO 类等待是排查重点也是最容易误判的地方。DataFileRead表示进程需要读一个数据块但在共享缓冲区里没找到必须去磁盘读。注意出现这种等待不能直接断定磁盘慢因为如果表没有合适的索引每次查询都是全表扫描即使磁盘是 SSD也会被逻辑读取量拖垮。先看整体缓存命中率SELECT sum(blks_hit)::float / NULLIF(sum(blks_hit blks_read), 0) AS cache_hit_ratio FROM pg_stat_database WHERE datname current_database();通常这个值应该在 99% 左右。如果低于 95%说明很多查询打到了磁盘上优先检查是否有大表全表扫描、缺失索引、随机读过量。再看表的读取统计SELECT relname, seq_scan, idx_scan, heap_blks_read, heap_blks_hit, idx_blks_read FROM pg_stat_user_tables ORDER BY heap_blks_read DESC LIMIT 10;如果seq_scan高且heap_blks_read高基本可以判断是查询缺索引或者统计信息不准执行计划选择了全表扫。这个时候加索引往往立竿见影不用急着骂存储性能。BufFileRead和BufFileWrite是临时文件读写。当排序、hash join、group by 等操作的数据量超过work_mem限制时PG 会把中间结果写到临时文件。看到这个事件第一反应是检查执行计划里的 Sort 是不是external merge Disk如果是说明内存不够用。处理办法调整work_mem或者优化 SQL 避免大排序。PG 13 以后还可以用hash_mem_multiplier控制 hash 操作的内存比粗暴调大work_mem更安全。WALWrite类事件保持高位经常和高频提交、低效批量写入有关。比如应用在一个循环里逐条 INSERT 且每条单独提交TPS 一高WAL 写入就成了瓶颈。优化思路很明确改成事务内批量提交或者适当调大max_wal_size减少检查点频率同时关注 WAL 所在磁盘的 fsync 延迟。3.3 锁类等待Lock / transactionid / tuple锁等待是 wait_event 里最有“一锤定音”效果的一类。看到wait_event_type Lock基本可以确定有会话在被另一把锁拦截。最经典的transactionid等待就是事务 A 更新了一行没提交事务 B 也想更新同一行于是 B 必须等 A 结束。排查锁等待PG 给出了一个非常实用的函数pg_blocking_pids(pid)直接返回阻塞指定 pid 的会话数组。我通常这样查SELECT blocked.pid AS blocked_pid, left(blocked.query, 60) AS blocked_query, blocking.pid AS blocking_pid, left(blocking.query, 60) AS blocking_query FROM pg_stat_activity AS blocked CROSS JOIN LATERAL unnest(pg_blocking_pids(blocked.pid)) AS blocker(pid) JOIN pg_stat_activity AS blocking ON blocking.pid blocker.pid WHERE blocked.wait_event_type Lock AND blocked.state active;这条 SQL 会把“谁被谁挡住了”直接列出来。看到结果后先判断阻塞者是不是一个长时间未提交的事务。如果是联系业务方提交或回滚如果阻塞者是后台进程比如 autovacuum就要考虑是不是 vacuum 被长时间事务挡住了。紧急情况下可以直接pg_terminate_backend(阻塞pid)把那个卡死的事务干掉让业务恢复。Lock大类里还有relation、tuple、extend等 locktype。看到relation多半是表级锁比如 DDL 在等 ACCESS EXCLUSIVE、VACUUM 在等 SHARE UPDATE EXCLUSIVE。tuple锁经常出现在并发操作同一批新插入行的时候持续较久一般和热点行、外键约束检查有关需要结合业务逻辑分析。另外强烈建议开启log_lock_waits on配合调整deadlock_timeout这样锁等待超过阈值后PG 会把等待信息写进日志事后排查特别有用。3.4 LWLock 与 BufferPin高并发下的隐形瓶颈LWLock 类等待不像锁等待那么直白因为 LWLock 通常只被持有几个微秒。如果你能看到稳定的 LWLock 等待说明某个内存结构已经成了热点背后往往是高并发压力的集中体现。buffer_mapping是共享缓冲区映射表的锁当多个进程同时访问同一个 buffer 或者频繁在 buffer 池里查找页面时会出现。典型场景是大量并发全表扫描同一个大表每个进程都要往共享缓冲区里塞页面映射表就被争用了。缓解手段是提高缓存命中率、消除不必要的全表扫描必要时通过调整 SQL 或分区表分散热点。WALInsertLock是所有要写 WAL 的进程都要先抢的锁高并发小事务场景尤其明显。比如应用开启大量短事务每秒成千上万次提交WALInsertLock 和 WALWrite 会同时飙升。优化手段优先考虑减少事务提交次数而不是动不动调大wal_buffers。因为wal_buffers再大事务提交时该刷盘还是得刷盘根本问题在业务写入模式。ProcArrayLock保护进程数组和事务快照高并发事务启动与结束时竞争明显。长事务越多snapshot 构建越重ProcArrayLock 压力越大。所以控制长事务、避免连接数堆积能有效缓解这类 LWLock 等待。BufferPin等待是另一个高频现象它表示一个进程要访问某个数据页但页被另一个进程 pin 住了。短时间的 BufferPin 是正常状态可如果持续出现常见于热点行更新、索引页分裂或者大并发执行同一行 UPDATE。处理方向是看业务是否存在极端热点比如秒杀、奖品发放数据层面能做的就是热点行拆分、减少单行更新频率。4. 实操5分钟锁定一次慢查询在等什么4.1 搭一个等待事件快照采集脚本等待事件是一个瞬时快照你查一次只能看到一个瞬间。某个事件可能只持续几十毫秒等你打开查询它就消失了。所以在定位周期性抖动时我会先跑一个快速采样脚本把 wait_event 持续记录下来#!/bin/bash INTERVAL1 DBappdb HOST127.0.0.1 USERpostgres LOGwait_event_$(date %Y%m%d).log while true; do echo $(date %F %T) $LOG psql -h $HOST -U $USER -d $DB -c \ SELECT pid, wait_event_type, wait_event, state, left(query, 80) AS query FROM pg_stat_activity WHERE wait_event IS NOT NULL AND pid pg_backend_pid() ORDER BY wait_event_type, wait_event; $LOG 21 sleep $INTERVAL done脚本很简单每秒钟采样一次。跑个三五十秒然后把日志丢到文本编辑器里按wait_event去做计数基本就能看出问题集中在哪一类。更规范一点可以建一张快照表CREATE TABLE wait_event_snapshot ( sample_time timestamptz DEFAULT now(), pid int, wait_event_type text, wait_event text, state text, query text );用循环定期 insert后面可以直接聚合统计不用再去翻日志文件。4.2 用 pg_blocking_pids 找出锁的源头采样后如果看到Lock类等待占大头马上用上一节那条pg_blocking_pids的 SQL 找出阻塞链。我遇到过一个比较典型的场景一个报表任务开了事务之后应用端发生了异常事务一直没提交但连接也没断开。业务侧执行一个很简单的查询结果全卡在transactionid等待上。通过pg_blocking_pids一眼就看到那个“僵尸事务”终止之后系统立刻恢复。注意pg_blocking_pids只给出直接阻塞者如果一个事务被另一个事务阻塞而这个事务又阻塞了另外三个事务你看到的是层层嵌套的关系。优先处理链条最顶端的那个 pid也就是“没有在等待任何锁但持有锁”的会话。可以再配合SELECT pid, locktype, mode, granted, relation::regclass FROM pg_locks WHERE granted false ORDER BY pid;把等待中的锁对象也列出来确认锁的是表、行还是事务 ID。如果是行锁看relation::regclass能知道具体是哪张表有助于定位业务接口。4.3 一张排查决策表把常见 wait_event 到处理动作映射成一张表排查的时候按图索骥主要等待优先检查项常用手段ClientRead连接池、idle in transaction 比例SELECT state, wait_event, count(*) GROUP BY 1,2ClientWrite结果集大小、网络、客户端消费减少返回行数、批量处理DataFileRead缓存命中率、全表扫描、索引计算 blks_hit/blks_readexplain analyzeBufFileRead / BufFileWritework_mem、排序/哈希溢出查看执行计划 Sort 方法调整内存参数Lock / transactionid / tuple阻塞者是谁pg_blocking_pids、pg_locksWALInsertLock / WALWrite提交频率、WAL 盘延迟批量提交、调整 checkpoint 参数buffer_mapping热点页、shared_buffers、全表扫描并发优化 SQL、分区表分散热点BufferPin热点行、索引页分裂热点行拆分、减少单行更新频率这张表的价值在于让你少走弯路。看到等待事件之后先去查对应的“源头”而不是直接动手改一堆参数。改参数是最后手段定位问题是第一优先级。5. 常见问题与避坑实录5.1 wait_event 为空到底是不是故障很多人查pg_stat_activity时发现有些 active 会话的wait_event是 NULL就开始紧张。实际上NULL 只表示这个进程当前没有在等待任何资源它正在 CPU 上真实地执行代码。比如一个复杂的计算型查询正在疯狂跑 CPU它暂时不需要等待锁或 IOwait_event 就是空。反过来你也不能因为单次查询没有看到等待事件就断定系统没有瓶颈。等待事件是瞬时的一个慢查询可能在多数时间里都在等锁但你采样的那一下它刚好不在等就会漏掉。这也是我一直强调“连续采样”的原因。另外并行查询的 worker 进程有自己的等待事件在backend_type里会显示为parallel worker排查时注意区分。5.2 等待事件不等于性能问题的三个误区第一个误区看到ClientRead大量出现就认为是负载过高。其实空闲连接就是 ClientRead真正要看的是有多少idle in transaction和 active 状态的等待。第二个误区看到 IO 类事件就怪存储。很多 IO 等待是查询计划不当导致的比如缺索引导致全表扫、表膨胀导致扫描块过多、work_mem太小导致大量临时文件读写。存储可能很冤枉。第三个误区看到 LWLock 就当是数据库 bug。WALInsertLock竞争通常意味着写入模式有问题buffer_mapping竞争通常意味着缓存命中率太低。这些都是可以调优的业务或配置问题不是 PG 本身有缺陷。别急着骂数据库先审视自己的 SQL 和参数。5.3 结合外部指标做交叉验证wait_event 是数据库内部的观测指标但它不能独立解释所有问题。比如看到大量DataFileRead你还需要知道磁盘在实际 IO 层面是否真的有压力。用iostat看一眼磁盘利用率iostat -dx 1重点关注%util、r_await、w_await。如果数据库报了很高的DataFileRead但磁盘%util很低那说明问题更多是逻辑读太多也就是 SQL 或索引问题而不是物理存储扛不住。反过来如果磁盘%util常年高位w_await明显异常那存储就是真实的瓶颈可能需要换更快的盘或者调整 IO 模型。数据库内部也有一些基础指标可以配合看。比如pg_stat_bgwriter里checkpoints_timed和checkpoints_req的比例如果checkpoints_req占比高说明检查点经常因为max_wal_size太小吃不消而触发可以适当调大。再配合pg_stat_statements找 top SQL就能把“哪条 SQL 在什么场景下产生等待”对上号。6. 从 wait_event 到系统化性能观测6.1 与 pg_stat_statements、pg_stat_database 联动wait_event 解决的是“这一刻在等什么”但要回答“哪个 SQL 产生了最多的等待”最好和pg_stat_statements一起看。先按总耗时排出问题 SQLSELECT calls, round(total_exec_time::numeric, 2) AS total_ms, round(mean_exec_time::numeric, 2) AS mean_ms, left(query, 60) AS query FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;注意 PG 13 以前这个字段叫total_timePG 13 之后改名total_exec_time如果你的版本比较老记得换字段名。拿到耗时最高的 SQL 后再回到 wait_event 采样结果里看它对应的等待类型这样就能形成一个闭环哪些 SQL 在等什么是因为缺索引、锁冲突还是临时文件溢出。6.2 开启 log_lock_waits 和 auto_explain 的干活细节有两个配置组合我几乎在每套生产 PG 上都开一个是log_lock_waits一个是auto_explain。log_lock_waits配合deadlock_timeout可以在锁等待超过阈值时输出日志log_lock_waits on deadlock_timeout 500msdeadlock_timeout默认是 1 秒调到 500ms 或 200ms 可以更早捕获锁等待但日志量会增加生产环境建议按需调整。配置修改后不需要重启reload即可生效。auto_explain可以自动记录慢查询的执行计划尤其适合那些“偶发慢”的 SQL。配置示例shared_preload_libraries auto_explain auto_explain.log_min_duration 1s auto_explain.log_buffers on auto_explain.log_timing onshared_preload_libraries需要重启才能加载。auto_explain.log_min_duration可以热加载生产环境可以先用一个较大的阈值观察日志量再逐步调低。开了auto_explain之后慢 SQL 的执行计划会直接进日志配合 wait_event 采样基本能还原绝大多数性能问题的现场。6.3 一个小技巧等待事件 TopN 统计与控制输出如果你用的是快照表采集方式统计 top 等待事件非常方便SELECT wait_event_type, wait_event, count(*) AS sample_cnt FROM wait_event_snapshot GROUP BY wait_event_type, wait_event ORDER BY sample_cnt DESC;这能告诉你在一段时间内系统最频繁的等待是什么。如果某个事件占比特别高比如DataFileRead占 80%那基本可以断定瓶颈在逻辑读如果ClientRead占大多数说明连接池空闲会话多但实际负载未必高。个人经验是拿到新环境的第一件事就是先跑半小时采样建立它的“等待基线”。系统健康时什么事件占多少比例、峰值出现在哪个时间点这些数据记下来。等真的出了问题拿现场采样和基线一对比异常一目了然。不要等到故障发生了才开始学 wait_event——那时候你不仅要面对正在发生的问题还要忍受“没基线可对比”的盲目感。先把这篇文章里的小脚本跑起来遇到问题的时候你手里就已经有了一副地图。
返回列表