1. PostgreSQL阻塞分析利器:pg_blocking_pids()函数解析
当数据库查询突然陷入停滞,应用日志里堆满超时错误时,有经验的DBA第一个反应就是——查阻塞。PostgreSQL提供的pg_blocking_pids()函数就像数据库里的CT扫描仪,能精准定位造成阻塞的罪魁祸首。这个看似简单的函数背后,藏着PG多版本并发控制(MVCC)的实现精髓。
上周我们生产环境就遭遇了一次诡异的死锁:凌晨批量作业卡死,连带拖垮了整个应用的响应速度。正是用这个函数配合pg_stat_activity,五分钟内就揪出了那个忘记提交事务的Python脚本。下面我就结合这次实战,详细拆解这个救命函数的使用技巧。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 函数工作原理与核心参数
2.1 函数签名与返回值
sql复制pg_blocking_pids(integer) → integer[]
这个函数的输入参数是待检查的会话PID,返回的是一个整数数组,包含所有阻塞该会话的其他会话PID。如果返回空数组,说明当前会话没有被阻塞。
关键点在于:阻塞是层级式的。A阻塞B,B阻塞C,那么查询C的阻塞情况时会返回B,而不会直接返回A。需要递归查询才能找到阻塞链的源头。
2.2 底层实现机制
函数通过查询PG内部的锁管理器实现,主要涉及这些系统表:
pg_locks:当前所有锁的状态pg_stat_activity:会话活动信息pg_database:数据库信息
其工作流程是:
- 检查目标会话是否在等待锁
- 找出持有该锁且与目标会话冲突的其他会话
- 返回这些会话的PID列表
注意:函数只显示直接阻塞者,不展示完整的阻塞链。实际排查时需要配合递归查询。
3. 实战排查步骤详解
3.1 基础查询模板
sql复制SELECT
blocked_locks.pid AS blocked_pid,
blocking_locks.pid AS blocking_pid,
blocked_activity.usename AS blocked_user,
blocking_activity.usename AS blocking_user,
blocked_activity.query AS blocked_query,
blocking_activity.query AS blocking_query
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks
ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
AND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.GRANTED;
3.2 增强版阻塞分析
sql复制WITH blocking_chains AS (
SELECT
a.pid,
a.usename,
a.application_name,
a.client_addr,
a.query,
a.state,
a.backend_start,
a.xact_start,
pg_blocking_pids(a.pid) AS blocking_pids
FROM pg_stat_activity a
WHERE cardinality(pg_blocking_pids(a.pid)) > 0
)
SELECT
bc.pid AS blocked_pid,
bc.blocking_pids,
array_to_string(bc.blocking_pids, ',') AS blocking_pids_str,
bc.usename AS blocked_user,
bc.application_name AS blocked_app,
bc.query AS blocked_query,
bc.state AS blocked_state,
bca.usename AS blocking_user,
bca.application_name AS blocking_app,
bca.query AS blocking_query,
bca.state AS blocking_state,
age(now(), bca.xact_start) AS blocking_duration
FROM blocking_chains bc
LEFT JOIN pg_stat_activity bca ON bca.pid = ANY(bc.blocking_pids)
ORDER BY blocking_duration DESC;
这个查询能显示:
- 被阻塞会话的详细信息
- 阻塞会话的PID列表
- 每个阻塞会话的详细信息
- 阻塞持续时间(关键指标)
- 按阻塞时长降序排列
4. 典型阻塞场景与解决方案
4.1 长事务阻塞DDL操作
现象:ALTER TABLE执行超时,pg_blocking_pids()显示被某个SELECT阻塞
根因:PG的DDL需要ACCESS EXCLUSIVE锁,与任何其他锁都冲突
解决方案:
- 查询阻塞会话的事务开始时间:
sql复制SELECT pid, usename, xact_start, query FROM pg_stat_activity WHERE pid = ANY(pg_blocking_pids(你的DDL会话PID)); - 判断是否可以终止该事务:
sql复制SELECT pg_terminate_backend(阻塞PID); - 预防措施:
- 设置锁超时:
SET lock_timeout = '5s'; - 在低峰期执行DDL
- 使用CONCURRENTLY选项(如创建索引)
- 设置锁超时:
4.2 外键未加索引导致的锁升级
现象:高并发时大量会话被少数几个会话阻塞
根因:外键列缺少索引导致行锁升级为表锁
验证方法:
sql复制SELECT c.conname, c.conrelid::regclass, c.confrelid::regclass
FROM pg_constraint c
WHERE c.contype = 'f'
AND NOT EXISTS (
SELECT 1 FROM pg_index i
WHERE i.indrelid = c.conrelid
AND (i.indkey::smallint[] @> array(SELECT attnum FROM pg_attribute WHERE attrelid = c.conrelid AND attname = ANY(string_to_array(pg_get_constraintdef(c.oid), ' ')::text[])))
);
解决方案:
- 为所有外键列创建索引
- 批量操作时暂时禁用外键约束:
sql复制ALTER TABLE ... DISABLE TRIGGER ALL; -- 执行批量操作 ALTER TABLE ... ENABLE TRIGGER ALL;
5. 高级应用技巧
5.1 自动化监控脚本
bash复制#!/bin/bash
BLOCKED_THRESHOLD=3
OUTPUT_FILE="/var/log/pg_blocking_monitor.log"
psql -U postgres -d your_db -t -c "
SELECT now() AS check_time,
blocked.pid AS blocked_pid,
blocked.usename AS blocked_user,
blocking.pid AS blocking_pid,
blocking.usename AS blocking_user,
blocking.query AS blocking_query,
age(now(), blocking.xact_start) AS blocking_duration
FROM pg_stat_activity blocked
JOIN pg_blocking_pids(blocked.pid) bp ON true
JOIN pg_stat_activity blocking ON blocking.pid = bp
WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0
AND age(now(), blocking.xact_start) > interval '${BLOCKED_THRESHOLD} min'
" >> $OUTPUT_FILE
配合crontab每5分钟运行一次,记录长时间阻塞事件。
5.2 锁等待超时配置
在postgresql.conf中设置:
ini复制deadlock_timeout = 1s # 死锁检测间隔
lock_timeout = 30s # 单条语句锁等待超时
statement_timeout = 0 # 语句执行超时(0表示禁用)
应用层重试策略示例(Python):
python复制from psycopg2 import OperationalError
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=10),
retry=retry_if_exception_type(OperationalError)
)
def execute_with_retry(conn, query):
with conn.cursor() as cur:
cur.execute("SET lock_timeout = '30s'")
cur.execute(query)
return cur.fetchall()
6. 性能考量与注意事项
-
监控开销:频繁调用pg_blocking_pids()会增加系统负载,建议:
- 生产环境监控间隔不低于1分钟
- 避免在事务中循环调用该函数
-
权限控制:普通用户只能看到自己会话的阻塞情况,需要超级用户权限才能查看所有阻塞关系
-
分布式事务:在Citus等分布式方案中,阻塞链可能跨节点,需要结合分布式锁监控工具
-
备库场景:在热备库上查询时,注意:
- 只能看到备库上的阻塞情况
- 主库上的阻塞不会反映在备库
-
版本差异:
- PG 9.6+ 支持该函数
- 更早版本需要使用pg_locks手工关联查询
实际使用中我发现一个有趣现象:大约15%的阻塞案例其实是由于应用层连接池配置不当导致的——连接泄漏使得事务长时间不释放。这种情况的典型特征是阻塞会话的query字段显示为<IDLE> in transaction,且xact_start时间很早。这时需要检查应用服务器的连接池配置(如HikariCP的maxLifetime设置)。
