别等数据库挂了才翻仪表盘:PostgreSQL 关键 PMM 告警配置指南

2026-05-26 19 预计阅读时间:1 分钟
来源:postgr.es AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:18 分钟

周五傍晚,你收拾好东西离开办公室,心里很踏实——系统跑了一整周没出问题。与此同时,服务器磁盘利用率悄悄爬到了 90%,WAL 文件堆积如山,一条查询已经执行超过一小时,没人注意到,因为仪表盘上的数字"看起来还行"。等到写入开始报错、同事发消息问"数据库好像有点慢?"的时候,数据库已经不是慢了——它已经挂了。

这不是什么新鲜事。麻烦的信号一直都在,只是监控没有连上正确的指标来拉响警报。这篇文章不讲理论,只讲在生产环境里真正能救命的 PMM 告警规则。

仪表盘和告警不是一回事

仪表盘擅长展示趋势,但它有一个危险的前提假设:有人会在来得及之前去看它。告警不做这个假设。

仪表盘温柔地告诉你 CPU 在升高;告警直接喊:"已经坏了 30 分钟了,醒醒。"仪表盘等你去看;告警主动找到你。经历过几次事故之后,你不会再指望仪表盘在半夜保护你——真正起作用的是告警。

操作系统层:三个必须拉响的红色警报

数据库进程可能完全健康,但底层服务器磁盘满了、内存耗尽了,PostgreSQL 同样会停摆。所以第一组告警盯的是操作系统。

磁盘空间低于 10%

磁盘到零的时候,PostgreSQL 不是挣扎——它是直接停。WAL 写入失败、checkpoint 失败、脏页无法刷盘、事务卡死。更麻烦的是,磁盘从来不是突然满的:没人清理的复制槽、悄悄坏掉的日志轮转、持续膨胀数周的表——都是渐进过程。等磁盘变成 Critical 时,早期修复的机会已经错过了。

(node_filesystem_avail_bytes{fstype!~"tmpfs|overlay", node_name="<node_name>"}
  / node_filesystem_size_bytes{fstype!~"tmpfs|overlay", node_name="<node_name>"}) * 100 < 10

可用内存低于 10%

内存压力也是渐进的。可用内存减少 → 查询变慢 → OS 开始换页 → 系统 swap → 性能崩塌。毫秒级查询拉伸到秒级,恢复通常需要重启 PostgreSQL。如果压力继续累积,Linux OOM killer 会出手终止进程,PostgreSQL 不在豁免名单上。

(node_memory_MemAvailable_bytes{node_name="<node_name>"}
  / node_memory_MemTotal_bytes{node_name="<node_name>"}) * 100 < 10

CPU 持续 5 分钟超过 80%

短暂的 CPU 峰值在任何活跃数据库上都正常。这条告警只在 CPU 连续 5 分钟超过 80% 时触发,通常意味着结构性问题:统计信息过期导致计划回归、大表缺少索引、严重膨胀表上的失控 autovacuum、或者一条没人终止的临时长查询。

(1 - avg by (node_name)(rate(node_cpu_seconds_total{mode="idle", node_name="<node_name>"}[5m]))) * 100 > 80

PostgreSQL 层:七条不能忽略的规则

PostgreSQL 宕机(pg_up == 0)

PMM 通过活连接采集指标。连接失败时 pg_up 返回 0——可能是进程崩溃、端口阻塞、凭据失效,但对应用来说区别不重要:数据库不可达就是不可达。这不是性能告警,这是宕机告警,没有"回头再看"的余地。

pg_up{service_name="<service_name>"} == 0

活跃连接超过 max_connections 的 80%

PostgreSQL 的连接限制是硬天花板,不是软阈值。达到上限后新连接直接被拒:FATAL: sorry, too many clients already。在 80% 触发告警,是因为这个水位还有调查空间——连接泄漏、部署意外翻倍了连接池、未回收的 idle session。正确做法是引入 PgBouncer 做连接池化,而不是简单调大 max_connections(每个连接都有内存开销,盲目调大会 destabilize 服务器)。

