ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Prometheus容器化部署实战:配置、告警与Grafana可视化

Prometheus容器化部署实战:配置、告警与Grafana可视化

1. 从裸安装到容器化:Prometheus部署方式背后的技术选择

1.1 Docker到底帮我们解决了什么问题

初次接触Prometheus的时候,我走的是传统的二进制部署路线:下载压缩包、解压、写systemd服务、设置开机自启,一套流程下来少说也要十分钟。如果还要部署node_exporter和Alertmanager,那每台机器都得重复一遍,版本差异、目录不一致、配置文件散落各处的问题很快就暴露出来。

换成Docker部署之后,大部分困扰直接消失了。镜像自带prometheus二进制文件,运行环境是固定的,只需要指定配置文件和数据目录,剩下的交给容器运行管理器。升级的时候拉一个新标签、重新创建容器,回滚也只需要把镜像tag切回去,几秒钟就能完成。对监控这种“平时不太关注、出了问题又必须马上能查”的基础设施来说,这种可复现、可回滚的部署方式,价值远远大于省下的那几分钟。

另一个容易被忽略的点是隔离。Prometheus本身是单二进程应用,但它依赖的配置文件、规则文件、数据目录如果直接铺在宿主机上,难免和其他应用产生交集。容器化之后,文件通过挂载卷传入,数据通过命名卷保存,宿主机上只保留一份干净的外部配置目录,整个生命周期完全可控。

1.2 镜像版本标签怎么选,不是latest就完事

官方镜像prom/prometheus的发布节奏很规律,每个版本都有对应的文档和特性说明。实际使用中我不建议用latest,原因是Prometheus大版本迭代时有行为变化,比如v2到v3在部分参数和默认行为上做了调整,如果哪天latest指到了新大版本,你的启动参数可能就不兼容了。

比较稳妥的做法是在docker run或者docker-compose里直接锁定一个具体的v2小版本,例如:

docker pull prom/prometheus:v2.53.0

同时,重要的一步是把镜像版本和官方文档对应起来。不同版本的prometheus.yml字段差异不大,但命令行参数、TSDB存储行为、API接口路径都会有细微差别。锁定版本后,查询文档时也要切到同版本页面,这样排查问题才不会被过时信息带偏。

1.3 资源预估:内存和磁盘要提前想好

Prometheus的时序数据库(TSDB)是内存友好的,但并非不加限制地占用资源。它的内存使用量主要取决于时序数量、标签基数、抓取频率和查询负载。简单估算时,可以按“大约每条时序占用300字节到1KB内存”来粗算。如果你只监控几台服务器、每台采集几百个指标,2GB内存就非常宽裕;但如果你把业务接口的自定义指标也接进来,指标量到了十几万甚至上百万条时序,内存就可能需要16GB以上。

磁盘方面,TSDB的压缩做得很好,实际占用取决于数据量、抓取间隔、标签数量。建议在启动参数里同时设置保留时间和保留大小,两者可以配合使用:

--storage.tsdb.retention.time=15d --storage.tsdb.retention.size=20GB

这样一组参数的含义是:按时间保留15天,同时磁盘占用超过20GB时会优先删除最老的区块。我建议都设上,只按时间保留的话,遇到指标暴涨容易出现磁盘写满;只按大小保留的话,又可能比预期删得快。

2. 先读懂prometheus.yml:抓取、评估与告警三条管线怎么协同

2.1 global配置中的几个interval,各管各的事

刚上手时,我把prometheus.yml里的三个时间参数搞混过:scrape_interval是抓取采集目标的周期,evaluation_interval是规则评估周期,两者默认都是15s,但职责完全不同。scrape_interval影响数据采集频率,evaluation_interval影响告警规则多久被评估一次。如果你的告警延迟很高,先检查是不是evaluation_interval被调大到了几分钟。

另外一个容易被忽略的参数是scrape_timeout,它控制单次抓取的超时时间,默认10s,理论上不能大于scrape_interval。如果遇到某个目标响应很慢,把抓取间隔调大不如先分析目标为什么慢,超时时间调过头反而会掩盖问题。

2.2 scrape_configs:一个job是如何变成targets的

配置里最核心的部分是scrape_configs。我习惯这样理解:一个job代表一类采集任务,比如node_exporter、cadvisor、prometheus各是一个job;每个job下面通过static_configs或服务发现机制定义一组targets,每个target就是一个具体的HTTP拉取端点。

