拿到一个卡住的数据库,最忌讳的做法就是凭感觉重启或者盲目杀进程。PostgreSQL其实早就在pg_stat_activity视图里把每个会话当前的状态、正在执行的SQL以及等待的事件都摆出来了,其中wait_event_type和wait_event这两个字段是判断问题性质的关键。看懂它们,你基本就能判断出会话是在等锁、等磁盘IO,还是在等某个轻量级锁,进而选择正确的处置方式。

一、wait_event_type字段详解:每种等待类型意味着什么
wait_event_type表示等待事件所属的大类,从PostgreSQL 9.6开始引入,到10以后覆盖面已经很完善。常见的取值包括Lock、LWLock、IO、Client、Extension、Timeout、IPC、Activity、BufferPin等。其中最需要重点关注的是下面几类。
Lock是重量级锁,也就是我们在pg_locks视图里看到的那种锁对象,比如表锁、行锁、事务ID锁、咨询锁。一旦看到wait_event_type是Lock,且wait_event是transactionid或tuple,基本可以断定这条会话在等另一个事务提交或回滚,属于典型的阻塞场景。而wait_event为relation时则说明在等表级锁,比如某条SELECT在大表扫描时被VACUUM FULL的AccessExclusiveLock挡住了。
LWLock是轻量级锁,主要用于保护共享内存中的内部数据结构,比如缓冲区映射表、WAL插入槽等。常见的有BufferContent、WALWrite、LockManager等。看到大量会话等WALWrite,通常说明WAL写入跟不上,可能是磁盘IO瓶颈;等BufferContent则可能与热点页争用有关。IO类事件直接说明进程在等磁盘操作,例如DataFileRead表示正在从数据文件读页,如果这类事件持续出现且读延迟高,大概率是缓存命中率低或者磁盘性能差。
Client类事件很容易被误解。wait_event_type='Client'且state='idle in transaction'时,说明服务端在等客户端发下一条语句,问题往往出在应用代码里事务开着不提交,而不是数据库本身。Activity和Timeout一般不用太在意,比如WalWriterMain、CheckpointerMain这类后台进程的常规活动就归在这些类别里。
二、如何区分正常等待与真正的阻塞
并不是所有等待都有问题。一个健康的数据库里,pg_stat_activity中出现IO等待、Client等待都很正常,关键要看持续时间和影响范围。判断是否真的发生了阻塞,需要同时满足几个条件:多个会话的wait_event_type为Lock、被阻塞会话的state是active、并且等待持续时间超过了正常业务阈值。
一个实用技巧是结合query_start和state来判断。如果一条UPDATE的query_start已经停在五分钟前,state还是active,wait_event是transactionid,那它大概率在等一个持有行锁的事务。此时要找的不是这条UPDATE本身,而是那个持有锁却迟迟不提交的事务——它可能正以idle in transaction的状态挂在应用连接池里。
还有一点要注意:xact_start和query_start的区别。query_start是当前这条语句开始的时间,xact_start是事务开始的时间。如果一个会话xact_start很早但query_start是刚刚,中间隔了很久,说明这个事务里已经执行过多条语句,长时间持有锁,正是阻塞链的常见源头。
三、阻塞诊断SQL模板:从定位到处置
下面这套模板可以直接保存备用。第一条用来快速查看当前所有非空闲会话及其等待事件:
SELECT pid,
usename,
state,
wait_event_type,
wait_event,
now() - query_start AS query_duration,
left(query, 60) AS query_text
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY query_duration DESC;
第二条是经典的阻塞链查询,能直接显示谁阻塞了谁。借助pg_blocking_pids()函数(9.6及以上版本可用),一行就能拿到阻塞源头:
SELECT blocked.pid AS blocked_pid,
blocked.usename AS blocked_user,
left(blocked.query, 50) AS blocked_query,
blocking.pid AS blocking_pid,
blocking.usename AS blocking_user,
blocking.state AS blocking_state,
left(blocking.query, 50) AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
ON blocking.pid = ANY (pg_blocking_pids(blocked.pid))
WHERE blocked.wait_event_type = 'Lock';
查到阻塞源头后,如果确认那个会话是异常的(比如应用崩溃后残留的idle in transaction连接),可以用下面两条语句处置。先温和地取消当前查询,不行再终止整个连接:
-- 只取消当前正在执行的语句,事务回滚由会话决定 SELECT pg_cancel_backend(12345); -- 直接终止连接,未提交事务全部回滚 SELECT pg_terminate_backend(12345); -- 批量终止所有空闲超过1小时的事务 SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'idle in transaction' AND now() - state_change > interval '1 hour';
四、把诊断常态化:加个阈值告警和锁超时
与其等故障发生再手忙脚乱,不如提前设置两道防线。第一道是lock_timeout参数,建议在业务SQL的会话级别设置,例如SET lock_timeout = '5s',这样等锁超过五秒的语句会主动报错退出,避免请求堆积拖垮整个应用。注意不要全局设置过小的值,否则DDL操作容易被误伤。
第二道防线是idle_in_transaction_session_timeout,比如设置为10分钟,让那些忘记提交的事务自动被断开。很多诡异的生产阻塞,追根溯源都是应用里某个异常分支开了事务没关闭,这个参数能兜住大部分风险。
如果需要监控告警,可以基于前面的阻塞链查询改造:统计wait_event_type为Lock且等待超过30秒的会话数量,超过阈值就触发告警。把这个查询接入定时任务或Prometheus exporter,就能在用户投诉之前发现锁冲突。配合定期归档pg_stat_activity快照,还能在事后回溯阻塞发生时各会话的SQL,形成完整的故障复盘材料。
pg_stat_activitywait_event_typePostgreSQL阻塞诊断修改时间:2026-09-12 02:34:33