前言 链接到标题

PostgreSQL 是生产环境最常用的关系型数据库之一。当服务挂了、连接爆了、死锁了,需要第一时间感知。

单纯靠 Prometheus 指标可以告诉你「连接数超了」,但说不出原因;单纯靠日志可以告诉你「too many connections」,但没有量化趋势。

本文的方案是 指标 + 日志协同监控,形成完整可观测性闭环:

  • 指标告警 → 感知异常(量变)
  • 日志告警 → 定位根因(质变)
  • 统一通知 → 飞书即时推送

架构 链接到标题

下面是完整的数据流:

graph TD PG[PostgreSQL 17
monkey:5432] --> PE[postgres-exporter
monkey:9187] PE --> PM[Prometheus
robin:9090] PM --> AM[Alertmanager
robin:9093] PG -->|Docker logs| AL[Alloy
monkey:12346] AL --> LK[Loki
robin:3100] LK -->|Loki Ruler LogQL| AM AM --> AT[alert-transformer
robin:9091] AT --> OC[OpenClaw
rivo:18789] OC --> FS[飞书] subgraph 指标路径 PE PM end subgraph 日志路径 AL LK end subgraph 通知路径 AM AT OC FS end

两条路径独立采集、独立告警,最终汇总到同一个通知链路。

一、部署环境 链接到标题

主机 IP 角色
monkey 192.168.0.73 PostgreSQL 17 + postgres-exporter + Alloy
robin 192.168.0.81 Prometheus + Loki + Alertmanager

PostgreSQL 版本 17.9,部署在 Docker 中(m.daocloud.io/docker.io/postgres:17-alpine),已开启 WAL 逻辑复制用于上游业务。

二、指标监控 — postgres_exporter + Prometheus 链接到标题

部署 postgres-exporter 链接到标题

在 monkey 上新建目录和 docker-compose:

# /opt/postgres-exporter/docker-compose.yaml
services:
  postgres-exporter:
    image: m.daocloud.io/docker.io/prometheuscommunity/postgres-exporter:latest
    container_name: postgres-exporter
    restart: always
    network_mode: host
    environment:
      - DATA_SOURCE_NAME=postgresql://postgres:password@localhost:5432/postgres?sslmode=disable
    command:
      - '--web.listen-address=:9187'

关键点:

  • network_mode: host — 直接通过 localhost 连接 PG,绕开 Docker 桥接网络
  • DATA_SOURCE_NAME — 数据库连接串,密码用实际值

启动:

docker compose up -d
# 验证
curl -s http://localhost:9187/metrics | grep pg_up
pg_up 1

输出约 1200+ 个 pg_ 指标,覆盖连接数、锁、死锁、事务、缓存命中率、复制延迟、数据库大小等。

Prometheus 抓取配置 链接到标题

新增 job:

# monitor/prometheus/prometheus.yml
  - job_name: postgres_monkey
    static_configs:
      - targets:
          - 192.168.0.73:9187
        labels:
          hostname: monkey

Prometheus 告警规则 链接到标题

# monitor/prometheus/alerts.yml
  - alert: PostgresDown
    expr: pg_up{server="localhost:5432"} == 0
    for: 1m
    labels:
      severity: critical

  - alert: PostgresConnectionUsageHigh
    expr: sum by(server) (pg_stat_activity_count{datname!~"template.*"})
          / pg_settings_max_connections * 100 > 85
    for: 5m
    labels:
      severity: warning

  - alert: PostgresDeadlocksDetected
    expr: rate(pg_stat_database_deadlocks[5m]) > 0
    for: 1m
    labels:
      severity: warning

三、日志监控 — Alloy + Loki + LogQL 链接到标题

部署 Alloy 链接到标题

在 monkey 上新开目录,部署独立的 Alloy 实例专门采集 postgres 日志:

# /opt/alloy-postgres/docker-compose.yaml
services:
  alloy:
    image: m.daocloud.io/docker.io/grafana/alloy:latest
    container_name: alloy-postgres
    restart: always
    network_mode: host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./config.alloy:/etc/alloy/config.alloy
    command:
      - run
      - /etc/alloy/config.alloy
      - --server.http.listen-addr=0.0.0.0:12346

Alloy 配置详解 链接到标题

// 发现 Docker 容器
discovery.docker "postgres" {
  host             = "unix:///var/run/docker.sock"
  refresh_interval = "5s"
}

// 只保留 postgres17 容器,重命名为 container 标签
discovery.relabel "postgres" {
  targets = discovery.docker.postgres.targets
  rule {
    source_labels = ["__meta_docker_container_name"]
    regex         = "/(postgres17)$"
    target_label  = "container"
  }
}

// 采集 Docker 日志
loki.source.docker "postgres" {
  host             = "unix:///var/run/docker.sock"
  targets          = discovery.relabel.postgres.output
  forward_to       = [loki.process.postgres.receiver]
  refresh_interval = "5s"
}

// 解析纯文本日志格式,提取 level 标签
loki.process "postgres" {
  stage.regex {
    expression = "(?P<ts>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3} \\w+) \\[(?P<pid>\\d+)\\] (?P<level>\\w+):\\s+(?P<msg>.*)"
  }
  stage.labels {
    values = {
      level     = "",
      container = "",
    }
  }
  forward_to = [loki.write.robin.receiver]
}