(sum(pg_stat_activity_count{service_name="<service_name>"})
  / pg_settings_max_connections{service_name="<service_name>"}) * 100 > 80

非预期重启

PostgreSQL 不会无缘无故重启。这条规则监控 postmaster 启动时间戳,5 分钟窗口内发生变化就告警。更危险的场景是:PostgreSQL 干净恢复了,一切看起来健康,没人去查原因。但 OOM kill、未沟通的手动 pg_ctl restart、有缺陷的扩展崩溃——每种原因需要不同应对。告警触发时,第一件事是查 PostgreSQL 日志。

changes(pg_postmaster_start_time_seconds{service_name="<service_name>"}[5m]) > 0

查询执行超过 1 小时

生产系统中跑超过一小时的查询几乎总是症状:缺索引、统计信息更新后的计划回归、开发环境小数据集上表现正常但生产扫描了几千万行的 join。也有合理场景——定时批处理、已知耗时的报表查询。终止前先搞清楚它在做什么,否则同样的查询会在相同条件下再次出现。

max(pg_stat_activity_max_tx_duration{service_name="<service_name>", state="active"}) > 3600

Idle in Transaction 超过 1 小时

这比长查询更具破坏性。活跃查询至少在推进,idle in transaction 则是:开了事务、拿了锁、然后完全停了。不做任何工作,但锁持续持有、autovacuum 被阻塞、连接槽被占用。健康事务在毫秒到秒级完成,idle 一小时意味着应用 bug、进程异常退出没 rollback、客户端断连没关事务。不会自行恢复,必须手动调查。

max(pg_stat_activity_max_tx_duration{service_name="<service_name>",
  state=~"idle in transaction|idle in transaction \\(aborted\\)"}) > 3600

事务 ID 环绕超过 75%

忽略这条告警足够久,PostgreSQL 会直接关闭数据库保护数据——没有警告,没有宽限期。32 位事务 ID 约 20 亿范围,计数器攀升时旧行版本必须被冻结,正常由 autovacuum 处理。当 autovacuum 因高写入负载、表膨胀或阻塞事务跟不上时,计数器不受控地攀升。到硬限制时,PostgreSQL 关闭受影响数据库、拒绝所有连接。75% 时还有时间行动,但如果百分比在持续上升而非持平,必须查明 autovacuum 为什么落后。

pg_stat_database_wraparound_percent{service_name="<service_name>"} > 75

表膨胀超过 70% / 索引膨胀超过 60%

每次 UPDATE/DELETE 留下死行版本,autovacuum 正常时回收它们。跟不上时,死行堆积在磁盘、查询花时间跳过它们、存储无限增长。表膨胀超过 70% 后,顺序扫描变慢、shared buffers 填满死数据、autovacuum 每轮耗时更长。索引膨胀往往是更 disruptive 的问题——膨胀索引包含空隙和死条目,强制索引扫描读取远超必要的页面,即使表本身看起来健康,查询时间也可能显著增加。

# 表膨胀
pg_stat_user_tables_table_bloat_ratio{node_name="<node_name>", service_name="<service_name>"} > 70

# 索引膨胀
pg_stat_user_indexes_idx_bloat_ratio{node_name="<node_name>", service_name="<service_name>"} > 60

实操:把告警规则写进 PMM

PMM 的告警底层基于 Prometheus Alertmanager,除了在 UI 上逐条创建,更高效的做法是用 YAML 文件批量管理规则。下面是一份可以直接改造使用的告警规则文件,覆盖了上面讨论的核心条目。

使用前需要替换 <node_name><service_name> 为你环境中 PMM 实际的服务标识符,可以通过 PMM UI 的 Inventory 页面或以下命令查看:

# 在 PMM Server 上查看已注册的 node_name
pmm-admin list

# 输出类似:
# SERVICE TYPE   SERVICE NAME          NODE NAME
# postgresql     pg-prod-main          db-node-01

以下是完整的告警规则 YAML 文件:

# pmm-postgres-alerts.yml
# 放到 PMM Server 的 /etc/prometheus/alert_rules/ 目录下,
# 或通过 PMM UI 的 Alerting → Alert Rules 页面导入。
# 重启 PMM 或等待 Prometheus 自动 reload 即生效。

