1. 理解PostgreSQL中的活动与阻塞查询
在PostgreSQL数据库管理过程中,监控活动查询和识别阻塞情况是每个DBA和开发者的必备技能。当数据库性能突然下降或应用程序出现超时,快速定位问题查询往往能节省大量故障排查时间。
PostgreSQL提供了丰富的系统视图和函数来帮助我们获取这些关键信息。其中,pg_stat_activity是最核心的系统视图,它实时展示了当前数据库中的所有会话及其状态。通过这个视图,我们可以:
- 查看哪些查询正在执行
- 识别长时间运行的查询
- 发现被阻塞的会话
- 分析锁等待情况
重要提示:在生产环境执行查询监控时,建议在非高峰时段进行,因为某些诊断查询本身可能对数据库性能产生影响。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 使用pg_stat_activity查看活动查询
2.1 基础查询方法
要查看当前所有活动会话,最基本的查询是:
sql复制SELECT * FROM pg_stat_activity;
这个查询会返回大量列,包括:
datname: 数据库名usename: 用户名application_name: 客户端应用名称state: 会话状态(active, idle, idle in transaction等)query: 当前或最后执行的SQL语句query_start: 查询开始时间backend_start: 后台进程启动时间
2.2 优化显示的关键列
实际工作中,我们通常只需要关注部分关键列:
sql复制SELECT
pid,
usename,
datname,
application_name,
client_addr,
state,
query,
query_start,
now() - query_start AS duration
FROM
pg_stat_activity
WHERE
state = 'active'
ORDER BY
duration DESC;
这个查询会:
- 只显示活动中的查询(state = 'active')
- 计算每个查询已运行的时间(duration)
- 按运行时间降序排列,方便优先处理长时间运行的查询
2.3 过滤特定数据库或用户
在大型系统中,我们可能只想监控特定数据库或用户的查询:
sql复制-- 监控特定数据库
SELECT * FROM pg_stat_activity WHERE datname = 'your_database';
-- 监控特定用户
SELECT * FROM pg_stat_activity WHERE usename = 'problem_user';
3. 识别阻塞查询和被阻塞会话
3.1 理解PostgreSQL锁机制
PostgreSQL使用多版本并发控制(MVCC)来处理并发事务,但在某些情况下仍然需要锁来保证数据一致性。常见的锁包括:
- 行级锁:最细粒度的锁,影响最小
- 表级锁:影响整个表的操作
- 咨询锁:应用程序控制的锁
当多个事务尝试获取冲突的锁时,就会发生阻塞。例如:
- 事务A持有某行的排他锁(X锁)
- 事务B尝试获取同一行的共享锁(S锁)或排他锁
- 事务B必须等待事务A释放锁后才能继续
3.2 查询阻塞关系
要识别阻塞链,我们可以使用以下查询:
sql复制SELECT
blocked_locks.pid AS blocked_pid,
blocked_activity.usename AS blocked_user,
blocking_locks.pid AS blocking_pid,
blocking_activity.usename AS blocking_user,
blocked_activity.query AS blocked_statement,
blocking_activity.query AS blocking_statement
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;
这个复杂查询的工作原理是:
- 找出所有未授予的锁(NOT blocked_locks.GRANTED)
- 找出持有这些锁的阻塞进程
- 关联对应的会话信息
3.3 简化版阻塞查询
如果上面的查询太复杂,可以使用这个简化版本:
sql复制SELECT
waiting.query AS waiting_query,
waiting.pid AS waiting_pid,
waiting.usename AS waiting_user,
waiting.state AS waiting_state,
blocking.query AS blocking_query,
blocking.pid AS blocking_pid,
blocking.usename AS blocking_user,
blocking.state AS blocking_state
FROM
pg_stat_activity waiting
JOIN
pg_locks l1 ON l1.pid = waiting.pid AND NOT l1.granted
JOIN
pg_locks l2 ON l2.pid != l1.pid AND l2.granted
AND l1.locktype = l2.locktype
AND l1.DATABASE IS NOT DISTINCT FROM l2.DATABASE
AND l1.relation IS NOT DISTINCT FROM l2.relation
AND l1.page IS NOT DISTINCT FROM l2.page
AND l1.tuple IS NOT DISTINCT FROM l2.tuple
AND l1.virtualxid IS NOT DISTINCT FROM l2.virtualxid
AND l1.transactionid IS NOT DISTINCT FROM l2.transactionid
AND l1.classid IS NOT DISTINCT FROM l2.classid
AND l1.objid IS NOT DISTINCT FROM l2.objid
AND l1.objsubid IS NOT DISTINCT FROM l2.objsubid
JOIN
pg_stat_activity blocking ON blocking.pid = l2.pid;
4. 高级监控与自动化
4.1 创建监控视图
为了方便日常监控,可以创建专门的视图:
sql复制CREATE OR REPLACE VIEW active_queries_monitor AS
SELECT
pid,
usename,
datname,
application_name,
client_addr,
state,
query,
query_start,
now() - query_start AS duration,
wait_event_type,
wait_event
FROM
pg_stat_activity
WHERE
state = 'active'
AND backend_type = 'client backend'
ORDER BY
duration DESC;
4.2 设置自动报警
可以设置定期执行的脚本,当发现长时间运行的查询或阻塞时发送报警:
bash复制#!/bin/bash
LONG_RUNNING_THRESHOLD="00:05:00" # 5分钟
BLOCKED_THRESHOLD=3 # 3个以上被阻塞会话
# 检查长时间运行的查询
LONG_QUERIES=$(psql -U postgres -d postgres -t -c \
"SELECT count(*) FROM pg_stat_activity
WHERE state = 'active' AND now() - query_start > interval '$LONG_RUNNING_THRESHOLD'")
# 检查被阻塞的会话
BLOCKED_SESSIONS=$(psql -U postgres -d postgres -t -c \
"SELECT count(*) FROM pg_stat_activity
WHERE wait_event_type IS NOT NULL")
if [ "$LONG_QUERIES" -gt 0 ] || [ "$BLOCKED_SESSIONS" -ge "$BLOCKED_THRESHOLD" ]; then
# 发送邮件或调用其他报警机制
echo "警报: 发现 $LONG_QUERIES 个长时间运行查询和 $BLOCKED_SESSIONS 个被阻塞会话" | mail -s "PostgreSQL 性能警报" admin@example.com
fi
4.3 使用扩展增强监控
PostgreSQL的扩展可以提供更强大的监控能力:
sql复制-- 安装pg_stat_statements扩展
CREATE EXTENSION pg_stat_statements;
-- 查询最耗资源的SQL
SELECT
query,
calls,
total_exec_time,
mean_exec_time,
rows
FROM
pg_stat_statements
ORDER BY
total_exec_time DESC
LIMIT 10;
5. 处理阻塞查询的实战技巧
5.1 终止问题会话
当确认某个会话是问题源头时,可以使用以下命令终止它:
sql复制SELECT pg_terminate_backend(pid);
其中pid是要终止的会话进程ID。如果要批量终止长时间运行的查询:
sql复制SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'active'
AND now() - query_start > interval '5 minutes';
警告:强制终止会话可能导致数据不一致,应作为最后手段使用。优先尝试通过应用层解决。
5.2 分析锁等待模式
通过分析常见的锁等待模式,可以预防阻塞问题:
sql复制SELECT
lock.locktype,
lock.mode,
lock.granted,
stat.usename,
stat.query,
stat.query_start,
age(now(), stat.query_start) AS age
FROM
pg_catalog.pg_locks lock
JOIN
pg_catalog.pg_stat_activity stat ON lock.pid = stat.pid
WHERE
NOT lock.granted
ORDER BY
age DESC;
5.3 优化事务设计
许多阻塞问题源于不良的事务设计:
- 避免长时间运行的事务
- 减小事务范围,尽快提交
- 在事务中先获取排他锁(如先更新关键记录)
- 考虑使用乐观并发控制而非悲观锁
5.4 使用锁超时
设置锁等待超时可以防止会话无限期等待:
sql复制-- 会话级别设置
SET lock_timeout = '5s';
-- 数据库级别设置
ALTER DATABASE your_db SET lock_timeout = '5s';
6. 常见问题排查案例
6.1 案例1:应用程序连接泄漏
症状:
- 大量idle in transaction状态会话
- 连接数持续增长不释放
解决方案:
- 检查应用是否正确关闭数据库连接
- 设置idle_in_transaction_session_timeout
sql复制ALTER SYSTEM SET idle_in_transaction_session_timeout = '10min';
6.2 案例2:批量更新阻塞查询
症状:
- 大批量UPDATE阻塞其他查询
- 阻塞链显示多个会话等待同一锁
解决方案:
- 将大更新拆分为小批次
- 在非高峰时段执行
- 考虑使用游标分批处理
6.3 案例3:死锁问题
症状:
- 错误日志中出现"deadlock detected"
- 事务自动回滚
解决方案:
- 检查日志获取死锁详情
- 确保事务以一致的顺序访问资源
- 考虑使用咨询锁协调并发访问
7. 性能优化建议
7.1 索引优化
确保频繁查询的列有适当索引:
sql复制-- 查找可能缺少索引的查询
SELECT
query,
calls,
total_exec_time,
mean_exec_time,
rows
FROM
pg_stat_statements
WHERE
query LIKE '%WHERE%'
AND mean_exec_time > 100 -- 超过100ms
ORDER BY
(total_exec_time * calls) DESC
LIMIT 10;
7.2 查询重写
优化常见问题查询模式:
- 避免SELECT *
- 使用LIMIT限制结果集
- 考虑物化视图处理复杂聚合
7.3 连接池配置
使用连接池如pgBouncer:
- 减少连接建立开销
- 控制最大连接数
- 复用空闲连接
7.4 定期维护
设置定期维护任务:
sql复制-- 手动执行VACUUM
VACUUM (VERBOSE, ANALYZE) your_table;
-- 重建索引
REINDEX TABLE your_table;
在实际工作中,我发现将监控查询保存为常用脚本非常有用。例如,我通常会维护一个包含10-15个关键诊断查询的SQL文件,当性能问题出现时可以快速运行这些查询定位问题。另外,设置一个定期执行的作业来记录pg_stat_activity的快照也很有帮助,这样可以在问题发生后追溯历史状态。