当Prometheus开始抓取一个target时,它会自动附带一些标签:job来自job_name配置,instance默认取__address__的值。这些自动标签会出现在所有查询结果里,后续在告警模板里会频繁用到。

一个最小可用的抓取配置如下:

scrape_configs: - job_name: 'prometheus' static_configs: - targets: ['localhost:9090']

如果只是想把监控跑起来,这段配置就够了。但实际使用中,我强烈建议从一开始就给目标打上更有语义的标签,因为默认的instance是IP加端口,时间长了根本看不出是哪台机器。

- job_name: 'node_exporter' static_configs: - targets: - '10.0.0.11:9100' - '10.0.0.12:9100' labels: env: production role: web

这些标签会直接进入每条时序数据的label集合,查询和告警时都能用。标签不宜过多,否则会推高基数,但要保证一眼能看出“这是什么环境、什么角色”。

2.3 rule_files与alerting:规则谁执行,告警发给谁

Prometheus的告警体系分成两层:规则评估和告警发送。规则文件的写入路径由rule_files指定,Prometheus会按照evaluation_interval周期检查规则。一旦规则表达式持续满足条件,就生成一条告警,并进入激活状态。

告警并不由Prometheus直接推送到钉钉或邮件,而是先发给一个独立的组件Alertmanager,由它负责分组、去重、路由和通知。配置上需要告诉Prometheus去哪找Alertmanager:

alerting: alertmanagers: - static_configs: - targets: - 'alertmanager:9093' rule_files: - '/etc/prometheus/rules/*.yml'

规则文件里定义的是一条条告警规则,例如:

groups: - name: node_alerts rules: - alert: HighCpuUsage expr: 100 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100 > 80 for: 10m labels: severity: warning annotations: summary: "CPU usage on {{ $labels.instance }} is above 80%"

这里的for: 10m非常关键,它表示指标需要持续10分钟超过阈值才触发告警,可以过滤掉瞬时尖峰。规则、抓取、告警发送这三条线,在时序上互相配合:抓取提供数据,规则周期评估,告警通过Alertmanager送出,理解了这个链路,后面排查告警丢失会轻松很多。

2.4 TSDB数据如何落盘:block、WAL与retention

Prometheus自带的时序数据库采用按区块组织的方式。每隔大约2小时,内存中的最新数据会被写入一个新的block文件;block包含指标数据、索引和元数据。写入过程中先有WAL预写日志做保障,Prometheus崩溃重启后可以靠WAL恢复未落盘的数据。

这解释了为什么数据目录里会有一堆时间戳命名的目录,也解释了为什么必须把数据目录做成持久化卷:容器一旦被删除,留在容器层里的block会一起被清掉,只有保存在卷里的数据才能活下来。

--storage.tsdb.retention.time和--storage.tsdb.retention.size在这里生效。注意二者是“或”的关系,任何一个条件先达到,就会触发旧block的清理。理解了这个机制,就知道为什么单节点Prometheus不适合承担超大规模指标存储,它是为可靠、简洁的本地存储设计的,长时间跨度查询和大规模历史回放并不擅长。

3. 用docker-compose搭一套能监控Docker主机和容器的基础设施

3.1 先用docker run验证最小可用环境

如果你第一次接触Prometheus,我不建议直接上compose。先跑一个最简容器,把抓取链路跑通,再逐步扩展,这样出了问题容易定位。先创建一个配置文件prometheus.yml:

global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: 'prometheus' static_configs: - targets: ['localhost:9090']

然后执行:

docker run -d \ --name prometheus \ -p 9090:9090 \ -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml:ro \ -v prometheus-data:/prometheus \ prom/prometheus:v2.53.0 \ --config.file=/etc/prometheus/prometheus.yml \ --storage.tsdb.path=/prometheus \ --storage.tsdb.retention.time=15d

启动后用浏览器访问宿主机的9090端口,进入Prometheus自带界面,点击“Status”下的“Targets”,如果看到prometheus这个job的状态是UP,说明最小链路已经通了。这个自监控target通过localhost:9090抓取,实际上抓取的是Prometheus容器自己。

3.2 compose纳入node-exporter和cadvisor

只监控Prometheus自身意义不大,我们通常还要监控宿主机的CPU、内存、磁盘,以及Docker容器本身的运行指标。这里会用到两个经典导出器:node_exporter负责暴露主机指标,cadvisor负责暴露容器指标。

下面是一个推荐的三服务compose配置:

services: prometheus: image: prom/prometheus:v2.53.0 container_name: prometheus ports: - "9090:9090" volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro - ./rules:/etc/prometheus/rules:ro - prometheus-data:/prometheus command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--storage.tsdb.retention.time=15d' - '--storage.tsdb.retention.size=20GB' - '--web.enable-lifecycle' restart: always node-exporter: image: prom/node-exporter:v1.8.2 container_name: node-exporter ports: - "9100:9100" volumes: - /proc:/host/proc:ro - /sys:/host/sys:ro - /:/rootfs:ro command: - '--path.rootfs=/host' restart: always cadvisor: image: gcr.io/cadvisor/cadvisor:v0.49.1 container_name: cadvisor ports: - "8080:8080" volumes: - /:/rootfs:ro - /var/run:/var/run:ro - /sys:/sys:ro - /var/lib/docker/:/var/lib/docker:ro - /dev/disk/:/dev/disk:ro privileged: true restart: always volumes: prometheus-data:

node-exporter需要挂载宿主机的/proc、/sys和根目录,通过--path.rootfs=/host让它在容器内正确读取主机信息。cadvisor需要读取Docker的运行目录,所以也要挂载相关系统路径,并加privileged权限。

对应的抓取配置扩展为:

scrape_configs: - job_name: 'prometheus' static_configs: - targets: ['localhost:9090'] - job_name: 'node_exporter' static_configs: - targets: ['node-exporter:9100'] - job_name: 'cadvisor' static_configs: - targets: ['cadvisor:8080']

这里的关键点是:容器之间通过compose创建的内部网络互相访问,所以Prometheus里填的target地址是服务名node-exporter:9100,而不是localhost或宿主机的IP。

3.3 数据卷与文件权限:别让容器以nobody身份写不了目录

用bind mount挂载配置文件和目录时,经常会碰上一个隐性问题:Prometheus容器默认不是以root运行的,容器内部用户是nobody。如果你的宿主机目录权限是700且仅属于root,容器进程可能就无法读取配置或写入数据目录。

我自己的习惯是:配置文件和规则文件所在目录设置为755或750,并且确保文件本身至少是644。数据目录直接用命名卷,不直接挂在宿主机的某个文件夹下面,这样权限由容器运行时管理,少踩很多坑。需要单独保存数据时,可以挂载并预先chown调整属主。

另外一个细节是挂载配置时加:ro只读。这不是必需的,但能防止容器内部误改配置文件,也提醒自己配置的源头只在宿主机那一侧。

3.4 用promtool做配置自检

Prometheus自带一个非常有用的命令行工具promtool,不需要额外安装。修改配置后先跑一次校验,再重启容器,能省去大量兜圈子排查时间:

docker run --rm -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml:ro prom/prometheus:v2.53.0 promtool check config /etc/prometheus/prometheus.yml

规则文件也可以校验:

docker run --rm -v $(pwd)/rules:/etc/prometheus/rules:ro prom/prometheus:v2.53.0 promtool check rules /etc/prometheus/rules/*.yml

promtool还会提示一些常见问题,比如规则表达式语法错误、配置字段拼写错误。上线前多花这几十秒,后面能少折腾半小时。

4. 让抓取目标从简单到复杂:静态配置、标记与文件发现

4.1 静态配置与标签设计

监控几台固定服务器时,静态配置最简单,也最好维护。建议从第一天就养成打标签的习惯。比如把生产环境和测试环境区分开:

- job_name: 'node_exporter' static_configs: - targets: ['10.0.0.11:9100', '10.0.0.12:9100'] labels: env: production - targets: ['10.0.0.21:9100'] labels: env: staging

这些标签会在查询和告警路由里发挥很大作用。比如Alertmanager可以根据env标签把生产环境的高优告警和测试环境的低优告警分开处理。

条目增多后,每个文件维护几百个target会显得笨重。这时候就需要引入服务发现机制,让Prometheus动态读取目标列表。

4.2 基于文件的服务发现:适合中小规模、好理解的动态方案

文件服务发现(file_sd)是静态配置到全面服务发现之间最好的过渡方案。它允许外部脚本把目标列表写入JSON或YAML文件,Prometheus按固定周期重新读取并自动更新target。

Prometheus配置里这样写:

- job_name: 'node_exporter' file_sd_configs: - files: - '/etc/prometheus/targets/*.yml' refresh_interval: 30s

对应的targets文件targets/node1.yml内容如下:

- targets: - '10.0.0.11:9100' - '10.0.0.12:9100' labels: env: production

这样运维脚本或配置管理工具只需要把新增主机的IP和标签追加到文件列表里,30秒后Prometheus就会自动纳入采集,不需要重启服务,也不需要reload。

我实际用的过程中,最大的收益是:每次加机器,只需要在targets目录下放一个新文件,既不影响现有配置,又能单独查看每个文件的变更记录。如果以后要迁移到Consul之类的注册中心,Prometheus配置文件的结构也不需要大改,只换discovery类型即可。

4.3 relabel:在抓取前调整标签的常见玩法

relabel_configs是Prometheus非常灵活、但是初学者最容易犯迷糊的模块。它的作用是在抓取前对目标标签进行增删改。最常见的需求是把默认的instance标签从“IP:9100”变成纯IP,让显示更干净。例如:

- job_name: 'node_exporter' static_configs: - targets: ['10.0.0.11:9100'] relabel_configs: - source_labels: [__address__] regex: '([^:]+):9100' target_label: instance replacement: '$1'

这段配置的含义是:从__address__这个内部标签提取出冒号前的IP部分,写入instance标签。__address__是Prometheus抓取时使用的一个内部标签,normal方式下会通过instance透出,但我们可以改掉它。

更复杂的relabel场景包括:从URL路径提取环境信息、过滤指定标签的目标、给不同job注入统一标签等。开始阶段不必贪多,先把source_labels、regex、target_label三个字段练熟,就已经能应对七八成的需求。

5. 把指标接进Grafana,把告警接进Alertmanager

5.1 容器网络里,Grafana为什么填prometheus:9090

Grafana是Prometheus最常用的可视化面板。如果Grafana也用Docker部署,并且和Prometheus在同一个compose环境里,数据源地址就可以直接写http://prometheus:9090,这是compose内部DNS解析的结果。

如果你把Grafana单独跑在另一个容器网络,或者直接跑在宿主机进程里,那数据源地址就要填http://宿主机IP:9090。很多人在这一步卡住,其实是没理清容器网络和宿主机网络的边界。

一个简单的判断方法:谁要访问谁,就站在谁的视角看地址。Prometheus要抓node-exporter,所以站在Prometheus容器视角,node-exporter的地址是服务名;Grafana要查询Prometheus API,所以站在Grafana容器视角,地址是prometheus:9090。宿主机浏览器要访问Grafana或Prometheus,用的是宿主机IP加映射端口。

Grafana里添加数据源时,类型选Prometheus,URL填对应地址,保存后切到Dashboards页面导入ID为1860的Node Exporter Full模板,就能看到一套完整的node指标面板。这套流程跑通后,相当于Prometheus + Grafana的可视化闭环已经完成。

5.2 一条告警从触发到通知的完整链路

如果把告警流程画成一条时间线,可以这样理解:

  1. Prometheus按evaluation_interval周期评估规则。
  2. 规则表达式持续满足for设置的时间后,告警进入Firing状态。
  3. Prometheus把激活的告警以HTTP请求发给Alertmanager。
  4. Alertmanager根据路由规则做分组、抑制后,把通知发到对应接收端。

这里特别要注意for的作用。如果阈值条件只持续了一分钟,而for设置的是5分钟,那么告警不会触发。反之,如果不想收到任何短暂抖动引起的告警,就把for设为一个合理区间,比如5分钟或10分钟。

Alertmanager的配置包含两个核心部分:route和receivers。一个简化但常用的配置如下:

route: group_by: ['alertname'] group_wait: 30s group_interval: 5m repeat_interval: 4h receiver: 'default-webhook' routes: - match: severity: critical receiver: 'critical-webhook' receivers: - name: 'default-webhook' webhook_configs: - url: 'http://10.0.0.30:9000/notify' - name: 'critical-webhook' webhook_configs: - url: 'http://10.0.0.30:9000/notify/critical'

这条路由配置的逻辑是:所有告警先进入默认的webhook接收端;但如果告警带有severity=critical标签,就转到critical-webhook。group_by: ['alertname']会把5分钟内相同告警名的告警合并成一组,避免通知轰炸。

5.3 通知落在钉钉或企业微信这类平台时,需要自己写转发层

Alertmanager原生接收端里没有钉钉或企业微信这类渠道,通常的做法是配置一个webhook_configs,指向一个自定义转发服务。这个转发服务接收Alertmanager的JSON告警体,再格式化成目标平台的消息结构发出去。

这里分享一个实践经验:不要在一个receivers里同时配多种通知渠道,初期只保留最稳定的一个,等告警链路跑稳定了再加其他渠道。否则告警触发时,多个渠道同时故障或重复,反而会掩盖真正的问题。

6. 运行阶段最值得记住的几类问题,以及它们背后的原因

6.1 容器启动后立刻退出,怎么定位

现象是docker run之后容器状态显示Exited,这时第一反应不要反复重启,先看日志:

docker logs prometheus

常见的错误包括:配置文件内字段拼写错误,找不到规则文件路径,数据目录没有写权限,命令行参数写错。promtool check config已经能过滤掉大部分语法问题,如果日志提示无法创建TSDB,优先检查数据目录权限。

6.2 挂载目录权限导致的TSDB写入失败

这件事我踩过不止一次。把数据目录bind mount到宿主机某个目录后,Prometheus启动时报错“mkdir /prometheus: permission denied”,原因是容器内用户nobody对宿主机目录没有写权限。

解决方式有两种:一是在宿主机上执行chown -R 65534:65534 /data/prometheus,65534是nobody用户的UID;二是直接改用命名卷,让容器运行时负责管理。考虑到数据可迁移性,我推荐第一种方式时只给自己本机实验用,生产环境用命名卷和工具链配合备份,不要手工管理一堆主机目录。

6.3 配置改了没生效,不是文件错了,而是没有reload

Prometheus默认不会监控配置文件变化。你可以通过两种方式触发热加载:

docker kill -s SIGHUP prometheus

或者启用--web.enable-lifecycle后调用API:

curl -X POST http://localhost:9090/-/reload

注意,kill -s SIGHUP发到容器里的进程1,也就是Prometheus主进程,它会重新加载配置。API方式更直观,尤其是你处在远程环境时。建议启动参数里直接加上--web.enable-lifecycle,但要注意这个参数在生产公网环境不要随意暴露,应在网关层做好防护。

6.4 容器删掉重来,监控数据全没了

这是容器部署最容易踩的坑。如果启动时没有给/prometheus挂载持久化存储卷,所有监控数据都写在容器可写层,容器删除后,数据随之消失。

解决方式就是我一直强调的命名卷或挂载目录。另外,升级Prometheus时也别直接docker rm -f老容器,最稳妥的做法是:先停容器,确认数据卷还在,再创建带相同卷引用的新容器。如果连卷名都变了,数据自然也就不在了。

6.5 时间不同步导致抓取周期不稳定

Prometheus对时间敏感,目标主机和Prometheus所在主机如果存在明显时钟漂移,会导致数据点错位、查询结果异常。Docker容器默认和宿主机共享内核时钟,但宿主机本身如果没做好时间同步,容器内也一样漂。

所以,宿主机上一定配置好时间同步服务。不要只盯着容器,底层的时钟源才是根因。这个问题的典型表现是:Targets显示UP,但图表里的数据断断续续,改进时间同步后立马恢复正常。

6.6 Target在Targets页面里长期DOWN,别急着怀疑Prometheus

当一个target在Prometheus的Targets页面上显示DOWN,问题不一定在Prometheus本身。要按顺序排查:

  1. 目标端口监听是否正常:在目标机器上执行curl http://localhost:9100/metrics。
  2. 端口是否对外可达:在Prometheus容器里执行wget -qO- http://目标IP:9100/metrics,这一步验证容器网络到达目标主机的连通性。
  3. 防火墙或安全组是否放行端口。
  4. 如果页面显示连接拒绝或超时,信息会直接写出来,按提示逐项排查就行。

还有一个小细节:如果node-exporter配置了--web.listen-address绑定非9100端口,target地址必须改成对应端口,否则Prometheus会默认访问9100。这类“配置和实际不符”的问题,比网络故障更常见。

我在实际操作中最深的体会是:Docker化并没有改变Prometheus本身的工作方式,它只是把“运行环境绑在一起”这件事交给了容器,但配置文件结构、数据持久化、网络连通性和告警链路这些基本功,仍然需要自己搞清楚。每多懂一环,出问题时的定位范围就缩小一大截。整套监控建好以后,我建议顺手做一次演练:把Prometheus数据目录彻底删掉再恢复,新建一台机器,通过文件发现自动加入监控,然后手动触发一条告警验证通知链路。这套演练做完,剩下的日常维护才会真正轻松。

RELATED READING

延伸阅读

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