ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从零搭建三节点监控告警平台:Prometheus + Grafana + Alertmanager 实战

从零搭建三节点监控告警平台:Prometheus + Grafana + Alertmanager 实战
用 3 台 Ubuntu 虚拟机,从裸机到"故障 60 秒内告警、恢复 30 秒内自动消除"的完整监控体系。
包含 10 个采集目标、18 条告警规则,以及我在过程中踩的 8 个坑。

一、为什么要做这套东西

在上一份实习里,我每天的工作之一是按 SOP 巡检服务器:登录、敲命令、看磁盘、看进程、记表格。

这套流程有两个问题:

  1. 发现问题靠人——巡检一天一次,故障可能在两次巡检之间发生,往往是用户先发现、我们后知道
  2. 告警靠邮件——邮件容易被淹没,也没有分级,CPU 高和数据库挂了是同一条消息

所以我想搭一套东西,把"人找问题"变成"问题找人"。目标很明确:

  • 三台主机、全部关键服务,指标统一采集
  • 故障能在 1 分钟内推到我面前
  • 恢复后自动消除,不需要人手动关告警
  • 告警不能刷屏——这是最容易被忽略的一点

二、最终效果

指标结果
被监控主机3 台
采集目标10 个(全部 UP)
告警规则18 条(主机 8 / 服务 8 / 监控自身 2)
采集间隔15 秒
故障发现MySQL 停止后60 秒内触发 critical 告警
故障恢复恢复后30 秒内告警自动消除
大屏8 块面板的统一总览

三、架构设计

┌──────────────────────────────────────┐ │ seek1 192.168.206.11 监控中心 │ │ ┌────────────┐ ┌───────────────┐ │ │ │ Prometheus │→ │ Alertmanager │ │ │ │ :9090 │ │ :9093 │ │ │ └─────┬──────┘ └───────┬───────┘ │ │ │ │ webhook │ │ ┌─────┴──────┐ ┌───────▼───────┐ │ │ │ Grafana │ │ alert-relay │───┼──→ 飞书 │ │ :3000 │ │ :5001 │ │ │ └────────────┘ └───────────────┘ │ │ ┌──────────────────────────────┐ │ │ │ blackbox-exporter :9115 │ │ │ └──────────────────────────────┘ │ └───────┬──────────────────┬───────────┘ 抓取 9100/9104│ │抓取 9100 ▼ ▼ ┌───────────────────┐ ┌────────────────────┐ │ seek2 .12 Nginx │ │ seek3 .13 MariaDB│ │ node_exporter │ │ node + mysqld_exp │ └───────────────────┘ └────────────────────┘

为什么这么分:

  • 监控中心独立成一台:监控和被监控的东西不能死在同一个地方
  • 被监控节点一台上跑 Web、一台上跑数据库:这样"业务指标"和"数据指标"都能覆盖
  • 全部走 Docker:环境可重建,换机器 20 分钟能复原

四、环境准备

4.1 三台虚拟机

主机IP规格角色
seek1192.168.206.112C / 2G / 48G监控中心
seek2192.168.206.122C / 2G / 48G业务节点(Nginx)
seek3192.168.206.132C / 2G / 48G数据节点(MariaDB)

系统:Ubuntu 24.04 LTS,VMware Workstation 16,NAT 网络。

4.2 网络规划

我用的是 VMware 的 NAT 网段192.168.206.0/24,网关192.168.206.2。

关键点:静态 IP 要避开 DHCP 池(.128-.254),所以选了.11/.12/.13。

4.3 系统初始化(三台都做)

# 时区(默认 UTC,不改的话日志和告警时间会差 8 小时) timedatectl set-timezone Asia/Shanghai # 主机名 hostnamectl set-hostname seek1 # 时间同步 apt install -y chrony && systemctl enable --now chrony

4.4 配置静态 IP

Ubuntu 24.04 用 netplan,但要注意cloud-init 会覆盖网络配置,先禁用它:

echo 'network: {config: disabled}' > /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg cat > /etc/netplan/99-static.yaml <<'EOF' network: version: 2 ethernets: ens33: dhcp4: false addresses: [192.168.206.11/24] routes: - to: default via: 192.168.206.2 nameservers: addresses: [223.5.5.5, 119.29.29.29] EOF chmod 600 /etc/netplan/99-static.yaml netplan apply

