做 PostgreSQL 运维这些年,我最怕的不是数据量有多大,也不是 SQL 写得多绕,而是大半夜监控突然响起来,屏幕上跳着一句“连接数告警”或者“复制延迟超过阈值”。很多故障,等到报警的时候其实已经影响业务了,所谓快速排查,拼的不是手速,而是你对常见故障有没有形成条件反射式的排查路径。PostgreSQL 生产环境的故障翻来覆去就那么几类——连接耗尽、慢查询、锁等待、磁盘膨胀、主从复制延迟。这篇文章把我自己踩过的坑、常用的排查 SQL、以及一些常规文档里不会写的小经验整理出来,希望能帮你少熬几个夜。
1. 生产环境 PostgreSQL 故障,到底难在哪
1.1 三类最容易撂挑子的故障场景
先说个很现实的问题:PostgreSQL 本身足够稳定,大多数故障其实是“配置不当 + 业务变化”叠加出来的。我梳理下来,生产环境里的翻车现场基本能归成三类。
第一类是连接层故障。表现为应用报“connection limit exceeded”“too many connections”,或者连接服务直接挂掉。这类问题往往不是数据库服务器宕机,而是连接数被耗尽,或者是认证配置突然被改了。最头疼的不是问题本身,而是你不确定到底是哪一个应用、哪一台机器把所有连接占掉的。
第二类是性能层故障,症状是接口变慢、CPU 飙升、磁盘 IO 打满。这类故障的难点在于诱因很多,可能是某一条 SQL 突然走错了执行计划,可能是锁等待把事务串起来了,也可能是 autovacuum 在高峰期跑起来抢资源。如果没有慢查询日志和统计信息兜底,现场基本靠猜。
第三类是存储与复制层故障,包括磁盘空间不足、WAL 日志目录暴涨、主从延迟持续扩大。这类故障有一个共同特征:刚开始没有明显报错,但业务会在某个时间点突然“雪崩”,而且一旦发生,恢复的时间往往很长。
1.2 快速排查的通用思路:先止损,再定位,后根治
遇到生产故障,我个人的习惯是三层递进,而不是一上来就翻各种视图。
第一层是止损。先判断是不是全局性的问题,比如连接数满了、磁盘满了、复制卡住了,如果是,第一时间通过临时手段恢复业务可用性——比如清理空闲连接、收缩不必要的占用、切换只读流量。注意,止损不等于修复,但能争取到定位时间。
第二层是定位。用 pg_stat_activity、pg_locks、pg_stat_replication、pg_stat_user_tables 这些现成的视图,结合慢查询日志和系统监控,把故障点缩小到具体的进程、SQL 或表。定位这一步不能凭感觉,必须要有数据支撑。
第三层是根治。找到根因后,再决定是改参数、加索引、调 SQL、还是调整运维策略。很多人故障恢复之后就直接撤了,结果过几天同一问题又来一遍,就是因为跳过了这一层。
这条思路听着简单,但实际操作中最容易犯的错是跳过“止损”直接“定位”。比如连接数打满的时候,你还在一条一条查 pg_stat_activity,其实新连接已经进不来了,先把连接释放一部分,你才有余裕去查问题。
2. 连接类故障:数据库“拒人门外”的快速处理
2.1 连接数打满:先看一眼到底被谁吃了
连接数打满是我在生产环境遇到频率最高的故障,没有之一。一般报警信息会直接点名“remaining connection slots are reserved”,此时应用层大概率已经在疯狂超时重试。
第一步要做的不是加 max_connections,而是确认谁占用了连接。这个 SQL 是我每次都会用的:
SELECT pid, usename, application_name, client_addr, backend_start, state, now() - state_change AS state_duration FROM pg_stat_activity ORDER BY state_duration DESC NULLS LAST LIMIT 50;注意看两个字段:state 和 application_name。state 为 idle 的进程不干活但占着 slot,如果数量巨大,基本可以判断是连接池或者应用层没有正确归还连接;state 为 active 的进程如果很多,说明数据库真的在扛高并发请求,这时候要结合 query 内容看是不是有慢 SQL 占着茅坑。
还有一种情况是“idle in transaction”,意思是事务开了没提交。这类连接最阴险,因为它不光占连接,还会持锁,导致后面的写操作全部卡住。看到大量 idle in transaction,可以直接把这部分连接终止掉,先止损:
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'idle in transaction' AND now() - state_change > interval '5 minutes';终止之前最好看一眼这些连接到底卡了多久,5 分钟是个经验值,你可以按业务实际情况调整。
2.2 认证失败:pg_hba.conf 和密码策略的坑
连接数没满,但应用一直报认证失败,这种情况常见于新上线环境,或者某些平台侧的配置扫描修改后。别急着改密码,先分清楚是认证方式问题还是密码本身的问题。
查看当前的 pg_hba.conf 配置,确认客户端来源 IP、数据库、用户命中的是哪一条规则。容易踩的坑有三个:一是规则顺序问题,pg_hba.conf 是自上而下匹配的,前面的规则优先,一旦有个宽松规则排在前面,后面的严格规则就形同虚设;二是加密方式不匹配,比如服务器用了 scram-sha-256,而客户端驱动或连接串里还写着 md5,部分老版本驱动就会出现认证失败;三是密码里包含特殊字符,比如 @、#、% 等,连接串没有做 URL 编码,驱动解析时直接截断或报错。
排查认证问题,最直接的办法是看 PostgreSQL 日志。认证失败后日志里会有明确的 FATAL 记录,比如“password authentication failed for user”,同时会带上来源 IP。日志不会骗人,别在主从库之间来回猜,直接对日志。
另一个经验是:密码策略变更后,一定要检查所有应用连接串。曾经遇到过一个场景,DBA 在平台上改了密码,但某个定时任务用的连接串还是旧密码,导致每天凌晨定时任务报错。这种问题排查起来很费时间,最好是建立密码变更的联检清单,把涉及到的应用、调度任务、BI 连接全都过一遍。
2.3 连接延迟和超时的隐性原因
连接数正常、认证也通过,但应用就是感觉建连很慢。这种问题比较隐蔽,因为数据库层面看,一切正常。
常见的隐性原因有:DNS 反解。PostgreSQL 默认会对客户端 IP 做反向 DNS 解析(如果开启了 log_hostname 或者系统配置了相关选项),一旦 DNS 服务响应慢,连接建立就会卡住。排查方法是开启 log_hostname = off,或者检查数据库主机上的 DNS 配置。
另一个隐性原因是 SSL 握手。如果数据库开启了 SSL 强制,而客户端所在网络握手延迟高,连接建立就会明显变慢。这种情况可以开 SSL 日志,或者用一个简单的工具从客户端侧做端口连通性测试,对比建连耗时。
还有一种情况容易被忽略:数据库负载高,进程调度不过来,导致新连接接受慢。此时数据库层看到的连接数不多,但 CPU 已经打满,新连接会在 accept 队列里排队。这类问题不能只看 PostgreSQL,要结合系统层面的 load average 一起看。
3. 性能类故障:慢查询、锁等待与资源飙升
3.1 三步定位最拖后腿的 SQL
慢查询是 PostgreSQL 生产环境最常见的性能杀手。我的定位套路分三步:打开慢查询日志、看统计视图、然后对嫌疑 SQL 做执行计划分析。
先检查慢查询日志是否开启。不要等到出故障才开,建议生产环境从一开始就设置:
log_min_duration_statement = 1000 log_checkpoints = on log_lock_waits = on1000 毫秒是底线,更严格的环境可以设到 500。如果故障已经发生,而慢日志没开,还有一种办法:查 pg_stat_statements,这个视图能按总耗时、平均耗时、调用次数排序,快速找到 TOP SQL:
SELECT queryid, calls, round(mean_exec_time::numeric, 2) AS avg_ms, round(max_exec_time::numeric, 2) AS max_ms, round(total_exec_time::numeric / 1000::numeric, 2) AS total_s, query FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20;注意 pg_stat_statements 是 PG 的扩展模块,默认不一定安装,它依赖 shared_preload_libraries 里有 pg_stat_statements。这个扩展强烈建议所有生产库都开,它是事后追查慢查询最可靠的抓手。
拿到嫌疑 SQL 后,下一步是 EXPLAIN。我的建议是至少用 EXPLAIN (ANALYZE, BUFFERS),不要只看 EXPLAIN,因为估算的行数和实际行数经常差得很远。BUFFERS 选项能告诉你 SQL 到底读了多少 shared buffers,这对判断是否需要加索引、是否需要调整 work_mem 很有帮助。
3.2 锁等待和阻塞链:一张图看清谁卡了谁
锁等待不像慢查询那么直观,因为业务的表现可能是“所有写操作都挂了”,但每一条 SQL 单独看又都挺正常。这时候关键是找到阻塞链的根节点。
我常用的阻塞查询是:
SELECT blocked.pid AS blocked_pid, blocked.application_name AS blocked_app, blocked.query AS blocked_query, blocking.pid AS blocking_pid, blocking.application_name AS blocking_app, blocking.query AS blocking_query FROM pg_stat_activity blocked JOIN pg_stat_activity blocking ON blocking.pid = ANY (blocked.pg_blocking_pids()) WHERE blocked.state = 'active' AND blocked.wait_event_type = 'Lock';这个查询能把“谁被谁阻塞”直接列出来。注意 pg_blocking_pids() 是 PG 9.6 之后的函数,如果生产环境版本较老,需要换一种写法。
定位到阻塞的源头进程后,先判断它是什么类型:如果是一个活跃的长事务,可能在跑大批量 DML,这时候要么等它跑完,要么联系应用方确认是否可以终止;如果是一个 idle in transaction 的进程,九成是应用忘了提交,可以直接终止。
处理锁问题时,我强烈建议设置 lock_timeout,给单条 SQL 加一个锁等待的上限。这个参数可以在会话级设置,也可以在应用连接串里设置,避免某条 SQL 无限期等下去。生产环境中一个常见的悲剧是:一次偶发的行锁冲突,导致几十个会话互相等待,最后把整个库拖垮。有了 lock_timeout,至少不会出现连锁雪崩。
关于死锁,PostgreSQL 会自动检测并回滚其中一个事务,日志里会打印“deadlock detected”和详细的进程信息。看到死锁别慌,这说明应用层存在两条相反的加锁顺序,需要业务代码层面调整加锁顺序,数据库侧只能等到自动检测,没有别的根治办法。
3.3 CPU 和 IO 飙升的现场还原
CPU 飙升不一定全都是慢查询,也有可能是 autovacuum 突然跑起来,或者是某个备份脚本在高峰期压榨 IO。排查这类问题,要数据库视图和系统工具两头看。
先系统侧执行 top 或 pidstat,找到 CPU 占用最高的 postgres 进程 PID。然后回到数据库执行:
SELECT pid, usename, application_name, state, query FROM pg_stat_activity WHERE pid = <进程PID>;如果关联出来的是一条 SQL,那基本就是这条 SQL 在裸跑;如果关联出来是 autovacuum worker,则要考虑是不是表膨胀严重、vacuum 赶不上垃圾产生速度。
IO 飙升的常见原因是检查点。PostgreSQL 的检查点进程会周期性地把脏页刷到磁盘,默认 interval 是 5 分钟,如果 wal 产生量大,检查点触发时会有一波明显的 IO 尖峰。缓解办法是适当调大 max_wal_size,让检查点不要那么频繁。还要提醒一句,不要为了追求性能把 shared_buffers 改得过大导致系统内存不足,OOM 时 postgres 进程被内核杀掉,那可比慢查询严重多了。
磁盘 IO 的另一个隐形杀手是临时文件。当排序或哈希操作超过 work_mem 的时候,PG 会把中间结果写到磁盘的临时文件里。大量并发的排序操作,会瞬间把磁盘 IO 打满。这类问题可以通过设置 temp_file_limit 每个会话的临时文件上限来保护整体 IO,同时从 SQL 侧优化排序逻辑。
4. 存储与复制类故障:膨胀、WAL 与主从延迟
4.1 表膨胀和 bloat:清理不及时的代价
表膨胀的原因是 update/delete 产生的死元组没有被及时清理。PostgreSQL 的 MVCC 机制决定了,每次更新会生成新版本,旧版本要等 VACUUM 来清理。如果 autovacuum 跟不上 DML 的速度,表就会越来越大,查询会变慢,磁盘占用会飙升。
排查表膨胀,先看死元组比例:
SELECT schemaname, relname, n_live_tup, n_dead_tup, round(100 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_ratio, last_autovacuum, last_autoanalyze FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 20;如果 dead_ratio 持续很高,而 last_autovacuum 很久没更新,说明 autovacuum 有可能没跑起来,或者被长事务挡住了。你要知道,VACUUM 无法清理早于最老活动事务的元组,哪怕只有一个老掉牙的 idle in transaction 事务,它后面的所有死元组都会一直堆积。这就是为什么长事务不仅是锁的问题,还是膨胀的元凶。
解决膨胀,常规手段是手动执行VACUUM (VERBOSE, ANALYZE),更严重的可以考虑VACUUM FULL或者用在线重组的工具比如 pg_repack。但要评估好业务窗口,VACUUM FULL 会锁表,不能在生产高峰执行。
4.2 WAL 目录增长:日志把磁盘吃满了
pg_wal 目录暴涨是另一类让人头疼的存储故障。WAL 文件是 PostgreSQL 的预写日志,正常会在 checkpoint 后自动清理或归档,但如果清理跟不上,目录就会把磁盘塞满。
先看当前 WAL 目录的大小:
du -sh $PGDATA/pg_wal再检查以下配置和状态:max_wal_size 设得过大,checkpoint 间隔长,WAL 文件积累多;wal_level 为 replica 或逻辑复制时,如果有复制槽未确认,WAL 就不会被清理;开启归档时,如果归档命令失败,WAL 也会一直堆积。
排查复制槽是否异常的 SQL:
SELECT slot_name, slot_type, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal FROM pg_replication_slots;如果某个复制槽的 retained_wal 一直在涨,而对应的 standby 或订阅端已经不存在,这个槽就是“悬空槽”,可以直接删除以释放 WAL 空间。删除前一定要确认下游确实不再使用,否则复制会中断。
4.3 复制延迟与主从切换的常见坑
主从复制延迟会直接导致只读业务读不到最新数据,严重的情况下,备库会被迫追日志,CPU 和 IO 跟着飙高。
先看延迟的现状:
SELECT pid, application_name, client_addr, state, sync_state, pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_bytes, now() - replay_timestamp AS lag_time FROM pg_stat_replication;lag_bytes 表示主库快照点与备库回放点的差距,lag_time 表示最后一条回放日志距现在的时间。如果 lag 一直变大,最常见的原因是备库上有慢查询或者大事务占了资源,回放进程没有 CPU 可吃;还有一种情况是主库有大事务长时间未提交,备库要等事务提交才能应用,这个属于物理限制,只能从业务侧拆分大事务。
主从切换的坑我专门提一下。很多人以为切换就是把备库 promote 一下,但生产环境经常死在切换前的细节上:切换前没有确认备库回放已追平,结果 promote 之后丢了几秒数据;切换后没有改应用连接串,流量还在往老主库打;多节点环境下没有调整复制槽和权限配置。
我的经验是:把切换步骤写成脚本和 checklist,至少每季度演练一次,不要等到真故障了才临场翻文档。
5. 排查工具箱:必开参数、监控视图和巡检清单
5.1 生产环境建议常开的参数和视图
排查效率很大程度上取决于你平时积累了哪些“现场信息”。下面这些参数我建议任何生产库都认真评估开启:
| 参数项 | 建议值 | 作用 |
|---|---|---|
| log_min_duration_statement | 500~1000ms | 记录慢 SQL |
| log_lock_waits | on | 记录锁等待 |
| log_checkpoints | on | 记录检查点行为 |
| pg_stat_statements | shared_preload_libraries | 记录 SQL 执行统计 |
| track_io_timing | on | 统计 IO 耗时 |
| track_functions | all | 统计函数调用耗时 |
| temp_file_limit | 视内存而定 | 限制临时文件大小 |
监控视图方面,日常巡检至少要覆盖这五个:pg_stat_activity 看会话和当前 SQL;pg_stat_statements 看 SQL 资源消耗;pg_stat_user_tables 看 DML 量和死元组;pg_stat_replication 看复制状态;pg_stat_database 看库级 commit/rollback 和缓存命中率。
5.2 一张可以直接照抄的日常巡检清单
以下是我自己在用的巡检清单,每天定时跑一遍,很多故障在爆发之前就能发现苗头。第一项,连接数使用率,超过 80% 要预警;第二项,慢查询数量,统计最近 1 小时内超过阈值的 SQL;第三项,死元组比例,超过 20% 且持续增长要处理;第四项,锁等待数量,检查是否有持续超过 5 分钟以上的等待;第五项,复制延迟,超过业务容忍阈值要人工介入;第六项,WAL 目录大小和复制槽状态;第七项,磁盘使用率,包括数据目录和归档目录;第八项,备份任务是否正常完成,备份失败往往比数据库故障更隐蔽。
这个巡检清单不复杂,但坚持跑下来的收益非常大。我见过太多生产环境,平时不监控做基线,到了出故障的时候连“正常值是多少”都不知道。
6. 常见问题速查表与经验之谈
6.1 故障现象、可能原因与快速处理对照表
下面这张表是我这几年运维经验的浓缩,直接照着做能解决八成常见问题:
| 故障现象 | 大概率原因 | 快速处理动作 |
|---|---|---|
| 应用报连接数超限 | 连接池连接泄漏 / 连接数配置过小 | 查 pg_stat_activity 空闲连接并终止,调连接池上限 |
| 认证失败 password authentication failed | 密码错误或 pg_hba.conf 加密方式不匹配 | 查日志确认来源 IP,逐项检查 pg_hba.conf |
| 某条 SQL 突然变慢 | 执行计划变化 / 统计信息过期 / 索引失效 | EXPLAIN ANALYZE,必要时 ANALYZE 对应表 |
| 所有写操作卡死 | 存在持锁未提交的长事务 | 查 pg_locks 阻塞链,终止源事务或等其提交 |
| CPU 正常但磁盘 IO 打满 | 检查点刷脏 / 临时文件大量写盘 | 调 max_wal_size,查 temp_file_limit |
| 表一直膨胀 | autovacuum 关闭 / 长事务阻塞 | 查死元组比例,手动 VACUUM,找长事务 |
| pg_wal 目录暴涨 | 复制槽未消费 / 归档失败 | 查 pg_replication_slots 和归档日志状态 |
| 主从延迟持续增大 | 备库有慢查询 / 大事务阻塞回放 | 查备库 pg_stat_activity 和回放进程状态 |
| 内存使用持续走高 | shared_buffers 过大 / 连接数太多 | 调参数并限制连接数,检查是否触发 OOM |
6.2 我的几点真心建议
最后说几个我在实战中沉淀下来的习惯。第一,不要轻易在生产环境动 max_connections,很多故障其实是应用层连接池配置不合理,把连接池上限调下来,数据库反而更稳。第二,慢日志和 pg_stat_statements 一定要开,这是所有故障排查的地基,没有数据,一切排查都是猜。第三,建立自己的故障案例库,每次出问题都记录现象、排查过程、根因和解决动作,半年之后你就有了一个非常宝贵的知识库。
我在实际维护过程中还有一个体会,很多故障之所以变成大故障,不是因为问题有多复杂,而是因为团队在故障发生时没有统一的排查入口,各自为战。如果你能提前把上面这些 SQL、命令和 checklist 整理成一份团队内的排查手册,遇到问题时按图索骥,效率会提升很多。