服务器监控体系实战:指标告警巡检全覆盖
发布日期: 2026/08/22 阅读总量: 0

一、凌晨2点的告警风暴,让我重新审视监控体系

2024年3月17日凌晨1点47分,我的手机被告警消息刷屏。打开电脑一看,Prometheus Alertmanager 弹出了42条告警,全部来自同一个节点:disk_usage > 80%。但奇怪的是,这台服务器的磁盘明明是300G,用了才120G。

排查了40分钟才发现根源:一台活动目录域控服务器,每隔5分钟往 /data/logs 目录写一次系统状态日志,单文件最大1GB。而我的监控规则写的是 disk_used / disk_total > 0.8,不对,我写的是 disk_free < 50G。域控服务器磁盘一共才80G,可用空间从48G跌破50G阈值的那刻起,告警就开始刷屏。

这个事故暴露出三个问题:指标采集口径混乱、告警规则没有分级、巡检完全靠人工。今天把这套完整的服务器监控体系拆开讲:指标怎么采、告警怎么分、巡检怎么做,每段代码都能直接复制用。

二、监控方案选型:Prometheus vs Zabbix vs 自研脚本

2.1 方案对比

维度Prometheus + GrafanaZabbix 6.x自研Shell脚本
采集方式拉取模式(Pull),默认15s间隔推送模式(Push),客户端主动上报cron定时执行,纯手动
数据存储TSDB,本地存储最长15天(可配),高基数数据量大后查询慢MySQL/PostgreSQL + 分区表,历史数据保留轻松达1年无存储,执行完丢弃
告警能力Alertmanager,支持路由树(routing tree)、分组、抑制内建告警,支持触发器表达式,依赖数据库存储状态mailx/sendmail 裸发邮件
部署复杂度中等,二进制+配置文件即可,无外部依赖(除告警通知外)较高,需要单独装Server端、数据库、Web界面、Agent极低,SSH到机器写脚本
适合规模中小规模(≤500台),云原生友好中大规模(≤5000台),传统运维环境1-10台,临时应急
查询语言PromQL,功能强但学习曲线陡峭类SQL表达式,容易上手但表达力弱

最终我选了 Prometheus + Grafana + Alertmanager。原因很简单:已有的服务都是通过 exporter 暴露 /metrics 端点,Nginx 有 nginx_exporter,MySQL 有 mysqld_exporter,Prometheus 天然兼容。Zabbix 需要每台机器装 agent,还要维护单独的数据库,对只有15台服务器的团队来说太重了。

2.2 监控架构总览

# docker-compose.yml - 监控栈全家桶
# 版本:Prometheus 2.53.0 / Grafana 11.1.0 / Alertmanager 0.27.0
version: '3.8'

services:
  prometheus:
    image: prom/prometheus:v2.53.0
    container_name: prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - ./rules/:/etc/prometheus/rules/
      - prom_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=15d'
      - '--storage.tsdb.retention.size=10GB'
      - '--web.enable-lifecycle'   # 支持热加载配置
    ports:
      - '9090:9090'
    restart: unless-stopped

  alertmanager:
    image: prom/alertmanager:v0.27.0
    container_name: alertmanager
    volumes:
      - ./alertmanager.yml:/etc/alertmanager/alertmanager.yml
      - alert_data:/data
    command:
      - '--config.file=/etc/alertmanager/alertmanager.yml'
      - '--log.level=debug'        # 排查告警丢失时开debug
    ports:
      - '9093:9093'
    restart: unless-stopped

  grafana:
    image: grafana/grafana:11.1.0
    container_name: grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=your_strong_password
      - GF_USERS_ALLOW_SIGN_UP=false
      - GF_SERVER_HTTP_PORT=3000
    volumes:
      - grafana_data:/var/lib/grafana
      - ./dashboards/:/etc/grafana/provisioning/dashboards/
      - ./datasources/:/etc/grafana/provisioning/datasources/
    ports:
      - '3000:3000'
    restart: unless-stopped

  node_exporter:
    image: prom/node-exporter:v1.8.2
    container_name: node_exporter
    network_mode: host        # 用host网络才能拿到宿主机真实指标
    pid: host                 # 关键:否则看不到宿主机进程
    restart: unless-stopped

