一、凌晨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 + Grafana | Zabbix 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: host 和 pid: 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_total | Counter | CPU累计时间(按模式分) | 使用率>85% 持续10m |
| node_memory_MemAvailable_bytes | Gauge | 可用内存(字节) | <总内存10% 持续5m |
| node_filesystem_avail_bytes | Gauge | 磁盘可用空间(字节) | <5GB 持续5m |
| node_disk_read_bytes_total | Counter | 磁盘累计读取字节 | 速率突增>100MB/s |
| node_load1 | Gauge | 1分钟平均负载 | >CPU核心数*2 持续15m |
| node_network_receive_bytes_total | Counter | 网卡累计接收字节 | 速率>80%带宽 |
| up | Gauge | 采集目标在线状态,1=正常,0=挂 | ==0 立即告警 |
| node_time_seconds | Gauge | 系统时间(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脚本 → 日报)→ 反向优化告警规则。每一条告警,都是这个循环的产物。告警少了,说明阈值太宽松;告警多了,说明规则需要调整。
这套方案不是最优的,但它是可以直接落地的。拿走就能用,坑我已经替你踩过了。