groups:
  - name: os-health-critical
    rules:
      - alert: DiskSpaceCritical
        expr: >
          (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay", node_name="db-node-01"}
            / node_filesystem_size_bytes{fstype!~"tmpfs|overlay", node_name="db-node-01"}) * 100 < 10
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "磁盘空间低于 10%"
          description: "节点 {{ $labels.node_name }} 可用磁盘仅剩 {{ $value }}%,PostgreSQL 可能即将停摆。"

      - alert: MemoryCritical
        expr: >
          (node_memory_MemAvailable_bytes{node_name="db-node-01"}
            / node_memory_MemTotal_bytes{node_name="db-node-01"}) * 100 < 10
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "可用内存低于 10%"
          description: "节点 {{ $labels.node_name }} 可用内存仅剩 {{ $value }}%,OOM killer 可能即将介入。"

      - alert: CPUHighCritical
        expr: >
          (1 - avg by (node_name)(rate(node_cpu_seconds_total{mode="idle", node_name="db-node-01"}[5m]))) * 100 > 80
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "CPU 持续超过 80%"
          description: "节点 {{ $labels.node_name }} CPU 连续 5 分钟超过 80%,当前值 {{ $value }}%。"

  - name: postgresql-critical
    rules:
      - alert: PostgreSQLDown
        expr: pg_up{service_name="pg-prod-main"} == 0
        for: 0m
        labels:
          severity: critical
        annotations:
          summary: "PostgreSQL 宕机"
          description: "服务 {{ $labels.service_name }} 不可达,数据库已宕机。"

      - alert: TooManyConnections
        expr: >
          (sum(pg_stat_activity_count{service_name="pg-prod-main"})
            / pg_settings_max_connections{service_name="pg-prod-main"}) * 100 > 80
        for: 3m
        labels:
          severity: critical
        annotations:
          summary: "连接数超过 80% 上限"
          description: "服务 {{ $labels.service_name }} 活跃连接占比 {{ $value }}%,即将触及硬上限。"

  - name: postgresql-warning
    rules:
      - alert: PostgreSQLUnexpectedRestart
        expr: changes(pg_postmaster_start_time_seconds{service_name="pg-prod-main"}[5m]) > 0
        for: 0m
        labels:
          severity: warning
        annotations:
          summary: "PostgreSQL 非预期重启"
          description: "服务 {{ $labels.service_name }}  5 分钟内发生了重启,请立即检查日志。"

      - alert: LongRunningQuery
        expr: >
          max(pg_stat_activity_max_tx_duration{service_name="pg-prod-main", state="active"}) > 3600
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "查询执行超过 1 小时"
          description: "服务 {{ $labels.service_name }} 有查询已执行 {{ $value }} 秒。"

      - alert: IdleInTransactionTooLong
        expr: >
          max(pg_stat_activity_max_tx_duration{service_name="pg-prod-main",
            state=~"idle in transaction|idle in transaction \\(aborted\\)"}) > 3600
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Idle in Transaction 超过 1 小时"
          description: "服务 {{ $labels.service_name }} 有事务 idle 超过 {{ $value }} 秒,锁和 autovacuum 被阻塞。"

      - alert: WraparoundRisk
        expr: pg_stat_database_wraparound_percent{service_name="pg-prod-main"} > 75
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "事务 ID 环绕风险"
          description: "服务 {{ $labels.service_name }} 环绕百分比 {{ $value }}%,超过 75% 阈值。"

      - alert: TableBloatHigh
        expr: >
          pg_stat_user_tables_table_bloat_ratio{node_name="db-node-01", service_name="pg-prod-main"} > 70
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "表膨胀超过 70%"
          description: "表 {{ $labels.relname }} 膨胀率 {{ $value }}%,顺序扫描和 autovacuum 效率下降。"

      - alert: IndexBloatHigh
        expr: >
          pg_stat_user_indexes_idx_bloat_ratio{node_name="db-node-01", service_name="pg-prod-main"} > 60
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "索引膨胀超过 60%"
          description: "索引 {{ $labels.relname }}.{{ $labels.index_relname }} 膨胀率 {{ $value }}%。"