volumes:
  prom_data:
  alert_data:
  grafana_data:

注意 network_mode: hostpid: host 这两个参数。我第一次部署时用了默认 bridge 网络,node_exporter 拿到的是容器自身的 /proc 数据,CPU 和内存指标全部是错的,查了半天才发现是网络模式的问题。

三、指标采集:从 node_exporter 到 PromQL 核心指标

3.1 机器级指标采集

以 node_exporter 为主采集器,监听在 9100 端口。Prometheus 拉取配置:

# prometheus.yml - 全局配置
# Prometheus v2.53.0
global:
  scrape_interval: 15s      # 默认15s拉一次
  evaluation_interval: 15s  # 告警规则评估间隔
  external_labels:
    cluster: 'prod'

# 告警规则文件
rule_files:
  - 'rules/*.yml'

# 抓取目标
scrape_configs:
  # 机器基础指标
  - job_name: 'node'
    static_configs:
      - targets:
          - '10.0.0.11:9100'   # nginx-01
          - '10.0.0.12:9100'   # nginx-02
          - '10.0.0.21:9100'   # php-01
          - '10.0.0.22:9100'   # php-02
          - '10.0.0.31:9100'   # mysql-master
          - '10.0.0.32:9100'   # mysql-slave
          - '10.0.0.41:9100'   # redis-01
        labels:
          env: 'prod'

  # MySQL 监控
  - job_name: 'mysql'
    static_configs:
      - targets: ['10.0.0.31:9104', '10.0.0.32:9104']
        labels:
          env: 'prod'

  # Nginx 监控
  - job_name: 'nginx'
    static_configs:
      - targets: ['10.0.0.11:9113', '10.0.0.12:9113']
        labels:
          env: 'prod'

  # 采集器自身健康
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']
# 每个节点启动 node_exporter
# node_exporter v1.8.2 / 系统 Ubuntu 22.04 x86_64
wget https://github.com/prometheus/node_exporter/releases/download/v1.8.2/node_exporter-1.8.2.linux-amd64.tar.gz
tar xzf node_exporter-1.8.2.linux-amd64.tar.gz
cp node_exporter-1.8.2.linux-amd64/node_exporter /usr/local/bin/

# 创建 systemd service
cat > /etc/systemd/system/node_exporter.service <<'EOF'
[Unit]
Description=Prometheus Node Exporter
After=network.target

[Service]
Type=simple
User=nobody
Group=nogroup
ExecStart=/usr/local/bin/node_exporter \
  --web.listen-address=:9100 \
  --collector.filesystem.mount-points-exclude='^/(dev|proc|sys|run|var/lib/docker)($|/)'
Restart=always

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now node_exporter
# 验证:curl -s http://localhost:9100/metrics | grep node_cpu_seconds_total | head -5

3.2 核心指标清单

指标名类型含义告警阈值建议
node_cpu_seconds_totalCounterCPU累计时间(按模式分)使用率>85% 持续10m
node_memory_MemAvailable_bytesGauge可用内存(字节)<总内存10% 持续5m
node_filesystem_avail_bytesGauge磁盘可用空间(字节)<5GB 持续5m
node_disk_read_bytes_totalCounter磁盘累计读取字节速率突增>100MB/s
node_load1Gauge1分钟平均负载>CPU核心数*2 持续15m
node_network_receive_bytes_totalCounter网卡累计接收字节速率>80%带宽
upGauge采集目标在线状态,1=正常,0=挂==0 立即告警
node_time_secondsGauge系统时间(unix时间戳)与服务器时间差>30s

3.3 PromQL 实战:解决 42 条告警风暴

开头说的告警风暴问题,用 PromQL 的 quantile 函数配合文件系统挂载点过滤解决:

# 只看根分区和 /data 分区,排除 /proc、/sys、/run 等虚拟文件系统
100 - (
  node_filesystem_avail_bytes{
    mountpoint=~"/|/data",
    fstype!~"tmpfs|overlay|squashfs"
  } / node_filesystem_size_bytes{
    mountpoint=~"/|/data",
    fstype!~"tmpfs|overlay|squashfs"
  } * 100
) > 85

