
前阵子做了一件事在内部代号为 Easypack 的这套基于 Ansible 的自动化部署工具里把 Prometheus 监控能力完整接入进去。为什么要动这个原因是部署工具跑顺了之后监控就成了最刚需的下一步。服务用 Ansible 部署到几十台机器上上线之后总得有双眼睛盯着CPU、内存、磁盘、服务存活状态这些如果不主动暴露出来等用户反馈问题再去查运维就被动了。这篇文章把我这次改造的方案、目录结构、角色拆分、配置模板、踩过的坑全部梳理一遍希望能给正在做同类事情的同学提供一个可直接抄作业的参考。这个需求的核心要点很明确不是单独装一套 Prometheus而是让它成为部署工具的一个标准化能力让新增机器、新增服务时监控配置跟着部署流程自动走。适合正在维护 Ansible 自动化部署体系或者想给自己的集群搭建基础监控的团队参考。1. 需求拆解部署工具怎么就差监控这一环1.1 为什么最终选了 Prometheus团队内部之前不是没有讨论过其他方案。用得最多的旧方案是 Zabbix但问题很明显Zabbix 的 Agent 部署相对笨重配置体系偏重如果用 Ansible 去管理一个 agent 的安装配置就要写一堆 task而且它的数据模型和云原生环境下常用的 Exporter 机制配合并不顺畅。另一个考虑是只接入云厂商自带的监控服务但这个方案一旦跨云或者迁机房就得重新适配锁定的问题比较大。最后我们一致倾向 Prometheus原因很实际部署轻量、配置是纯文本 YAML 模板化友好、社区生态成熟一个 node_exporter 就能覆盖主机基础指标后续再想接 MySQL、Redis、Nginx 业务组件都有现成的 exporter 可以直接纳入。另外一个很实际的考量是Prometheus 的抓取模型非常适合 Ansible 这类基础设施即代码的体系。所有被监控对象的地址、端口、抓取间隔全部可以沉淀成 inventory 变量或 group_vars 里的结构体用 Jinja2 模板直接渲染成 prometheus.yml。这意味着监控配置和部署配置能共用同一套变量来源不会出现部署平台知道主机列表、Prometheus 配置却要靠人手工维护的割裂状态。所以这个改造的核心思路就一句话让部署工具不仅能装应用还能把被监控建模成配置数据。Prometheus 在这里不只是个软件组件它变成了一种标准的可观测性接口任何服务只要暴露 metrics 端点Easypack 就能自动把它纳入抓取范围。1.2 需求边界与控制面设计动手之前我先把需求边界划清楚了。这次改造第一版的目标是三个第一主机层监控全覆盖每台被管节点都部署 node_exporter第二服务存活状态可感知通过 Prometheus 的 up 指标自动发现目标是否运行正常第三基础告警能触达关键异常通过 Alertmanager 发到内部协作群里。至于日志采集、链路追踪、Grafana 可视化面板这次先不碰避免第一个版本就把范围撑得太大。边界控制这点特别重要。我之前见过不少团队做监控平台一开始就想着把全链路都做齐结果部署复杂、配置互相依赖最后连基础监控都跑不起来。这次我坚持用最小闭环推进先把指标采集这条线打通告警规则只放了最核心的几条比如节点在线状态、磁盘使用率超过阈值、node_exporter 本身挂掉。这样的好处是验收路径短几个小时就能看到数据从被管节点流到 Prometheus 页面里。从这个边界出发整个项目需要新增的组件也就清楚了一套 Prometheus Server 的安装和配置角色一套 node_exporter 字符角色以及可选的 Alertmanager 角色。下面我详细拆一下目录和角色设计。2. 整体架构设计与目录规划2.1 角色划分与目录结构我采用的目录结构延续了 Easypack 原有的角色风格没有引入太复杂的 Ansible 控制逻辑保持每台机器角色清晰。新增的三个角色分别是roles/ ├── prometheus │ ├── tasks/ │ │ ├── main.yml │ │ ├── install.yml │ │ ├── config.yml │ │ └── service.yml │ ├── templates/ │ │ ├── prometheus.yml.j2 │ │ ├── alert.rules.yml.j2 │ │ └── prometheus.service.j2 │ └── vars/ │ └── main.yml ├── node_exporter │ ├── tasks/ │ │ ├── main.yml │ │ └── service.yml │ ├── templates/ │ │ └── node_exporter.service.j2 │ └── vars/ │ └── main.yml └── alertmanager ├── tasks/ │ ├── main.yml │ └── service.yml ├── templates/ │ ├── alertmanager.yml.j2 │ └── alertmanager.service.j2 └── vars/ └── main.yml三个角色拆开有一个好处实际使用时可以根据环境裁剪。测试环境可以只装 node_exporter 和 prometheus生产环境再加 alertmanager。角色之间不互相依赖只通过变量约定一套公共的配置契约。比如 node_exporter 监听端口统一设定为 9100Prometheus 通过 group_vars 里定义的 node_group 动态发现目标。这个结构还有一个深层的考虑。Easypack 原有的角色体系里是不区分 state 概念的所有任务幂等执行。现在新角色必须延续这一风格不能搞成“重复执行就报错”的剧本。所以我把安装、配置、服务管理拆成独立文件用 handlers 统一控制服务重载时机保证重复跑 playbook 不会产生副作用。2.2 变量分层与配置来源监控接入要复用已有的 inventory 信息而不是单独维护一份监控清单。这个原则贯穿了整个设计。inventory 文件里定义了两个组prometheus 组保存监控服务端主机targets 组保存所有被监控节点。targets 组里每台主机的变量可以再扩展出 export_port、extra_labels 这类字段。[prometheus] monitor-01 ansible_host192.168.10.10 [targets] app-01 ansible_host192.168.10.21 app-02 ansible_host192.168.10.22 app-03 ansible_host192.168.10.23然后在 group_vars 里定义抓取目标。先把 targets 组的变量统一起来再用一个循环生成 Prometheus 的抓取配置。关键方法是利用 Ansible 内置的 hostvars 或者直接用 group 的 host 列表。实际我用的方式是在 prometheus 角色的模板里直接引用 groups[targets]scrape_configs: - job_name: node_exporter scrape_interval: 30s static_configs: - targets: {% for host in groups[targets] %} - {{ hostvars[host][inventory_hostname] }}:{{ export_port | default(9100) }} {% endfor %}这里要注意循环变量里取的是 inventory_hostname 而不是 host 变量本身因为 Ansible 在循环中如果想拿到主机对应的变量必须通过 hostvars 这个字典去查。这个细节如果写错配置就会变成主机名而不是 IP导致抓取失败。这也是我这次踩的第一个坑后面会在问题排查部分专门讲。变量分层方面我建议把所有默认值下沉到 role 的 vars 目录中比如 prometheus 版本号、数据保留时间、监听端口。而需要根据环境变化的变量放到 group_vars/host_vars 中比如告警接收人、告警阈值、抓取目标列表。这样角色本身是在任何环境都可以复用的真正差异化的内容收拢在 inventory 层。3. 核心角色实现细节3.1 prometheus server 端的具体部署Prometheus 的逻辑在安装部分不算复杂真正的难点在配置模板和服务管理。先看安装任务的核心片段这里用了二进制方式部署而不是容器。为什么不用容器因为 Easypack 的目标环境有一部分是老旧的物理机和内网服务器没有标准的容器运行时为了一个监控组件强推 docker 会增加很多前置条件所以我选择用二进制加 systemd 这种最通用的方式。- name: 创建 prometheus 系统用户 user: name: prometheus system: true create_home: false shell: /usr/sbin/nologin - name: 解压 prometheus 二进制包 unarchive: src: prometheus-{{ prometheus_version }}.linux-amd64.tar.gz dest: /opt/prometheus remote_src: false owner: prometheus group: prometheus - name: 创建数据目录 file: path: /data/prometheus state: directory owner: prometheus group: prometheus mode: 0750二进制包我这里没有直接从互联网下载而是放在 Easypack 的 files 目录里。为什么这样因为目标环境普遍在内网直接拉 GitHub 释放不稳定。把版本固定在本地仓库里虽然升级需要手动替换文件但换来的是安装过程的确定性和离线可部署性这在企业内部场景里是加分项。当然如果你所在环境没问题也可以用 get_url 动态下载核心逻辑不变。服务管理采用 systemd。我模板文件里最核心的是 ExecStart 参数这里有一个非常容易出问题的点[Unit] DescriptionPrometheus Server Wantsnetwork-online.target Afternetwork-online.target [Service] Typesimple Userprometheus Groupprometheus ExecStart/opt/prometheus/prometheus \ --config.file/etc/prometheus/prometheus.yml \ --storage.tsdb.path/data/prometheus \ --storage.tsdb.retention.time30d \ --web.listen-address:9090 Restartalways RestartSec15 [Install] WantedBymulti-user.targetstorage.tsdb.path 指向的目录必须存在且属主为 prometheus 用户否则启动直接失败。这个目录的权限问题几乎是 Prometheus 最常见的启动失败原因。另一个我在实际中踩到的坑是 systemd 的 Type 选 simple 没问题但如果没有 Wantsnetwork-online.target在机器重启后服务可能启动在网络就绪之前导致抓取本地目标时报错因此这里一定要保留。配置文件生成的核心是 prometheus.yml我把全局配置抽取成模板变量。下面是模板里比较关键的一部分# prometheus.yml.j2 核心片段 global: scrape_interval: {{ prometheus_scrape_interval | default(30s) }} evaluation_interval: {{ prometheus_evaluation_interval | default(30s) }} scrape_configs: - job_name: prometheus static_configs: - targets: [localhost:9090] {% if prometheus_scrape_targets is defined %} {% for target in prometheus_scrape_targets %} - job_name: {{ target.name }} scrape_interval: {{ target.interval | default(30s) }} static_configs: - targets: [{{ target.address }}] labels: service: {{ target.name }} {% endfor %} {% endif %}这里我保留了一个灵活性出口既支持用 groups[targets] 动态生成 node_exporter 的抓取列表也支持通过 prometheus_scrape_targets 变量手动追加任意自定义 exporter 目标。两种方式可以并存因为我可以把 node_exporter 这组目标以固定 job 的方式写进去其他业务组件通过变量扩展。后面实际对接 MySQL exporter 或者 Redis exporter 时只需要往变量里加一段结构不用改模板。3.2 node_exporter 角色与抓取链路node_exporter 是监控的最前线。它部署在每一台被管节点上负责暴露主机的 CPU、内存、磁盘、网络等基础指标。部署逻辑相对简单下载二进制、配置 systemd、启动服务、开放防火墙端口。这里有一个值得关注的设计决定Easypack 的每个角色都执行 handler 来 reload 或者 restartnode_exporter 也不例外。但是 reload 一个二进制服务其实没有意义所以 node_exporter 角色里我用的是 restart 而不是 reload。Prometheus server 才用 reload因为它在运行期可以接收 SIGHUP 信号重新加载配置。这里引出一个 Ansible handler 的设计经验restart 和 reload 的使用场景要区分清楚服务类型决定用哪种方式不要一概而论。node_exporter systemd 模板相对简单[Unit] DescriptionNode Exporter Afternetwork.target [Service] Typesimple Usernode_exporter Groupnode_exporter ExecStart/opt/node_exporter/node_exporter \ --web.listen-address:{{ export_port | default(9100) }} \ --collector.systemd \ --collector.processes Restartalways RestartSec20 [Install] WantedBymulti-user.target加了 systemd 和 processes 两个 collector这两个采集器能拿到系统服务的运行状态和进程维度数据对故障定位用处比较大。不过要留意processes collector 需要 root 权限才能读取 /proc 下其他用户的进程信息如果使用普通用户运行这个指标会不完整。所以我在安装任务里把 node_exporter 的用户设为 root虽然不够安全但在这个场景下属于常见做法。如果你有更严格的安全要求可以去掉 processes collector改用普通用户运行。防火墙的处理我用的是 firewalld 模块开放式只对内网网段开放 9100 端口- name: 防火墙开放 metrics 端口 firewalld: port: {{ export_port | default(9100) }}/tcp permanent: true state: enabled immediate: true when: ansible_facts[os_family] RedHat这里注意 immediate: true 一定要加不然只写入永久配置当前会话不生效测试环境里就会出现端口明明放通了但 Prometheus 抓不到的情况。3.3 告警链路快速接入 Alertmanager告警在这个改造里是第二优先级但它对监控体系的价值很关键。没有告警的监控就像没有闹钟的火警系统指标页面做得再花哨出了问题永远要等人盯。Alertmanager 的角色我尽量做得轻量只负责把 Prometheus 推送的告警消息转发到内部协作工具的 webhook。Alertmanager 配置文件模板核心是路由和接收器route: group_by: [alertname] group_wait: 10s group_interval: 30s repeat_interval: 4h receiver: internal-bot receivers: - name: internal-bot webhook_configs: - url: {{ alertmanager_webhook_url }} send_resolved: trueAlertmanager 的机制有个细节Prometheus 根据告警规则计算出的告警会推送到 Alertmanager但不会在 Alertmanager 侧做去重和抑制这些全靠路由配置完成。这里我用一个比较短的 repeat_interval4小时避免同一个告警刷屏太频繁。组等待时间设置得短是为了让同一批故障快速聚合成一组消息而不是每个目标都单独发一条。这个配置在生产环境里可以根据团队实际响应速度再调但不要一开始就把 repeat_interval 设成 24 小时容易让值班的人漏掉非紧急告警。Prometheus 端的告警规则模板我单独放在 alert.rules.yml.j2 里。几条核心规则如下groups: - name: node-alerts rules: - alert: NodeDown expr: up 0 for: 5m labels: severity: critical annotations: summary: 节点离线 description: {{ $labels.instance }} 已经离线超过 5 分钟 - alert: DiskUsageHigh expr: (1 - (node_filesystem_avail_bytes{fstype!~tmpfs|overlay} / node_filesystem_size_bytes{fstype!~tmpfs|overlay})) * 100 85 for: 10m labels: severity: warning annotations: summary: 磁盘空间不足 description: {{ $labels.mountpoint }} 使用率超过 85%当前 {{ $value | printf \%.1f\ }}%这里 for 参数的作用是防止瞬时抖动导致误报比如 node_exporter 重启过程中 up 指标会短暂变成 0如果不加 for 条件每次重启都会误发一条告警。生产经验是关键服务存活类告警 for 设 3-5 分钟资源用量类告警设 8-10 分钟这样告警准确率会提升很多。4. 编排 Playbook 与验证流程4.1 Playbook 入口与 tag 控制整体 playbook 入口我是在 Easypack 已有的 site.yml 里加了一个 include而不是另起炉灶。这样部署工具的所有能力仍然在一个入口下管理。我单独拆了一个 monitoring.yml 文件来承载监控相关角色- hosts: prometheus roles: - role: prometheus tags: - prometheus - hosts: targets roles: - role: node_exporter tags: - node_exporter - hosts: prometheus roles: - role: alertmanager tags: - alertmanagertag 的设计很有必要。Easypack 原来的部署流程是多阶段执行的新增节点时可能只需要跑 node_exporter 这个阶段而不是把整个监控栈全部重装一遍。通过 tag 可以做到精确控制ansible-playbook -i inventory/hosts monitoring.yml --tags node_exporter如果已经部署过监控只想更新 Prometheus 的配置可以这样ansible-playbook -i inventory/hosts monitoring.yml --tags prometheus --limit prometheus这里还需要注意一个 Ansible 的特性handlers 默认只在一个 play 结束时执行。如果你在一个 playbook 里同时更新了配置文件和重启了服务一定要确保 handler 的监听关系正确否则配置变更不会触发服务重载。我在配置任务里显式地加了 notify- name: 渲染 prometheus 配置 template: src: prometheus.yml.j2 dest: /etc/prometheus/prometheus.yml notify: - reload prometheus同时 handler 里定义好监听和动作- name: reload prometheus systemd: name: prometheus state: reloaded这个 mode 虽然看起来基础但它保证了配置是幂等地应用到既有集群上的。只要模板文件有任何变化配置就会更新并触发重载而如果模板没变整个任务就走 no_changes 分支不会产生任何动作。4.2 验证步骤与 PromQL 确认部署完成后验证动作不能省。我习惯把验证逻辑分成三层第一层是端口层面检查监听是否正常。在监控服务器上执行curl -s http://localhost:9090/-/healthy返回 Prometheus is Healthy 就说明服务端没挂。节点层面检查curl -s http://localhost:9100/metrics | head -n 5第二层是 Prometheus 的 target 状态页面。直接用 API 查询curl -s http://localhost:9090/api/v1/targets | jq .data.activeTargets[] | {scrapeUrl, health}正常情况下所有 target 的 health 都应该是 up。如果出现 down这个命令能快速告诉你是哪台机器的地址有问题。第三层是 PromQL 验证这是确认监控数据真正在产生价值的环节。我最常用的三条查询节点在线状态up 0能直接列出所有失联节点负载情况node_load1 4结合机器核数可以判断是否突发高负载磁盘余量node_filesystem_avail_bytes{mountpoint/} 10 * 1024 * 1024 * 1024检查根分区剩余是否小于 10GB跑到这一步Prometheus 的数据流已经完整打通。到告警侧再验证一轮可以在测试环境故意停掉一个 node_exporter过几分钟在 Alertmanager 的页面就能看到对应的 NodeDown 告警出现。这个验证虽然有点破坏性但效果最直观建议在预发布环境做一次。5. 常见问题与排查技巧实录5.1 服务起不来先查这三个高频点Prometheus 启动失败的原因我遇到过很多次大部分跑不出去下面三个方向排查第一个是数据目录权限。凡是看到日志里报 permission denied 的基本都指向 /data/prometheus 目录不是 prometheus 用户所属。这个目录在安装任务里创建的如果某次手动介入改了目录属主或者数据盘是后挂载的权限就很容出问题。排查命令ls -ld /data/prometheus journalctl -u prometheus | tail -n 50第二个是配置文件 YAML 语法错误。Prometheus 对 YAML 的缩进要求非常严格而且 config file 加载失败时会直接拒绝启动。Ansible 渲染模板时如果 for 循环的缩进不对渲染出来的文件常常看不出明显问题但就是无法加载。这时候先用 promtool 做本地校验最方便/opt/prometheus/promtool check config /etc/prometheus/prometheus.yml这个方法比看日志快太多promtool 会直接报出第几行有语法错误。如果你用的版本比较新还可以用 check rules 单独校验规则文件。第三个是 systemd 启动超时。网络就绪之前的启动延迟会导致服务进入失败状态这时把 sleep 塞到 ExecStartPre 里不算好办法。更靠谱的是保证 unit 文件里 Afternetwork-online.target 和 Wantsnetwork-online.target 都写上然后让 systemd 等待网络就绪。5.2 Target 状态不是 UP别急着改业务代码Prometheus 页面里看到某个 target 是 down 的时候第一反应不是去排查被监控服务的问题而是先确认 Prometheus 到底能不能通过网络触达目标端口。我见过最典型的误判场景是node_exporter 服务明明起来了但 internal-bot 抓取超时原因是 Prometheus 服务器和被监控节点之间有防火墙策略9100 端口压根没放通。排查顺序应该是先在 Prometheus 所在服务器上用 telnet 或者 nc 探测目标端口nc -vz 192.168.10.21 9100如果端口不通说明是网络层问题检查防火墙和安全组即可。如果端口通再看 target 页面里的 scrape 错误信息是什么常见的是 connection refused服务没起来或者 context deadline exceeded抓取超时一般是被监控端太忙或者网络丢包严重。还有一个坑值得提Prometheus 的 job_name 里如果有重复定义后加载的会覆盖前面的导致部分 target 消失。这个问题在模板渲染后我遇到过解决办法是抓取前检查最终渲染出的配置文件里 scrape_configs 是否有重复 job_name。5.3 幂等性与重复执行的安全感问题Ansible 的核心价值之一是幂等但监控组件的角色里我见过不少破坏幂等的写法。最常见的坑是安装任务每次都下载和解压二进制包表面上重复执行没问题但实际会反复覆盖文件导致服务重启。虽然不影响最终结果但这个行为不够干净还会拖慢整个 playbook 的执行速度。我的做法是在所有安装步骤前加一个状态判断Exists 判断文件已经存在就跳过解压- name: 检查 prometheus 是否已安装 stat: path: /opt/prometheus/prometheus register: prometheus_binary - name: 解压 prometheus 二进制包 unarchive: src: ... dest: /opt/prometheus when: not prometheus_binary.stat.exists这样重复执行的时候安装步骤会诚实地说 ok而不是 changed服务也不会被无端重启。对运维同学来说重复执行整个站点级 playbook 是一种安全感测试如果每跑一次都有服务被重启那这工具根本无法放心调度。另一个幂等细节是 Alertmanager 的 webhook 地址可能在后期变更而配置文件模板渲染后的 checksum 比对是 Ansible 自己处理的。如果通知地址改了notify 到 handler 自动重载即可。重点是不要用 command 模块去手动 sed 修改地址那是反 Ansible 的。5.4 模板渲染的隐藏细节最后这部分分享一个容易被忽略但真实影响运行的教训Jinja2 模板里写 with item 循环时缩进一定要严格控制。Prometheus 配置对缩进极度敏感模板输出后看起来格式正常实际文件可能因为多了两个空格导致整个 static_configs 块解析失败。我建议任何模板改动后先跑一次语法检查并且把结果文件下载到本地检查一遍再应用到生产。可以在 playbook 里加一个 debug 任务输出最终渲染结果- name: 输出最终配置检查 debug: msg: {{ lookup(template, prometheus.yml.j2) }}这个方法在联调阶段特别有用能直观地看到变量被替换后的真实内容缩进问题一眼就能看出来。经验是宁可多花两分钟看渲染结果也不要直接推送到几十台服务器上去试错。6. 融入 Easypack 原有体系的一些体会6.1 与既有服务部署流程的衔接这次改造不是孤立的组件安装而是把监控能力接到现有部署链路里。Easypack 原有的站点级 playbook 里有 nginx、应用程序部署等步骤为了让新部署的服务自动进入监控范围我在应用的 role 末尾加了一个可选的任务把当前主机的 exporter 目标写入监控配置的变量源里。具体实现是使用一个共享的 group_vars 文件部署应用和 Prometheus 配置都从这里读取目标列表。这个设计思路是把监控目标的注册从“部署后手工添加”改成“部署时自动登记”。比如部署一个 MySQL 实例时角色里会检测 mysql_exporter 的开关如果开启就自动把这个实例的地址追加到监控目标列表。虽然这个追加动作本身是通过一个小的 template 任务完成的但它把“部署”和“可观测”这两个阶段从流程上打通了。在实践中这个变化带来的感受很直接以前新服务上线后要单独找运维同学加监控项现在一条 Ansible 命令跑完Prometheus 的 target 页面里立刻多出对应的 job。这件事看似是方便但本质上是把监控从“事后补救”变成了“交付流程的一部分”。6.2 版本升级与回滚策略Prometheus 生态迭代不算快但也不慢每隔几个月就有新版本。Easypack 对组件版本的循环方式我很早就定了规矩版本号全部收敛在 role 的 vars 文件里升级只改一个变量。因为二进制文件本身放在本地升级时需要手动替换 files 目录里的包这个动作是人工触发的正好提供了一个 review 的时机。回滚的策略更简单只要把版本号改回旧版本重新跑 install 任务即可。唯一要注意的是版本回滚时配置文件的兼容性Prometheus 的新版配置项在旧版未必能解析。我遇到过一次从 2.5x 回滚到 2.4x 时新版配置里多了一个 old scrapes 参数旧版直接拒绝启动。所以回滚之后一定先跑一遍 promtool check config确认配置文件兼容再启动服务。如果你也要在类似 Easypack 项目里做这样的改造我的建议是尽量保持角色精简、配置模板化、变量分层清晰同时把验证步骤固化成脚本。后面无论是加监控目标还是升级版本都不会因为一个动作忘记了验证而引入故障。最后再分享一个小技巧把 Prometheus 的配置校验和节点存活检查写成一个 post_task挂在 playbook 末尾这样每次部署完监控部分结果会自动输出到终端一眼就能看到所有节点是否成功纳管。这个习惯帮我省了非常多的排查时间。