用 3 台 Ubuntu 虚拟机从裸机到故障 60 秒内告警、恢复 30 秒内自动消除的完整监控体系。包含 10 个采集目标、18 条告警规则以及我在过程中踩的 8 个坑。一、为什么要做这套东西在上一份实习里我每天的工作之一是按 SOP 巡检服务器登录、敲命令、看磁盘、看进程、记表格。这套流程有两个问题发现问题靠人——巡检一天一次故障可能在两次巡检之间发生往往是用户先发现、我们后知道告警靠邮件——邮件容易被淹没也没有分级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业务节点Nginxseek3192.168.206.132C / 2G / 48G数据节点MariaDB系统Ubuntu 24.04 LTSVMware Workstation 16NAT 网络。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 chrony4.4 配置静态 IPUbuntu 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五、部署监控中心seek15.1 安装 Dockerapt-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.35.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.json5.4 docker-compose.ymlservices: 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.time15d - --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 ps5.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、负载、内存高/耗尽、磁盘、磁盘预测、inodeservice.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 855.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三层探测HTTPseek2 的 Nginx 首页TCPseek3 的 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/dashboards8 块面板在线主机、触发中告警、采集目标状态、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 seek2Nginx node_exporterapt-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 -16.2 seek3MariaDB 两个 exporterMariaDB 已经有重点是把监控账号和 exporter 装好# 1. 建监控专用账号最小权限 mysql -e CREATE USER exporterlocalhost IDENTIFIED BY Exporter123; mysql -e GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO exporterlocalhost; # 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] Usermysqld_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/targets10 个目标全绿打开 Grafana8 块面板有数据。八、故障演练最重要的一步监控不能看起来能用要能证明它在故障时真的响。演练步骤# 1. 停掉 seek3 的 MySQL systemctl stop mariadb观察结果时间现象0sMySQL 停止~15smysql_up采集到 0~60sMySQLDown从 pending 转firing推送到 Alertmanager恢复后 ~30s告警自动变 resolvedGrafana 上看到触发中告警从 0 变 1采集目标里mysql变红 DOWN。恢复systemctl start mariadb30 秒内告警自动消除——这就是告警闭环也是简历上最值钱的一句话。九、踩坑记录8 个真实的坑这部分是整篇文章最有价值的内容网上教程基本不会写。坑 1VMware 会覆盖你设置的 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:0100:50:56是 VMware 允许手工指定的网段坑 2克隆虚拟机会导致三台拿同一个 IP现象克隆出三台 Ubuntu开机后 IP 全是192.168.206.132互相抢地址。原因克隆带着相同的/etc/machine-idDHCP 用它算 client id三台在服务器看来是同一台机器。解决每台开机后重置truncate -s 0 /etc/machine-id systemd-machine-id-setup坑 3Docker 起不来报docker.socketfailed现象systemctl start docker失败日志里docker.socket也是 failed但手动跑dockerd却正常。原因之前手动跑过一次dockerd留下了/var/run/docker.socksocket 激活时端口被占。解决rm -f /var/run/docker.sock systemctl reset-failed docker.socket docker.service systemctl start docker坑 4Grafana 加载不了仪表盘容器里Permission denied现象provisioning 日志显示完成但仪表盘列表是空的。原因配置放在/root/下目录权限是drwx---rwx容器里的 grafana 用户uid 472读不到。解决chmod -R arX /root/monitor-lab docker compose restart grafana教训容器挂载的目录权限要按容器内用户来设不是按宿主机 root 来设。坑 5TCP 拨测把 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这个坑值得单独讲做监控的时候要意识到监控本身也会产生指标。坑 6MariaDB 只监听 127.0.0.1端口拨测必失败现象PortUnreachable告警 firing提示192.168.206.13:3306 连接失败但本机mysql -uroot能连。原因MariaDB 默认bind-address 127.0.0.1只监听回环外部探测当然失败。解决改成0.0.0.0生产环境再用防火墙限制来源。坑 7apt 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 roothost echo $b64 | base64 -d /etc/docker/daemon.json这个技巧在写自动化脚本时非常有用能绕开所有引号地狱。十、复盘这套东西的价值在哪做得好的地方有闭环不是采了数据而是故障 → 告警 → 恢复全链路验证过有收敛分组 抑制 分级避免了告警风暴可重建全部配置化docker compose up -d就能还原踩过坑上面 8 个坑都是真实遇到的不是抄教程还能改进的地方单机 Prometheus 不是高可用——监控中心挂了就全瞎生产要上 Thanos 或双副本静态 targets——主机多了要换成服务发现Consul / K8s SD没有长期存储——15 天保留期历史趋势查不了告警没有升级机制——critical 应该能打电话不能只发消息如果重做一次我会先想清楚告警的消费方是谁谁在什么时间看什么告警、看到之后做什么动作。监控的价值不在于采了多少指标而在于每一条告警都有人会行动。没有动作的告警都是噪音。附一键部署清单# 1. seek1装 Docker略 # 2. 上传配置 scp -r monitor-lab root192.168.206.11:/root/ # 3. 启动 ssh root192.168.206.11 cd /root/monitor-lab docker compose up -d # 4. seek2 ssh root192.168.206.12 apt install -y nginx prometheus-node-exporter # 5. seek3 ssh root192.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{alertstatefiring})在匹配不到任何序列时返回空向量Grafana 会把空结果渲染成 No data。解决外面套一层or vector(0)让空结果回退到 0。count(ALERTS{alertstatefiring}) or vector(0)启示监控面板要能区分「没有告警」和「查询失败」。如果只看到 No data运维会以为监控本身坏了。