3.4 MySQL 指标采集

用 mysqld_exporter 配合专门的监控账号,不要用 root,权限最小化:

-- 在 MySQL 8.0.35 上执行
CREATE USER 'exporter'@'10.0.0.%' IDENTIFIED BY 'exporter_pass_2024' WITH MAX_USER_CONNECTIONS 3;
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'10.0.0.%';
FLUSH PRIVILEGES;
# 启动 mysqld_exporter v0.15.1
cat > /etc/systemd/system/mysqld_exporter.service <<'EOF'
[Unit]
Description=Prometheus MySQL Exporter
After=network.target

[Service]
Type=simple
User=nobody
Environment="DATA_SOURCE_NAME=exporter:exporter_pass_2024@tcp(127.0.0.1:3306)/"
ExecStart=/usr/local/bin/mysqld_exporter \
  --config.my-cnf=/etc/mysqld_exporter/.my.cnf \
  --collect.auto_increment.columns \
  --collect.binlog_size \
  --collect.global_status \
  --collect.global_variables \
  --collect.info_schema.processlist \
  --collect.info_schema.tablestats \
  --collect.mysql.user \
  --collect.slave_status
Restart=always
EOF

四、告警体系:Alertmanager 分级 + 路由树

4.1 告警级别分级标准

级别定义通知方式响应时间示例
P0(灾难)核心业务不可用、数据丢失风险电话+短信+邮件+企业微信立即MySQL主库宕机、API成功率<95%
P1(严重)功能受损但可降级运行短信+邮件+企业微信15分钟磁盘即将写满、Redis可用内存<10%
P2(警告)资源接近瓶颈、有恶化风险邮件+企业微信2小时CPU>80%、Nginx 5xx率>1%
P3(提示)服务异常重启、指标抖动仅企业微信当天Nginx worker 重启次数>5

4.2 告警规则文件(核心代码)

# rules/host.yml - 机器级告警规则
# Prometheus rule v2
groups:
  - name: host_basic_alerts
    rules:
      # 主机存活检测 - P0
      - alert: HostDown
        expr: up == 0
        for: 1m
        labels:
          severity: P0
        annotations:
          summary: '{{ $labels.instance }} 已宕机'
          description: '实例 {{ $labels.instance }} 超过1分钟无法采集'

      # CPU 使用率过高 - P2
      - alert: HighCPUUsage
        expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
        for: 10m
        labels:
          severity: P2
        annotations:
          summary: '{{ $labels.instance }} CPU 使用率过高'
          description: 'CPU使用率持续超过85%已超过10分钟,当前值:{{ $value | humanizePercentage }}'

      # 内存不足 - P1
      - alert: LowMemory
        expr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100 < 10
        for: 5m
        labels:
          severity: P1
        annotations:
          summary: '{{ $labels.instance }} 内存不足'
          description: '可用内存低于10%,当前剩余:{{ $value | humanize1024 }}B'

      # 磁盘空间不足 - P1
      - alert: DiskSpaceLow
        expr: (node_filesystem_avail_bytes{mountpoint=~"/|/data"} / node_filesystem_size_bytes{mountpoint=~"/|/data"} * 100) < 10
        for: 5m
        labels:
          severity: P1
        annotations:
          summary: '{{ $labels.instance }} 磁盘空间不足'
          description: '{{ $labels.mountpoint }} 可用空间低于10%,当前剩余:{{ $value | humanize1024 }}B'

      # 磁盘IO瓶颈 - P2
      - alert: DiskIOHigh
        expr: rate(node_disk_read_bytes_total[5m]) + rate(node_disk_write_bytes_total[5m]) > 100 * 1024 * 1024
        for: 10m
        labels:
          severity: P2
        annotations:
          summary: '{{ $labels.instance }} 磁盘IO过高'
          description: '磁盘读写速率超过100MB/s,当前值:{{ $value | humanize1024 }}B/s'

      # 负载过高 - P2
      - alert: LoadHigh
        expr: node_load1 > (count by(instance) (node_cpu_seconds_total{mode="user"})) * 2
        for: 15m
        labels:
          severity: P2
        annotations:
          summary: '{{ $labels.instance }} 负载过高'
          description: '1分钟负载为 {{ $value }},已超过CPU核心数的2倍'

      # 系统时间偏差 - P2
      - alert: TimeSkew
        expr: abs(node_time_seconds - time()) > 30
        for: 1m
        labels:
          severity: P2
        annotations:
          summary: '{{ $labels.instance }} 系统时间偏差过大'
          description: '与真实时间偏差超过30秒'
