主题
PgSQL 监控指标与告警
一句话:PostgreSQL 的雪崩通常不是某条 SQL 突然变慢,而是长事务或复制槽卡住 WAL 回收、死元组拖垮 autovacuum、连接耗尽后重试放大;盯住这三条慢变量,比盯 QPS 更早拿到处置窗口。适用版本:PostgreSQL 13+ / 16+,阈值按基线校准。
为什么需要
PostgreSQL 的统计视图大多是累计值,重启或 pg_stat_reset() 后清零;直接拿绝对值设阈值会误报,必须看单位时间增量。更关键的是,它的故障往往先体现在「空间与回收」而不是「CPU」:WAL 不回收会写满磁盘,死元组不回收会让表和索引膨胀,idle in transaction 会同时拖住锁和 vacuum。因此监控要分清结果指标和先兆指标。P99、错误率是结果;复制槽保留的 WAL、idle in transaction 时长、死元组比例、checkpoint 频率是先兆。下面按连接、库表、复制/WAL、checkpoint、锁、SQL 扩展分层,最后串成雪崩链。
核心机制
连接:进程模型决定了上限
| state | 含义 | 风险 |
|---|---|---|
active | 正在执行查询 | 结合 wait_event_type 判断在等锁、等 IO 还是等客户端 |
idle | 空闲,不在事务中 | 本身无害,但连接池泄漏会堆高总数 |
idle in transaction | 开了事务但没提交 | 持有快照和锁,阻塞 vacuum,最容易拖出膨胀和 DDL 排队 |
max_connections 默认 100,另有 superuser_reserved_connections 默认 3 个保留位。每个连接是独立进程,连接数受内存和进程调度约束;idle_in_transaction_session_timeout 默认 0(不超时)。连接耗尽的路径是:查询变慢 → 连接占用变长 → 达到上限报 too many clients → 应用重试 → 新连接更多,形成正反馈。
数据库与表:命中率、死元组、扫描
pg_stat_database:blks_hit与blks_read算共享缓冲命中率;deadlocks看死锁;temp_bytes看排序/哈希落盘;xact_rollback与xact_commit算回滚比例。pg_stat_user_tables:n_dead_tup、last_autovacuum、seq_scan/idx_scan。死元组比例用n_dead_tup / (n_live_tup + n_dead_tup);last_autovacuum过旧说明回收没跟上。autovacuum 默认在「死元组超过 20% + 50 行」时触发(autovacuum_vacuum_scale_factor=0.2,autovacuum_vacuum_threshold=50),大表 20% 可能是几千万行,必须按表调小比例。idle in transaction持有xmin,快照还需要看到的旧版本无法被 vacuum 回收;长事务不结束,vacuum 做了也白做。精确膨胀用pgstattuple,近似用pgstattuple_approx,清理优先pg_repack,不要在高峰做VACUUM FULL。
复制与 WAL:最容易把磁盘写满
pg_stat_replication看state、write_lag、flush_lag、replay_lag,以及pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)的字节差。replay_lag是时间,字节差更适合判断「还差多少」。pg_replication_slots看active、restart_lsn、wal_status、safe_wal_size,用pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)算保留的 WAL。槽不消费,WAL 就一直留着。max_slot_wal_keep_size(13+,默认 -1 不限制)是保险丝:超过后槽会失效(wal_status=lost),订阅端会断。设置它是拿「复制连续性」换「磁盘安全」,必须明确取舍。- 归档失败同样会留住 WAL:
pg_stat_archiver的failed_count、last_failed_wal、last_failed_time持续增长,pg_wal就会涨。max_wal_size是软限制,不能阻止复制槽或归档需要的 WAL 继续堆积。
checkpoint 与 IO:抖动来自集中刷脏
pg_stat_bgwriter 在 13–16 提供 checkpoint 与 bgwriter 计数;17 起 checkpoint 统计拆到 pg_stat_checkpointer,以版本文档为准。16+ 的 pg_stat_io 按 backend_type、object、context 看读写、命中、eviction 和 fsync,能区分是 checkpointer、client backend 还是 autovacuum 在打 IO。checkpoint_timeout 默认 5 分钟,max_wal_size 默认 1GB,checkpoint_completion_target 在 14+ 默认 0.9(13 为 0.5)。WAL 生成超过 max_wal_size 会触发 requested checkpoint;脏页集中刷盘,await 抖动,查询 P99 跟着上升。buffers_backend_fsync > 0 说明后台写不过来、客户端进程被迫自己 fsync,是 IO 压力的强信号。
锁与 SQL 扩展:把阻塞链和慢 SQL 落到会话
pg_locks要与pg_stat_activity关联看;pg_blocking_pids(pid)直接返回阻塞它的 PID 数组。DDL 取ACCESS EXCLUSIVE,它排队后,后续读请求也可能排在它后面,表现为整表查询突然卡住。lock_timeout默认 0(不超时),deadlock_timeout默认 1 秒;应用侧要设语句和事务超时,不能只靠数据库。pg_stat_statements需要shared_preload_libraries加CREATE EXTENSION;13+ 用total_exec_time/mean_exec_time,更早版本是total_time。track_io_timing默认关闭时,blk_read_time/blk_write_time为 0。pg_stat_progress_vacuum可看 vacuum 进度。
雪崩链路
左边是空间链:WAL 回收被卡住后,磁盘写满会让实例 PANIC 或宕机,这是最短的雪崩路径。右边是回收与连接链:死元组让扫描变慢,连接被占满后重试又反过来制造长事务。下方 checkpoint 是加速器,它不会单独致命,但会把已经紧张的 IO 推过阈值。
最小上手
sql
-- 连接按状态和等待事件聚合
SELECT state, wait_event_type, wait_event, count(*)
FROM pg_stat_activity WHERE backend_type = 'client backend'
GROUP BY 1,2,3 ORDER BY 4 DESC;
-- 长事务与 idle in transaction
SELECT pid, datname, state, now() - xact_start AS xact_age,
now() - state_change AS state_age, wait_event_type, wait_event, left(query, 80) AS query
FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_age DESC LIMIT 10;
-- 复制延迟与槽保留的 WAL
SELECT application_name, state, sync_state,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_bytes,
write_lag, flush_lag, replay_lag
FROM pg_stat_replication;
SELECT slot_name, active, wal_status,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots
ORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC;
SELECT archived_count, failed_count, last_archived_time, last_failed_time FROM pg_stat_archiver;
-- 死元组、扫描方式与阻塞链
SELECT relname, n_live_tup, n_dead_tup,
round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 2) AS dead_pct,
seq_scan, idx_scan, last_autovacuum
FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10;
SELECT pid, pg_blocking_pids(pid) AS blockers, wait_event_type, wait_event, left(query, 80) AS query
FROM pg_stat_activity WHERE cardinality(pg_blocking_pids(pid)) > 0;promql
# 共享缓冲命中率
100 * sum(rate(pg_stat_database_blks_hit[5m])) / clamp_min(sum(rate(pg_stat_database_blks_hit[5m])) + sum(rate(pg_stat_database_blks_read[5m])), 1)
# 连接使用率(指标名随 postgres_exporter 版本和 collector 变化,以 /metrics 为准)
100 * sum(pg_stat_database_numbackends) / scalar(pg_settings_max_connections)生产动作
| 告警 | 起点阈值 | 说明 |
|---|---|---|
| 连接使用率 | 超过 max_connections 的 80%,持续 5 分钟 | 先查泄漏和 idle in transaction,不要只调大上限 |
| 长事务 | 事务打开超过 5 分钟;idle in transaction 超过 60 秒 | 它同时拖住锁、快照和 vacuum |
| 锁等待 | 阻塞会话超过 5 秒,或 wait_event_type='Lock' 数量超基线 | 用 pg_blocking_pids() 找根节点,不要杀所有等待者 |
| 复制延迟 | replay_lag > 30s、字节差持续增长,或 state != streaming | 秒数之外还要看 WAL 字节差 |
| 复制槽 | 槽 inactive 超过 10 分钟,或保留 WAL 超过 10GB | 起点值;确认订阅端是否真的下线再处理 |
| WAL 与磁盘 | pg_wal 超基线、磁盘超过 80%、归档失败计数增长 | 归档失败和槽滞留是两条独立原因 |
| 死元组 | 单表死元组比例超过 20%,且 last_autovacuum 超过 24 小时 | 大表要按表调阈值,不能只看全局默认 |
| checkpoint 与临时文件 | requested checkpoint 频率超基线、buffers_backend_fsync > 0,或 temp_bytes 增量突增 | 结合 pg_stat_io 看谁在写;临时文件突增常是排序/哈希落盘或锁冲突 |
处置顺序默认是「先保护连接和 WAL,再处理膨胀与锁,最后扩容」:
- 保护入口:终止明确的
idle in transaction,设置statement_timeout、lock_timeout、idle_in_transaction_session_timeout;连接池满先修池,不要只加max_connections。 - 止住 WAL 增长:先修归档;对 inactive 槽,确认订阅端/备库确实不再需要后才能删除,绝不能删 active 槽;把
max_slot_wal_keep_size当作保险丝而不是日常回收手段。 - 处理锁与膨胀:杀掉阻塞链根节点的事务;膨胀优先
pg_repack或错峰 vacuum,不在高峰VACUUM FULL。 - 定位与扩容:用
pg_stat_statements和pg_stat_io判断读放大、写放大还是 checkpoint;读瓶颈加副本/缓存,写瓶颈分区或分片,checkpoint 参数调整要结合 WAL 生成率验证。
边界
- 统计视图是累计值或估算值:
n_dead_tup是估算,精确值用pgstattuple;pg_stat_statements需要 preload,未加载时查不到。 replay_lag受时钟和网络影响;跨机房或同步复制下,要结合 LSN 字节差和业务读一致性判断。- 复制槽是「保留 WAL」的承诺,不是备份;槽删了 WAL 可能立刻回收,但订阅端也可能永久断档。
max_wal_size不是硬上限,归档或槽需要时会继续增长,磁盘告警必须独立于它。监控也替代不了业务指标,用户侧 P99、错误率和数据新鲜度才是最终判据。
优缺点与扬长避短
优点是扩展性强:pgvector、PostGIS、JSONB、全文检索都能作为普通列和普通索引参与事务;SQL 标准和复杂查询强(CTE、窗口、LATERAL、并行查询);复制形态多(物理/逻辑、同步/异步、级联),DDL 事务化。代价是 autovacuum 和 bloat 必须长期治理;没有聚簇索引(CLUSTER 是一次性且锁表,之后不维持顺序);复制槽与 WAL 运维有坑;每连接一个进程,连接成本高;超高写入吞吐仍要靠分片方案。
| 约束条件 | 判断 | 说明 |
|---|---|---|
| 业务库同时要 JSONB、全文、地理或向量 | 适用 | 扩展与业务数据共享事务,少一套同步链路 |
| 复杂报表、窗口、CTE、并行查询 | 适用 | 优化器和 SQL 能力更合适 |
| 需要逻辑复制、多订阅或 CDC | 适用 | 逻辑复制形态完整,但要管槽和 WAL |
| 超高写入、海量简单 KV | 单实例不适用 | 应分片或换专用系统,不是调参能解决 |
| 团队没有 vacuum/bloat 治理能力 | 不适用 | 需要托管服务或长期治理机制 |
| 连接数极高且没有连接池 | 不适用 | 进程模型成本高,先用 PgBouncer |
与 MySQL 按约束对比:单主点查和成熟托管生态,MySQL 更省心;事务内混合 JSONB/全文/地理/向量、复杂查询、逻辑复制,PgSQL 更合适;超高连接数两者都要池化,PgSQL 的进程模型更敏感;超高写入两者都要分片,MySQL 的分库分表中间件生态更常见,PgSQL 可用 Citus。结论不是谁更强,而是约束落在哪一边。
讲清楚
PostgreSQL 监控先看三条慢变量:WAL 能不能回收、死元组能不能被 vacuum 回收、连接有没有被慢查询和重试占满。WAL 侧盯 pg_stat_replication 的 lag、pg_replication_slots 保留的 WAL、pg_stat_archiver 的失败计数和 pg_wal 目录大小;回收侧盯 pg_stat_user_tables 的死元组比例和 last_autovacuum,并用 pg_blocking_pids() 找锁阻塞根节点;连接侧按 pg_stat_activity 的 state 和 wait_event 聚合,重点抓 idle in transaction。雪崩链是「长事务/未消费槽 → WAL 或死元组积压 → 磁盘满或表膨胀 → P99 上升 → 连接耗尽 → 重试放大」。处置先保护连接和 WAL,再处理膨胀与锁,最后扩容;复制槽、autovacuum 和连接池是长期治理项,不是一次调参。