五、部署监控中心(seek1)

5.1 安装 Docker

apt-get update apt-get install -y docker.io docker-compose-v2 systemctl enable --now docker docker --version # Docker version 29.1.3 docker compose version # Docker Compose version 2.40.3

5.2 配置镜像加速(国内必做)

默认的 Docker Hub 在国内基本连不上,会报:

dial tcp 31.13.75.5:443: connect: connection refused

配置镜像加速器:

mkdir -p /etc/docker cat > /etc/docker/daemon.json <<'EOF' { "registry-mirrors": ["https://docker.m.daocloud.io"] } EOF systemctl restart docker

验证:

docker pull hello-world # 能拉下来就说明配好了

5.3 目录结构

/root/monitor-lab/ ├── docker-compose.yml ├── prometheus/ │ ├── prometheus.yml │ └── rules/ │ ├── host.yml # 主机类 8 条 │ ├── service.yml # 服务类 8 条 │ └── self.yml # 自身 2 条 ├── alertmanager/alertmanager.yml ├── blackbox/blackbox.yml ├── feishu/alert_relay.py └── grafana/ ├── provisioning/ │ ├── datasources/prometheus.yml │ └── dashboards/provider.yml └── dashboards/overview.json

5.4 docker-compose.yml

services: prometheus: image: prom/prometheus:v2.53.0 restart: unless-stopped volumes: - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro - ./prometheus/rules:/etc/prometheus/rules:ro - prom_data:/prometheus command: - --config.file=/etc/prometheus/prometheus.yml - --storage.tsdb.retention.time=15d - --web.enable-lifecycle # 支持热加载 ports: ["9090:9090"] alertmanager: image: prom/alertmanager:v0.27.0 restart: unless-stopped volumes: - ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro ports: ["9093:9093"] grafana: image: grafana/grafana:11.1.0 restart: unless-stopped environment: GF_SECURITY_ADMIN_PASSWORD: admin123 GF_USERS_ALLOW_SIGN_UP: "false" volumes: - ./grafana/provisioning:/etc/grafana/provisioning:ro - ./grafana/dashboards:/var/lib/grafana/dashboards:ro ports: ["3000:3000"] blackbox: image: prom/blackbox-exporter:v0.25.0 restart: unless-stopped volumes: - ./blackbox/blackbox.yml:/etc/blackbox_exporter/config.yml:ro ports: ["9115:9115"] alert-relay: image: python:3.11-alpine restart: unless-stopped volumes: - ./feishu:/app:ro environment: FEISHU_WEBHOOK: ${FEISHU_WEBHOOK:-} command: python /app/alert_relay.py ports: ["5001:5001"] volumes: prom_data:

启动:

docker compose up -d docker compose ps

5.5 Prometheus 配置