// 发送到中央 Loki
loki.write "robin" {
  endpoint {
    url       = "http://192.168.0.81:3100/loki/api/v1/push"
    tenant_id = "tenant1"
  }
}

三个坑 链接到标题

  1. 正则必须有 capture group

    // 错误:$1 会是整个匹配值,label 变成 "/postgres17"
    regex = "/postgres17$"
    
    // 正确:用 () 捕获纯名称 "postgres17"
    regex = "/(postgres17)$"
    
  2. PostgreSQL jsonlog 在 Docker 中不可用

    尝试过 log_destination=jsonlog,但 PG 的 jsonlog 仅在 logging_collector=on 时输出 JSON,开启后日志写入文件而非 stderr,Docker 采集不到。最终使用 stage.regex 解析传统纯文本格式。

  3. Alloy 端口冲突

    monkey 与 robin 会在同一 subnet,如果 robin Alloy 用了 :12345,monkey 这个实例改用 :12346 避免冲突。

Loki 日志告警 — LogQL 规则 链接到标题

# /opt/monitor/loki-data/rules/tenant1/postgres.yaml
groups:
  - name: postgres_monkey_errors
    interval: 30s
    rules:
      - alert: PostgresFatalError
        expr: |
          count_over_time({container="postgres17"}
            |~ "FATAL|PANIC" [5m]) > 0
        for: 1m
        labels:
          severity: critical

      - alert: PostgresErrorMessage
        expr: |
          count_over_time({container="postgres17"}
            |~ "ERROR" [5m]) > 2
        for: 2m
        labels:
          severity: warning

      - alert: PostgresConnectionLimit
        expr: |
          count_over_time({container="postgres17"}
            |~ "too many connections" [5m]) > 0
        labels:
          severity: critical

      - alert: PostgresDeadlock
        expr: |
          count_over_time({container="postgres17"}
            |~ "deadlock detected" [5m]) > 0
        labels:
          severity: warning

      - alert: PostgresOOMKill
        expr: |
          count_over_time({container="postgres17"}
            |~ "[Oo]ut of memory|OOM|killing process" [5m]) > 0
        labels:
          severity: critical

注意权限:Loki API 需要 X-Scope-OrgID header,查询和写入都要带上。

四、告警通知链路 链接到标题

graph LR AM[Alertmanager] --> AT[alert-transformer] AT --> OC[OpenClaw] OC --> FS[飞书 Webhook]
  • Alertmanager (robin:9093) — 接收 Prometheus 和 Loki Ruler 推送的告警
  • alert-transformer (robin:9091) — 格式转换、去重、分级路由
  • OpenClaw (rivo:18789) — AI Agent Gateway,最终推送到飞书群

五、验证与维护 链接到标题

验证命令 链接到标题

# 检查 exporter 是否抓取到 PG
ssh robin "curl -s 'http://localhost:9090/api/v1/query?query=pg_up'"

# 检查 Loki 中 postgres 日志
ssh robin "curl -s -G -H 'X-Scope-OrgID: tenant1' \
  'http://localhost:3100/loki/api/v1/query_range' \
  --data-urlencode 'query={container=\"postgres17\"}' \
  --data-urlencode 'limit=3'"

# 查看 Loki ruler 加载的规则
ssh robin "curl -s -H 'X-Scope-OrgID: tenant1' \
  http://localhost:3100/loki/api/v1/rules"

# 查看 Prometheus 告警状态
ssh robin "curl -s http://localhost:9090/api/v1/alerts"

查看 Alloy 状态 链接到标题

# alloy 日志
ssh monkey "docker logs alloy-postgres --tail 20"

# 检查链路:Loki 是否有指标过来
ssh robin "curl -s -G -H 'X-Scope-OrgID: tenant1' \
  'http://localhost:3100/loki/api/v1/query' \
  --data-urlencode 'query=count_over_time({container=\"postgres17\"}[5m])'"

日常巡检 链接到标题

  • 指标告警 → 关注连接数使用率、死锁速率、事务回滚率
  • 日志告警 → 关注 ERROR/FATAL 级别日志、连接限流、OOM
  • 对比两套告警触发时间差,调整阈值使日志告警比指标告警更早触发

六、总结 链接到标题

维度 指标告警 日志告警
监控工具 Prometheus + postgres-exporter Loki + LogQL
覆盖场景 连接数/缓存/事务/复制/磁盘 FATAL/ERROR/deadlock/OOM
优势 量化、趋势、历史对比 文本匹配、根因分析
局限 只能看数值,看不清原因 容易被大量 LOG 淹没

最佳实践:两条腿走路。指标做第一道防线,日志做第二道深度分析。

完整文件清单 链接到标题

文件 位置
docker-compose.yaml monkey:/opt/postgres-exporter/
config.alloy monkey:/opt/alloy-postgres/
docker-compose.yaml monkey:/opt/alloy-postgres/
prometheus.yml robin:/opt/monitor/prometheus/
alerts.yml robin:/opt/monitor/prometheus/
postgres.yaml robin:/opt/monitor/loki-data/rules/tenant1/

本文的实现全部开源可复现,完整配置见 github.com/MarshalW/devops