# rules/mysql.yml - MySQL 告警规则
groups:
  - name: mysql_alerts
    rules:
      - alert: MySQLDown
        expr: mysql_up == 0
        for: 1m
        labels:
          severity: P0
        annotations:
          summary: 'MySQL {{ $labels.instance }} 宕机'

      - alert: MySQLReplicationLag
        expr: mysql_slave_status_seconds_behind_master > 30
        for: 3m
        labels:
          severity: P1
        annotations:
          summary: 'MySQL 主从延迟过大'
          description: '{{ $labels.instance }} 从库延迟已超过30秒,当前值 {{ $value }}s'

      - alert: MySQLTooManyConnections
        expr: mysql_global_status_threads_connected / mysql_global_variables_max_connections * 100 > 80
        for: 5m
        labels:
          severity: P1
        annotations:
          summary: 'MySQL 连接数过多'
          description: '当前连接数已达到最大连接数的80%以上'

4.3 Alertmanager 路由配置

# alertmanager.yml - v0.27.0
global:
  smtp_smarthost: 'smtp.exmail.qq.com:465'
  smtp_from: 'monitor@yourcompany.com'
  smtp_auth_username: 'monitor@yourcompany.com'
  smtp_auth_password: 'your_smtp_password'
  smtp_require_tls: false
  resolve_timeout: 5m      # 告警自动恢复后,resolve通知的确认周期

# 路由树
route:
  receiver: 'default'
  group_by: ['alertname', 'instance']  # 相同告警名+实例合并
  group_wait: 30s          # 同一组告警首次通知等待时间,攒批
  group_interval: 5m       # 同一组之间下一次通知间隔
  repeat_interval: 4h      # 相同告警重复通知间隔
  routes:
    # P0:立即电话+短信
    - matchers:
        - severity = P0
      receiver: 'page-oncall'
      group_wait: 0s
      repeat_interval: 30m

    # P1:短信+邮件
    - matchers:
        - severity = P1
      receiver: 'sms-email'
      group_wait: 10s

    # P2/P3:仅企业微信
    - matchers:
        - severity =~ "P2|P3"
      receiver: 'wechat'
      group_wait: 1m

# 接收人定义
receivers:
  - name: 'default'
    email_configs:
      - to: 'devops@yourcompany.com'

  - name: 'page-oncall'
    email_configs:
      - to: 'oncall@yourcompany.com'
    webhook_configs:
      - url: 'http://10.0.0.50:8080/dingtalk/notify'
        send_resolved: true

  - name: 'sms-email'
    email_configs:
      - to: 'devops@yourcompany.com'
    webhook_configs:
      - url: 'http://10.0.0.50:8080/wechat/sms'

  - name: 'wechat'
    webhook_configs:
      - url: 'http://10.0.0.50:8080/wechat/notify'
        send_resolved: true

五、自动化巡检:每天凌晨3点,脚本替你做检查

告警解决的是「已经出事」的问题,巡检解决的是「快出事」的问题。我写了一个巡检脚本,每天凌晨3点跑一遍,输出 HTML 报告发到运维群。覆盖以下检查项:

  • 磁盘 Inode 使用率(df -i)——经常被忽略,inode 耗尽比空间耗尽更隐蔽
  • MySQL 慢查询数量(慢查询日志 + performance_schema)
  • Nginx 错误日志最近1小时 ERROR 数量
  • SSL 证书剩余有效期(<30天就警告)
  • 系统关键服务状态(nginx、php-fpm、mysqld、redis)
  • 内核日志中的 OOM 记录(dmesg | grep -i oom)
  • 昨天凌晨2点~4点的日志备份是否成功
#!/bin/bash
# inspect.sh - 服务器日常巡检脚本
# 使用方法:bash inspect.sh > report.html
# 版本:v2.3 / 运行环境:Ubuntu 22.04 / 适用于所有CVM/物理机