global: scrape_interval: 15s evaluation_interval: 15s rule_files: - /etc/prometheus/rules/*.yml alerting: alertmanagers: - static_configs: - targets: ["alertmanager:9093"] scrape_configs: - job_name: prometheus static_configs: - targets: ["localhost:9090"] - job_name: node static_configs: - targets: ["192.168.206.11:9100"] labels: { role: monitor } - targets: ["192.168.206.12:9100"] labels: { role: web } - targets: ["192.168.206.13:9100"] labels: { role: db } - job_name: mysql static_configs: - targets: ["192.168.206.13:9104"] # 拨测:HTTP - job_name: blackbox-http metrics_path: /probe params: { module: [http_2xx] } static_configs: - targets: ["http://192.168.206.12/"] relabel_configs: - source_labels: [__address__] target_label: __param_target - source_labels: [__param_target] target_label: instance - target_label: __address__ replacement: blackbox:9115
拨测的 relabel 是固定套路:把目标地址塞进__param_target,再把真正的抓取地址改成 blackbox exporter。

5.6 告警规则(18 条)

分三个文件,按故障域拆分:

文件条数覆盖
host.yml8采集失联、CPU、负载、内存(高/耗尽)、磁盘、磁盘预测、inode
service.yml8目标掉线、HTTP 拨测、TCP 端口、证书到期、MySQL 宕机/连接数/慢查询/连接失败
self.yml2Prometheus 配置热加载失败、Alertmanager 失联

两个我比较满意的规则:

① 预测型磁盘告警(不是"已满"才报,而是"4 小时后会满")

- alert: DiskWillFillIn4Hours expr: predict_linear(node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"}[1h], 4 * 3600) < 0 for: 10m labels: { severity: critical }

② inode 告警(磁盘还有空间但写不进文件,很多人漏掉这个)

- alert: InodeUsageHigh expr: | (1 - node_filesystem_files_free{fstype!~"tmpfs|overlay"} / node_filesystem_files{fstype!~"tmpfs|overlay"}) * 100 > 85

5.7 Alertmanager:分组、抑制、静默

route: receiver: feishu group_by: ["alertname", "instance"] group_wait: 30s # 新告警先攒 30 秒 group_interval: 5m repeat_interval: 4h routes: - matchers: [severity = critical] receiver: feishu group_wait: 10s repeat_interval: 1h receivers: - name: feishu webhook_configs: - url: "http://alert-relay:5001/webhook" send_resolved: true # 抑制:主机失联时,别再刷它上面的一堆资源告警 inhibit_rules: - source_matchers: [alertname = NodeExporterDown] target_matchers: [alertname =~ "Host.*|Disk.*|Inode.*"] equal: ["instance"] - source_matchers: [alertname = MySQLDown] target_matchers: [alertname =~ "MySQL.*"] equal: ["instance"]

抑制是这套系统里最值钱的一条配置——一台机器挂了,如果你不做抑制,30 秒内会收到 8 条告警,人就会开始忽略告警。

5.8 Blackbox 拨测

modules: http_2xx: prober: http timeout: 5s http: preferred_ip_protocol: ip4 valid_status_codes: [200, 201, 204, 301, 302, 401, 403] tcp_connect: prober: tcp timeout: 5s

三层探测:

  • HTTP:seek2 的 Nginx 首页
  • TCP:seek3 的 3306 端口
  • HTTPS:外部站点(顺便监控证书到期)

5.9 Grafana 自动配置

不手工点,全部用 provisioning 文件:

# provisioning/datasources/prometheus.yml apiVersion: 1 datasources: - name: Prometheus type: prometheus uid: prometheus # 固定 uid,大盘里引用它 url: http://prometheus:9090 isDefault: true
# provisioning/dashboards/provider.yml apiVersion: 1 providers: - name: "lab-dashboards" folder: "运维监控" type: file options: path: /var/lib/grafana/dashboards

8 块面板:在线主机、触发中告警、采集目标状态、CPU、内存、磁盘、拨测结果、MySQL 连接数。

5.10 飞书告警中继

Alertmanager 的 webhook 格式飞书不认,需要一个转换层。只用标准库,一个文件:

def build_card(payload): alerts = payload.get("alerts", []) status = payload.get("status", "firing") lines = [] for a in alerts[:8]: lab = a.get("labels", {}) ann = a.get("annotations", {}) icon = {"critical": "🔴", "warning": "🟠"}.get(lab.get("severity")) lines.append(f"{icon} **{lab.get('alertname')}**\n 摘要:{ann.get('summary')}") return { "msg_type": "interactive", "card": { "header": {"title": {"tag": "plain_text", "content": f"告警:{status}"}}, "elements": [{"tag": "div", "text": {"tag": "lark_md", "content": "\n\n".join(lines)}}], }, }

没有飞书机器人也能用——FEISHU_WEBHOOK留空时把卡片打到容器日志,一样能验证链路。


六、部署被监控节点

6.1 seek2:Nginx + node_exporter

apt-get install -y nginx prometheus-node-exporter systemctl enable --now nginx prometheus-node-exporter echo 'web-01 on seek2' > /var/www/html/index.html curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1/ # 200 curl -s http://127.0.0.1:9100/metrics | head -1

6.2 seek3:MariaDB + 两个 exporter

MariaDB 已经有,重点是把监控账号和 exporter 装好:

# 1. 建监控专用账号(最小权限) mysql -e "CREATE USER 'exporter'@'localhost' IDENTIFIED BY 'Exporter@123';" mysql -e "GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'localhost';" # 2. 允许远程探测(默认只监听 127.0.0.1,拨测会失败) cat > /etc/mysql/mariadb.conf.d/99-bind.cnf <<'EOF' [mysqld] bind-address = 0.0.0.0 EOF systemctl restart mariadb # 3. 装 mysqld_exporter curl -fsSL -o me.tgz https://github.com/prometheus/mysqld_exporter/releases/download/v0.15.1/mysqld_exporter-0.15.1.linux-amd64.tar.gz tar xzf me.tgz install -m755 mysqld_exporter-0.15.1.linux-amd64/mysqld_exporter /usr/local/bin/

systemd 服务:

[Service] User=mysqld_exporter ExecStart=/usr/local/bin/mysqld_exporter \ --config.my-cnf=/etc/mysqld_exporter.cnf \ --web.listen-address=:9104

七、验证

# 全部目标是否 UP curl -s http://localhost:9090/api/v1/targets | grep -o '"health":"[a-z]*"' | sort | uniq -c # 10 up # 规则是否加载 curl -s http://localhost:9090/api/v1/rules | grep -c alertname

打开http://192.168.206.11:9090/targets,10 个目标全绿;打开 Grafana,8 块面板有数据。


八、故障演练(最重要的一步)

监控不能"看起来能用",要能证明它在故障时真的响。

演练步骤:

# 1. 停掉 seek3 的 MySQL systemctl stop mariadb

观察结果:

时间现象
0sMySQL 停止
~15smysql_up采集到 0
~60sMySQLDown从 pending 转firing,推送到 Alertmanager
恢复后 ~30s告警自动变 resolved

Grafana 上看到:触发中告警从 0 变 1,采集目标里mysql变红 DOWN。

恢复:

systemctl start mariadb

30 秒内告警自动消除——这就是"告警闭环",也是简历上最值钱的一句话。


九、踩坑记录(8 个真实的坑)

这部分是整篇文章最有价值的内容,网上教程基本不会写。

坑 1:VMware 会覆盖你设置的 MAC 地址

现象:.vmx里写死ethernet0.generatedAddress = "00:0c:29:11:20:01",开机后变成了00:0c:29:c7:82:b2。

原因:addressType = "generated"时,VMware 会重新生成,忽略你的值。

解决:改成静态:

ethernet0.addressType = "static" ethernet0.address = "00:50:56:3f:11:01"

(00:50:56是 VMware 允许手工指定的网段)

坑 2:克隆虚拟机会导致三台拿同一个 IP

现象:克隆出三台 Ubuntu,开机后 IP 全是192.168.206.132,互相抢地址。

原因:克隆带着相同的/etc/machine-id,DHCP 用它算 client id,三台在服务器看来是同一台机器。

解决:每台开机后重置:

truncate -s 0 /etc/machine-id systemd-machine-id-setup

坑 3:Docker 起不来,报docker.socketfailed

现象:systemctl start docker失败,日志里docker.socket也是 failed,但手动跑dockerd却正常。

原因:之前手动跑过一次dockerd,留下了/var/run/docker.sock,socket 激活时端口被占。

解决:

rm -f /var/run/docker.sock systemctl reset-failed docker.socket docker.service systemctl start docker

坑 4:Grafana 加载不了仪表盘,容器里Permission denied

现象:provisioning 日志显示"完成",但仪表盘列表是空的。

原因:配置放在/root/下,目录权限是drwx---rwx,容器里的 grafana 用户(uid 472)读不到。

解决:

chmod -R a+rX /root/monitor-lab docker compose restart grafana

教训:容器挂载的目录,权限要按容器内用户来设,不是按宿主机 root 来设。

坑 5:TCP 拨测把 MySQL 的"连接失败"计数打爆

现象:一条MySQLAbortedConnects告警一直 firing,说"5 分钟内连接失败 20 次",但数据库好好的。

原因:Blackbox 每 15 秒连一次 3306 做拨测,连上就断开、不做认证,MySQL 把这种连接记为aborted_connects。监控自己成了噪音源。

解决:按基线调阈值(从 10 提到 60),并在规则里写清楚原因:

# TCP 拨测每 15s 连一次 3306 但不认证,MySQL 会记 4~5 次/分钟的基线。 # 阈值必须高于基线,否则监控自己就是噪音源。 - alert: MySQLAbortedConnects expr: increase(mysql_global_status_aborted_connects[5m]) > 60

这个坑值得单独讲:做监控的时候要意识到"监控本身也会产生指标"。

坑 6:MariaDB 只监听 127.0.0.1,端口拨测必失败

现象:PortUnreachable告警 firing,提示192.168.206.13:3306 连接失败,但本机mysql -uroot能连。

原因:MariaDB 默认bind-address = 127.0.0.1,只监听回环,外部探测当然失败。

解决:改成0.0.0.0(生产环境再用防火墙限制来源)。

坑 7:apt install报 404,镜像里没有这个包

现象:

Err: prometheus-node-exporter 1.7.0-1ubuntu0.2 404 Not Found

原因:阿里云镜像的noble-updates同步不全,个别包缺失。

解决:改用 GitHub 直接下二进制(GitHub 在这台机器上可达):

curl -fsSL -o ne.tgz https://github.com/prometheus/node_exporter/releases/download/v1.8.2/node_exporter-1.8.2.linux-amd64.tar.gz

更快的兜底:从已经装好的机器把二进制scp过来,比重新下载稳。

坑 8:用 shell 传含引号的配置,引号会被吞掉

现象:远程执行echo '{"a":"b"}' > file,结果文件里是{a:b},JSON 解析失败,Docker 起不来。

原因:PowerShell → ssh → bash 三层转义,双引号在某一层被吃掉了。

解决:配置内容先 base64,远端解码:

$json = '{"registry-mirrors":["https://docker.m.daocloud.io"]}' $b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($json)) ssh root@host "echo $b64 | base64 -d > /etc/docker/daemon.json"

这个技巧在写自动化脚本时非常有用,能绕开所有引号地狱。


十、复盘:这套东西的价值在哪

做得好的地方

  1. 有闭环:不是"采了数据",而是"故障 → 告警 → 恢复"全链路验证过
  2. 有收敛:分组 + 抑制 + 分级,避免了告警风暴
  3. 可重建:全部配置化,docker compose up -d就能还原
  4. 踩过坑:上面 8 个坑都是真实遇到的,不是抄教程

还能改进的地方

  1. 单机 Prometheus 不是高可用——监控中心挂了就全瞎,生产要上 Thanos 或双副本
  2. 静态 targets——主机多了要换成服务发现(Consul / K8s SD)
  3. 没有长期存储——15 天保留期,历史趋势查不了
  4. 告警没有升级机制——critical 应该能打电话,不能只发消息

如果重做一次

我会先想清楚告警的消费方是谁:谁在什么时间看什么告警、看到之后做什么动作。监控的价值不在于采了多少指标,而在于每一条告警都有人会行动。没有动作的告警,都是噪音。


附:一键部署清单

# 1. seek1:装 Docker(略) # 2. 上传配置 scp -r monitor-lab root@192.168.206.11:/root/ # 3. 启动 ssh root@192.168.206.11 "cd /root/monitor-lab && docker compose up -d" # 4. seek2 ssh root@192.168.206.12 "apt install -y nginx prometheus-node-exporter" # 5. seek3 ssh root@192.168.206.13 "bash /root/setup-seek3.sh" # 6. 验证 curl -s http://192.168.206.11:9090/api/v1/targets | grep -o '"health":"[a-z]*"' | sort | uniq -c

*环境:Ubuntu 24.04 · Docker 29.1.3 · Compose 2.40.3 · Prometheus 2.53 · Grafana 11.1 · VMware Workstation 16*

附:告警面板里的 No data 问题

这个问题是在截取上面那几张图时发现的,单独记一笔。

现象:没有告警时,「触发中告警」面板显示 No data,而不是 0。

原因:count(ALERTS{alertstate="firing"})在匹配不到任何序列时返回空向量,Grafana 会把空结果渲染成 No data。

解决:外面套一层or vector(0),让空结果回退到 0。

count(ALERTS{alertstate="firing"}) or vector(0)

启示:监控面板要能区分「没有告警」和「查询失败」。如果只看到 No data,运维会以为监控本身坏了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进