部署步骤:

# 1. 将 YAML 文件放到 PMM Server 上
scp pmm-postgres-alerts.yml <pmm-server-host>:/etc/prometheus/alert_rules/

# 2. 让 Prometheus 重新加载规则(PMM v2+ 通常自动检测,也可手动触发)
curl -X POST http://<pmm-server-host>:9090/-/reload

# 3. 验证规则已加载
curl http://<pmm-server-host>:9090/api/v1/rules | python3 -m json.tool | head -40

# 4. 配置 Alertmanager 的 contact point(路由到 Slack/PagerDuty/邮件)
#    编辑 /etc/alertmanager/alertmanager.yml 或在 PMM UI 中设置

告警触发后,快速排查的几个常用命令:

# 磁盘告警触发时:找出最大的文件
du -ah /var/lib/postgresql/ | sort -rh | head -20

# 连接告警触发时:查看当前连接分布
psql -c "SELECT state, count(*) FROM pg_stat_activity GROUP BY state ORDER BY count DESC;"

# Idle in Transaction 告警触发时:定位阻塞源头
psql -c "SELECT pid, now() - xact_start AS duration, query, state
         FROM pg_stat_activity
         WHERE state IN ('idle in transaction', 'idle in transaction (aborted)')
         ORDER BY duration DESC LIMIT 10;"

# 环绕告警触发时:检查各数据库的事务 ID 年龄
psql -c "SELECT datname, age(datfrozenxid) AS xid_age,
         current_setting('autovacuum_freeze_max_age')::int AS freeze_max
         FROM pg_database ORDER BY xid_age DESC;"

# 重启告警触发时:查看最近日志中的关键事件
tail -200 /var/log/postgresql/postgresql-*.log | grep -iE 'fatal|panic|oom|crash|restart'

告警疲劳:比没有告警更危险

告警建了,又加了几条,手机一天响 40 次,团队开始对每一条都无感。这就是监控悄悄失败的方式——告警存在、阈值设了,但信号被噪声埋到没人再行动。

好的告警不是捕获一切,而是可靠地捕获重要的事。如果一条 Warning 每天触发只是因为阈值刚好设在系统正常运行水位之下,那不是监控,那是噪声。目标应该是:每一条触发的告警都值得你从别的事情中抽身去看。

几个减少噪声的实操原则:

  • for 字段是降噪利器:所有非宕机类告警都加上持续时间门槛(上面 YAML 里的 for: 2m / 5m / 10m),短暂峰值不触发。
  • Critical 只留给必须立即行动的事件:宕机、磁盘满、内存耗尽、连接即将耗尽——这些是"现在就得处理"的事。其余用 Warning。
  • 定期审计:每季度回顾告警触发记录,触发超过 20 次但从未引发行动的规则,要么调阈值要么删除。
  • 路由分离:Critical 走 PagerDuty/电话;Warning 走 Slack 频道。不同严重级别不同通道,避免混在一起互相淹没。

检查清单

把这篇文章变成行动,对照下面清单逐项确认:

检查项 状态
OS 层三条 Critical 告警已配置(磁盘 <10%、内存 <10%、CPU >80% 持续 5m)
PostgreSQL 宕机告警已配置(pg_up == 0)
连数超 80% 告警已配置
非预期重启告警已配置,且触发后第一动作是查日志
长查询和 idle in transaction 告警已配置
环绕告警已配置(>75%)
表/索引膨胀告警已配置
所有告警的 for 持续时间门槛已设置,避免瞬时峰值噪声
Critical 和 Warning 路由到不同通知通道
<node_name><service_name> 已替换为实际值
Alertmanager contact point 已测试(发一条测试告警确认通道可达)

监控不是看图表觉得一切正常。它是在还有时间行动的时候抓住问题——在磁盘耗尽之前、在连接撞上限之前、在 autovacuum 跑到无法恢复之前。告警触发时,调查是你唯一的选择。你忽略的那一条,可能就是让你通宵的那一条。


相关推荐