set -euo pipefail

HOSTNAME=$(hostname)
DATE=$(date '+%Y-%m-%d %H:%M:%S')
FAILED_ITEMS=()
WARN_ITEMS=()

# ---------- 函数定义 ----------
check() {
  local desc="$1"
  local status="$2"   # PASS / WARN / FAIL
  local detail="$3"
  if [ "$status" == "FAIL" ]; then
    FAILED_ITEMS+=("$desc")
  elif [ "$status" == "WARN" ]; then
    WARN_ITEMS+=("$desc")
  fi
  echo "$desc$status$detail"
}

# ---------- 1. 磁盘与inode检查 ----------
disk_check() {
  # 检查根分区和/data分区,阈值80%
  df -hP / /data 2>/dev/null | tail -n +2 | while read fs size used avail use_pct mount; do
    pct_num=$(echo "$use_pct" | tr -d '%')
    if [ "$pct_num" -gt 80 ]; then
      check "磁盘使用率 $mount" FAIL "已用 $pct_num% - $used/$size"
    else
      check "磁盘使用率 $mount" PASS "已用 $pct_num%"
    fi
  done

  # inode使用率
  df -iP / /data 2>/dev/null | tail -n +2 | while read fs inodes used avail use_pct mount; do
    pct_num=$(echo "$use_pct" | tr -d '%')
    if [ "$pct_num" -gt 80 ]; then
      check "Inode使用率 $mount" FAIL "已用 $pct_num% - $used/$inodes"
    else
      check "Inode使用率 $mount" PASS "已用 $pct_num%"
    fi
  done
}

# ---------- 2. MySQL慢查询统计 ----------
mysql_check() {
  # 需要提前在 ~/.my.cnf 配置好凭据
  if command -v mysql &>/dev/null; then
    slow_count=$(mysql -N -e "SHOW GLOBAL STATUS LIKE 'Slow_queries'" 2>/dev/null | awk '{print $2}')
    # 最近1小时慢查询数
    recent_slow=$(mysql -N -e "
      SELECT COUNT(*) FROM performance_schema.events_statements_summary_by_digest
      WHERE LAST_SEEN > NOW() - INTERVAL 1 HOUR
      AND SUM_TIMER_WAIT > 1000000000" 2>/dev/null || echo "N/A")
    check "MySQL慢查询(累计)" PASS "总计 $slow_count / 最近1h新增 $recent_slow"
  else
    check "MySQL客户端" WARN "未安装mysql客户端,跳过慢查询检查"
  fi
}

# ---------- 3. Nginx错误日志 ----------
nginx_check() {
  NGINX_ERROR_LOG="/var/log/nginx/error.log"
  if [ -f "$NGINX_ERROR_LOG" ]; then
    err_count=$(tail -n 2000 "$NGINX_ERROR_LOG" | awk -v date="$(date '+%Y/%m/%d %H:%M' -d '1 hour ago')" '$0 >= date' | grep -c "\[error\]" || true)
    if [ "$err_count" -gt 10 ]; then
      check "Nginx错误日志" WARN "最近1小时 $err_count 条error"
    else
      check "Nginx错误日志" PASS "最近1小时 $err_count 条error"
    fi
  else
    check "Nginx错误日志" WARN "日志文件不存在"
  fi
}

# ---------- 4. SSL证书有效期 ----------
ssl_check() {
  # 获取所有Nginx配置里的证书路径(谨慎:需要sed支持)
  for cert in $(grep -rhoP 'ssl_certificate\s+\K[^;]+' /etc/nginx/ 2>/dev/null | sort -u); do
    if [ -f "$cert" ]; then
      days_left=$(openssl x509 -enddate -noout -in "$cert" 2>/dev/null | cut -d= -f2 | date -f - +%s | awk -v now=$(date +%s) '{print int(($1-now)/86400)}')
      if [ "$days_left" -lt 30 ]; then
        check "SSL证书(30天内)" FAIL "$cert 剩余 $days_left 天"
      else
        check "SSL证书" PASS "$cert 剩余 $days_left 天"
      fi
    fi
  done
}

# ---------- 5. 系统关键服务 ----------
service_check() {
  for svc in nginx php8.3-fpm mysqld redis-server; do
    if systemctl is-active --quiet "$svc" 2>/dev/null; then
      check "服务 $svc" PASS "运行中"
    else
      check "服务 $svc" FAIL "未运行"
    fi
  done
}

# ---------- 6. OOM检查 ----------
oom_check() {
  recent_oom=$(dmesg -T 2>/dev/null | grep -i "out of memory" | tail -n 5 || true)
  if [ -n "$recent_oom" ]; then
    check "内核OOM事件" WARN "最近发生OOM:$(echo "$recent_oom" | tail -1)"
  else
    check "内核OOM事件" PASS "无OOM记录"
  fi
}

# ---------- 7. 备份结果验证 ----------
backup_check() {
  BACKUP_DIR="/data/backup/mysql"
  if [ -d "$BACKUP_DIR" ]; then
    latest_backup=$(ls -t "$BACKUP_DIR"/*.sql.gz 2>/dev/null | head -1)
    if [ -n "$latest_backup" ]; then
      backup_time=$(stat -c '%y' "$latest_backup" | cut -d'.' -f1)
      backup_hour=$(date -d "$backup_time" +%H)
      if [ "$backup_hour" -ge 2 ] && [ "$backup_hour" -le 5 ]; then
        check "MySQL备份" PASS "最新备份:$latest_backup ($backup_time)"
      else
        check "MySQL备份" WARN "备份生成时间异常:$backup_time(应在凌晨2-5点)"
      fi
    else
      check "MySQL备份" FAIL "备份目录为空"
    fi
  else
    check "MySQL备份" WARN "备份目录不存在 $BACKUP_DIR"
  fi
}

# ---------- 生成HTML报告 ----------
cat <






服务器巡检报告

主机:$HOSTNAME

时间:$DATE

结果:${#FAILED_ITEMS[@]} 项失败,${#WARN_ITEMS[@]} 项警告

EOF disk_check mysql_check nginx_check ssl_check service_check oom_check backup_check cat < EOF # 有FAIL项则退出码为1,方便cron通知 if [ ${#FAILED_ITEMS[@]} -gt 0 ]; then exit 1 fi

cron 配置:

# /etc/cron.d/inspect
# 每天凌晨3点执行巡检,输出报告到 /var/www/html/inspect/ 目录
# 配合 nginx 搭建简单web访问,或者直接邮件发出去
0 3 * * * root bash /opt/scripts/inspect.sh > /var/www/html/inspect/report_$(date +\%Y\%m\%d).html 2>&1 || true

六、效果数据:这套体系上线后发生了什么

上线这套监控体系前,团队的情况是:

  • 平均每周被业务方投诉2次「系统慢了」,排查方向靠猜,通常花1小时定位到具体机器
  • 有一次 MySQL 主库磁盘满导致写入失败,业务中断45分钟才被发现
  • 告警集群在凌晨刷屏,导致真正重要的告警被忽略

上线后(2024年4月~7月),统计到的数据:

检查项状态详情
指标上线前(1-3月)上线后(4-7月)变化
生产故障平均发现时间约37分钟约4分钟缩短89%
月度P0/P1安全事故平均1.7次0.5次下降70%
磁盘写满导致的中断2次(分别中断45min/26min)0次提前预警,扩容解决
巡检人工耗时每周约3小时(手动SSH逐台检查)0分钟(全自动)100%机器人
告警噪音(每天平均条数)120+条17条降低86%

举一个具体的例子:2024年6月18日,巡检脚本在凌晨3:02检测到 php-02 服务器的 /data 分区 inode 使用率到了 91%(阈值80%)。原因是 PHP 的 session 文件没清理,session.gc_maxlifetime 设置的是 2小时,但是 cron 清理任务挂了。巡检脚本在凌晨3点发现,4点运维通过清理旧的 session 临时文件解决了,全程没有影响业务。如果没有巡检,按之前的经验,inode 100%的时候 PHP 会报 No space left on device,用户前端直接白屏。

监控本身的资源开销:Prometheus 容器平均内存 780MB,node_exporter 每个节点约 35MB,mysqld_exporter 每个约 25MB。15台服务器,拉取间隔15s的情况下,Prometheus 本地存储每天约 800MB,设置15天保留期后稳定在 12GB 左右。对生产服务器的影响基本可忽略(CPU占用 <1%)。

七、避坑指南(每一坑我都踩过)

坑1:node_exporter 容器网络模式没设 host

现象:容器部署 node_exporter 后,CPU Idle 始终是99%以上,所有指标都「正常」得离谱。
原因:容器用 bridge 网络,/proc 挂载的是容器自身的,不是宿主机。
解决:network_mode: host + pid: host 缺一不可。

坑2:告警 for 参数没有调好,导致误报抖动

现象:for: 1m 太短,Nginx 在发布重启的瞬间 CPU 飙到90%,触发了 P2 告警,5分钟后恢复正常,但告警已经发出,值班同事被吓醒。
解决:for 设置给足「抖动」容忍度。CPU 用 for: 10m,内存用 for: 5m,只有 HostDown 这种硬故障才用 for: 1m。关键数据点:我们在测试环境模拟了3次发布,每次 CPU 峰值 88%~94% 持续 3~8分钟,所以10分钟是安全阈值。

坑3:Alertmanager 没有配置 group_interval,告警轰炸

现象:一台数据库挂了,相关联的所有进程连不上,告警从不同规则里出来,一共42条。手机直接弹窗弹到卡死。
原因:每一条告警独立发出,没有分组。
解决:路由树里设置 group_by: ['alertname'](按告警类型分组,把同一个类型合并)或 group_by: ['alertname', 'instance'](按类型+实例分组)。group_wait: 30s 让30秒内的告警攒一批。

坑4:磁盘告警用了错误的匹配表达式

现象:开头提到的域控事件,42条告警刷屏。直接原因就是 node_filesystem_avail_bytes < 50GB 这个写法,没有排除虚拟文件系统。
解决:用 mountpoint=~"/|/data" 过滤,配合 fstype!~"tmpfs|overlay|squashfs" 排除虚拟文件系统,同时改成百分比形式。

坑5:巡检脚本有失败点,但 cron 不会告诉你

现象:巡检脚本输出重定向到 report_$(date +%Y%m%d).html,但没有加 || true。有一天脚本里的 mysql_check() 因为 .my.cnf 权限变更失败了,整个脚本以非0退出,cron 把错误邮件发给 root 邮箱,压根没看。
解决:脚本结尾判断 FAILED_ITEMS 数量,有失败就显式 exit 1;cron 行加 || true 避免脚本错误影响自身。同时,检查脚本是否真的跑了:grep inspect /var/log/syslog 看 cron 记录。

坑6:Prometheus 配置文件热加载失效

现象:改了 rules/*.yml 后,用 curl -XPOST localhost:9090/-/reload 触发加载,没有生效。
原因:docker-compose 启动时没有加 --web.enable-lifecycle 参数。
解决:在 command 里加上这个 flag,同时确认容器内的文件路径与挂载一致。否则就只能 restart Prometheus,但会丢失未持久化的告警状态。

坑7:Grafana 告警和 Alertmanager 混用

最后一条,也很关键。如果你同时配了 Grafana 的告警规则和 Alertmanager 告警,同一指标会触发双份告警,值班同事会收到「同一个问题来自两个系统」的消息。解决办法:统一走 Alertmanager,Grafana 只做展示。把 Grafana 的告警全部禁用,用 provisioning 的方式定义数据源和面板,不要手动在 Grafana 里点来点去,避免环境不一致。

八、总结:整套体系长什么样

整套监控体系部署在一台 4核8GB 的 CVM 上(Docker Compose 方式),它负责监控15台生产服务器。每天处理约126万条指标样本,数据保留15天,占用存储12GB。

核心就是一个循环:采集(exporter → Prometheus)→ 评估(rules)→ 告警(Alertmanager → 通知渠道)→ 巡检(cron脚本 → 日报)→ 反向优化告警规则。每一条告警,都是这个循环的产物。告警少了,说明阈值太宽松;告警多了,说明规则需要调整。

这套方案不是最优的,但它是可以直接落地的。拿走就能用,坑我已经替你踩过了。