1. 容器化 Prometheus 到底解决了什么问题
前段时间帮一个团队收拾监控烂摊子,宿主机上那份 Prometheus 是两年前手工装的,配置文件散落在三个目录,版本号只记在某位同事的聊天记录里,机器一换就得从头再来一遍。这类场景我见过太多次,所以现在只要有人问监控怎么搭,我的第一反应都是把Prometheus 容器化。容器化这件事本身没什么玄学,它真正解决的是三个很具体的问题:环境一致性、升级回滚成本、以及配置文件的可迁移性。
Prometheus是一套开源的指标采集与时序数据库系统,核心工作模式是“拉取”——它按固定周期主动去各个目标上抓取/metrics暴露的指标,存进本地时序库,再配合告警规则和查询语言把这些数字变成可判断的信号。容器化搭建,就是把 Prometheus 服务端、各类 exporter、告警组件统统装进容器,用编排文件描述它们之间的关系。搞完之后你得到的是:一条docker compose up -d就能拉起整套监控,配置文件全在版本控制里,换台机器十分钟复现。
这套内容适合谁看?我按三类人给个参考。第一类是刚接手监控、手上只有一堆零散 shell 脚本的同学,你需要的是一个能跑起来的最小可用版本;第二类是自己搭过二进制版、但被升级和依赖折腾过的人,你会更关心镜像版本策略、数据卷和参数调优;第三类是团队里负责基础设施的,你要考虑的是目录规范、告警规则怎么管、数据保留多久、磁盘怎么估。下面这些内容我会从选型一直讲到排错,参数计算过程尽量写清楚,你照着改改就能用。
提示:容器化不等于“随便跑个镜像”。Prometheus 是有状态服务,数据目录、配置文件、告警规则三样东西必须显式管起来,否则容器一重建,历史数据全丢。
2. 方案选型:为什么这样拆,而不是别的拆法
2.1 二进制、单容器、编排三者的取舍
先把选项摆清楚,省得后面反复纠结。二进制部署的优点是性能路径最短、排查问题时能直接strace到进程,缺点是依赖管理全靠人,systemd 单元文件、用户权限、目录结构每台机器都要维护一遍。单容器(docker run一把梭)比二进制省事,但参数一多命令行就没法看,多组件协同时网络和依赖顺序全靠手写。编排方案(这里用 Compose,K8s 场景同理但对象不同)把服务定义、网络、卷、健康检查、依赖顺序都写进一个声明式文件,代价是多一层抽象。
我实际的取舍标准很朴素:组件数量超过两个,或者需要长期维护,就上编排。Prometheus 单独跑没意义,你至少要配一个 exporter 才有数据,再加上告警规则和可能的 Grafana,妥妥超过两个。另外还有个隐性收益:编排文件本身就是文档。新人接手时看compose.yaml就知道这套监控由哪些部分组成、端口怎么映射、配置从哪挂进去,比看十页 wiki 都管用。
| 维度 | 二进制部署 | 单容器 docker run | Compose 编排 |
|---|---|---|---|
| 环境一致性 | 差,依赖宿主 | 较好 | 好,声明式可复现 |
| 升级回滚 | 手工替换文件 | 改镜像 tag 重跑 | 改 tag 后up -d |
| 多组件协同 | 手工维护 | 手工维护网络 | 一个网络内服务名互访 |
| 配置管理 | 分散 | 命令行参数 | 集中挂载 |
| 排错难度 | 最低 | 中等 | 中等,需熟悉容器网络 |
有一点要提前说清楚:不要把 Prometheus 当成无状态服务来对待。它的 TSDB 是本地磁盘上的块文件,容器删除后数据随之消失。所以卷挂载不是可选项,是必选项。同理,配置文件也别用COPY打进镜像,那等于把配置和版本绑死,改一个抓取间隔就要重新构建镜像,完全没有必要。
2.2 镜像选择与版本策略
镜像我一般用官方prom/prometheus,理由很实际:多架构支持完整、Dockerfile 公开可查、tag 命名规范。tag 有两种用法,latest和固定版本号。监控组件不要用latest,这是我踩过坑之后的硬性要求。原因很直接:Prometheus 大版本之间配置文件语法会有变化,某些抓取相关的字段在升级后被废弃或语义调整,自动拉最新镜像等于把升级时机交给了运气。我的做法是固定到具体的次版本号,比如v2.51.x这种,先在测试环境跑一周,确认promtool check config和查询都正常,再改生产编排文件里的 tag。
镜像本身基于一个精简系统,里面没有 shell 工具链,别指望进容器curl或者vi。这不是缺陷,是安全实践——攻击面小。需要调试时用docker exec进不去就换思路:要么在宿主机上用promtool,要么临时起一个带工具的容器挂同一个网络。我通常的做法是把promtool的校验放进 CI 或启动前脚本,配置错了在启动阶段就拦住,而不是等运行起来发现指标缺失。
2.3 整体架构与目录规划
小规模场景我推荐的结构是这样的:Prometheus 主服务 + node-exporter(采集主机指标)+ 若干业务 exporter,全部放在同一个自定义 bridge 网络里,通过服务名互相访问。Grafana 可以放同一个网络,也可以后加。告警部分先用 Prometheus 自带的规则引擎,需要发通知时再看要不要接 Alertmanager,别一上来就堆满组件。
目录规划我固定成这套,用了很久没改过:
prom-stack/ ├── compose.yaml ├── conf/ │ ├── prometheus.yml │ └── rules/ │ └── node-basic.yml └── data/ └── prometheus/conf只读挂载进容器,data是可写的持久化目录。这样备份策略也很清晰:conf走代码仓库,data走定期快照或者干脆接受丢失(取决于你的监控数据保留要求)。把这两个东西分开,是我认为容器化 Prometheus 最重要的一条设计决定,后面所有操作都围绕它展开。
3. 核心配置逐项拆解:每个参数为什么这么写
3.1 全局配置与采集节奏怎么定
prometheus.yml的第一段是global,这里的三个参数决定了整个系统的“心跳”:
global: scrape_interval: 15s scrape_timeout: 10s evaluation_interval: 15s external_labels: cluster: demo-cluster replica: Ascrape_interval是抓取周期,15 秒是社区里最常见的默认值。为什么不是 5 秒?因为周期越短,写入的样本点越多,磁盘和内存压力线性上升,而大多数业务指标的关注粒度根本用不到秒级。为什么不是 60 秒?告警从发生到触发会多等一个周期,for再叠加,响应就太迟钝了。15 秒是实践经验里比较平衡的点。如果某个业务确实需要高频,可以在单个scrape_configs条目里单独覆盖,不要全局改。
scrape_timeout必须小于scrape_interval,否则上一次还没抓完下一次就开始了,目标会被标记成超时。我一般设成周期的三分之二左右,15 秒周期配 10 秒超时,给慢目标留出余量又不会拖累整体。
evaluation_interval是告警规则的评估周期,和抓取周期保持一致最省心。设得比抓取周期短没有意义——数据都没更新,重复评估只是浪费 CPU。
external_labels很多人会忽略,但它在两种场景下非常关键:多集群数据汇总时用来区分来源,以及后面如果要接远程写入,这些标签会自动附加到所有数据上。建议一开始就加上,哪怕现在只有一个集群,将来合并数据时你会感谢自己。
注意:
external_labels不会覆盖目标自带的标签,它是附加的。如果两边标签名冲突,抓取目标上的标签优先,这一点在排查“为什么标签不对”时经常被忽略。
3.2 抓取目标与 relabel 的实战用法
scrape_configs是配置文件里最长的一段,也是最容易写错的地方。先看基础写法:
scrape_configs: - job_name: prometheus static_configs: - targets: ["localhost:9090"] - job_name: node static_configs: - targets: ["node-exporter:9100"] labels: env: prod role: appjob_name会作为job标签自动附加到该任务的所有指标上,这是后面写查询和告警时的主要过滤维度,命名要克制。我见过把所有 exporter 都叫exporter的配置,最后查指标得靠instance硬猜,非常难受。建议按“采集对象类型”命名,比如node、mysql、app-java,一眼能看出数据来源。
targets里写的是容器网络内的服务名加端口。这里有个新手常犯的错:写成宿主机的localhost:9100。容器里的localhost指容器自己,永远连不上宿主上的 exporter。要么用编排网络里的服务名,要么用host.docker.internal(Linux 下需要在服务里加extra_hosts: ["host.docker.internal:host-gateway"]),要么直接用宿主机内网 IP。我推荐服务名,因为它不依赖宿主网络配置,可移植性最好。
静态配置够用一阵子,但目标多了就得换file_sd_configs,把目标列表放到独立文件里,Prometheus 会自动监听文件变化:
- job_name: file-sd-demo file_sd_configs: - files: - /etc/prometheus/targets/*.json refresh_interval: 30s对应的 JSON 文件内容是一个数组,每项包含targets和labels。这个方式的好处是:加一台机器只要改 JSON,不用碰主配置,也不需要重载 Prometheus。配合配置管理工具自动生成,规模化之后非常省事。
relabel_configs是进阶内容,但有两个场景绕不开:标签清洗和多目标拆分。比如你的 exporter 暴露了一堆用不上的内部标签,可以用labeldrop干掉;再比如一个任务里混了不同环境的目标,可以用relabel按标签分流。规则是从上到下依次执行的,action有replace、keep、drop、labelmap等,写的时候一定要用promtool验证,这些规则的语义很容易想当然。
3.3 存储保留期与资源参数的实际算法
这部分是真正需要算的。三个关键参数:--storage.tsdb.retention.time、--storage.tsdb.retention.size、以及容器资源限制。两者同时设置时,哪个先达到就按哪个执行,所以两个都要给,防止某一种维度失控。
磁盘估算的常用经验公式是:
磁盘占用 ≈ 每秒采集样本数 × 保留秒数 × 每样本字节数Prometheus 压缩后每个样本大约占 1 到 2 字节,取 1.5 做估算比较稳妥。举个具体例子:你有 1000 个活跃时间序列,抓取周期 15 秒,那么每秒样本数是1000 / 15 ≈ 66.7。保留 15 天等于15 × 86400 = 1296000秒。乘起来:66.7 × 1296000 × 1.5 ≈ 1.3 × 10^8字节,约 130 MB。这个数字看起来很小,因为 1000 序列确实是小规模。换成 5 万序列、15 秒周期、保留 30 天:50000 / 15 ≈ 3333样本每秒,乘2592000秒再乘 1.5,大约是 13 GB。
内存的经验值是每百万活跃序列约 2 到 4 GB,这个区间跨度大是因为它高度依赖标签基数、查询复杂度和并发查询数。我的做法是先用保守值起,观察一周的process_resident_memory_bytes再调整。
| 规模 | 活跃序列数 | 抓取周期 | 建议保留 | 磁盘估算 | 容器内存限制 |
|---|---|---|---|---|---|
| 小型 | 1 千 | 15s | 15 天 | 约 130 MB | 512 MB |
| 中型 | 5 万 | 15s | 30 天 | 约 13 GB | 2 GB |
| 较大 | 50 万 | 30s | 30 天 | 约 65 GB | 8 GB |
这张表里的数字只能当起点,真实值必须用实际数据校准。校准方法很简单:跑满一天后看prometheus_tsdb_storage_blocks_bytes,除以天数就能推 30 天的量,比任何公式都准。另外别忘了磁盘要留出至少 30% 余量,TSDB 压缩和块合并时需要额外空间,写满磁盘导致 Prometheus 崩溃是运维事故的常见起因。
4. 动手实操:从零拉起一套可用的监控
4.1 环境准备与目录初始化
先确认宿主机上有 Docker 和 Compose 插件,docker --version和docker compose version都能输出版本号就行。然后建目录,注意data目录的属主问题——这是最容易卡住的地方。
mkdir -p prom-stack/conf/rules prom-stack/data/prometheus cd prom-stack chown -R 65534:65534 data/prometheus chmod 755 conf conf/rules为什么要chown 65534?官方镜像默认以nobody用户运行,UID 就是 65534。如果不改属主,容器启动后会因为无法写入/prometheus直接报permission denied然后退出。这个报错信息很短,新手经常以为是镜像坏了,其实是宿主机目录权限问题。另一种做法是在编排文件里写user: "0:0"用 root 跑,我不推荐,一是没必要,二是违背最小权限原则。
配置文件写好后,先校验再启动。这一步能挡住八成低级错误:
docker run --rm \ -v "$(pwd)/conf:/etc/prometheus" \ prom/prometheus:v2.51.2 \ promtool check config /etc/prometheus/prometheus.yml输出SUCCESS就说明语法、规则引用、字段拼写都没问题。如果提示某个字段不认识,大概率是版本对不上——你用的配置语法比镜像版本新,或者反过来。
4.2 编排文件逐段说明
完整的compose.yaml长这样,我按块解释:
services: prometheus: image: prom/prometheus:v2.51.2 container_name: prometheus restart: unless-stopped ports: - "127.0.0.1:9090:9090" volumes: - ./conf/prometheus.yml:/etc/prometheus/prometheus.yml:ro - ./conf/rules:/etc/prometheus/rules:ro - ./data/prometheus:/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" - "--web.listen-address=0.0.0.0:9090" networks: - monitor healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:9090/-/healthy"] interval: 30s timeout: 5s retries: 3 node-exporter: image: prom/node-exporter:v1.7.0 container_name: node-exporter restart: unless-stopped ports: - "127.0.0.1:9100:9100" networks: - monitor networks: monitor: driver: bridge几个点值得单独说。端口映射写了127.0.0.1:前缀,意思是只监听本机回环,外部访问不了。监控数据里往往包含主机名、内部 IP、业务指标,直接暴露到公网是纯粹的风险。需要远程查看时走内网访问或者前置一层反向代理做认证,别图省事写成0.0.0.0。
--web.enable-lifecycle这个参数开启后,才能用 HTTP 请求触发热重载,改完配置不用重启容器:
curl -X POST http://127.0.0.1:9090/-/reload注意它只重载配置文件和规则文件,命令行参数改了必须重启。
两个 retention 参数同时设置是有意为之。time控制逻辑生命周期,size是磁盘安全的兜底。我曾经遇到过一个标签基数爆炸的案例,序列数在一周内涨了十倍,如果只设时间不设大小,磁盘会在保留期之前就被写满。
healthcheck用的wget --spider做探活,探的是/-/healthy端点。这个端点只表示进程活着,不表示能正常抓取。要判断是否真的在干活,得看/-/ready或者直接查up指标。
4.3 启动验证:三个层次确认服务可用
启动分两步,先起 exporter 再起 Prometheus,虽然 Compose 网络能自动解析服务名,但这样日志更清晰:
docker compose up -d node-exporter docker compose up -d prometheus docker compose logs -f prometheus日志里看到Server is ready to receive web requests就说明起来了。验证要分三层做,别只做第一层:
第一层,进程存活。访问http://127.0.0.1:9090能看到界面,或者命令行curl -s http://127.0.0.1:9090/-/healthy返回Prometheus Server is Healthy.。
第二层,抓取正常。打开界面上的 Status → Targets,两个任务都应该是UP状态。如果node显示DOWN,把鼠标放在错误信息上看具体原因,常见的是connection refused(目标没起来或端口错)和context deadline exceeded(超时,网络不通或目标太慢)。
第三层,数据真的进来了。在查询框输入up,应该能看到两条时间序列,值为 1。再查node_memory_MemAvailable_bytes,如果能出数,说明整条链路通了:exporter 暴露指标、Prometheus 抓取、写入 TSDB、查询返回。三层都过,这套监控才算真的可用。
4.4 接入告警规则并验证触发
告警规则单独放文件,用rule_files引进来。先写一个最基础、但确实有用的规则:
groups: - name: node-basic interval: 30s rules: - alert: InstanceDown expr: up == 0 for: 2m labels: severity: warning annotations: summary: "实例 {{ $labels.instance }} 抓取失败" description: "job={{ $labels.job }} 已持续 2 分钟无法抓取,请检查目标进程与网络。" - alert: HighMemoryUsage expr: | (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) > 0.9 for: 10m labels: severity: critical annotations: summary: "主机内存使用率超过 90%"for字段的作用是抑制抖动,up == 0持续两分钟才真正触发,避免一次网络抖动就报警。annotations里用{{ $labels.xxx }}模板变量把具体实例名带进消息,否则收到告警还得自己去查是哪台机器,效率很低。
规则写完后同样先校验:
docker run --rm -v "$(pwd)/conf:/etc/prometheus" \ prom/prometheus:v2.51.2 \ promtool check rules /etc/prometheus/rules/node-basic.yml验证规则真的能触发,我有个笨但有效的办法:故意把node-exporter停掉,等两分钟,然后看界面上 Alerts 页面是否出现InstanceDown处于pending再转firing。确认无误后把 exporter 拉起来,告警自动恢复。没验证过的告警规则等于没写,这一点在真实故障时会被无限放大。
5. 常见问题与排查技巧实录
5.1 高频报错速查
下面这些是我在实际操作中反复遇到的,按出现频率排序:
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| 容器启动后立刻退出 | 配置文件语法错误 | 用promtool check config校验,看docker compose logs |
| 日志报 permission denied | data 目录属主不对 | chown -R 65534:65534 data/prometheus |
| Targets 页面显示 DOWN | 目标未启动/端口错/网络不通 | 进 Prometheus 容器测试连通性,确认服务名与端口 |
| 查询报 out of bounds | 宿主机时间偏移或数据目录混用 | 校准宿主机时间,检查是否挂载了旧数据目录 |
| 磁盘持续增长不回落 | 标签基数爆炸 | 查看prometheus_tsdb_head_series,排查高基数标签 |
| 改配置不生效 | 未触发重载或语法被忽略 | curl -X POST .../-/reload,确认返回 200 |
| 界面 404 访问不到 | 端口映射到 127.0.0.1 | 从本机访问或调整映射后做好访问控制 |
out of bounds这个错误值得多说一句。它的本质是写入的时间戳超出了 TSDB 允许的范围,最常见的原因是宿主机时间被改过,或者你把一份旧的数据目录挂到了新容器上,而新容器的时间比块文件里的时间还早。处理办法是先确认时间同步正常,再检查数据目录是不是复用错了。这个错误不会自动恢复,往往需要清理冲突的块。
5.2 抓取目标 DOWN 的分层排查法
遇到目标 DOWN,我固定按四层往下查,一层层排除,不要跳步:
第一层,目标本身活着吗。在宿主机上直接访问目标端口,比如curl -s http://127.0.0.1:9100/metrics | head。如果这里就出不来数据,问题在 exporter,跟 Prometheus 无关,先修 exporter。
第二层,网络通不通。从 Prometheus 容器内部去访问目标,docker exec prometheus wget -qO- http://node-exporter:9100/metrics。这一步能区分是“容器网络解析问题”还是“目标问题”。如果容器里访问不了但宿主机能访问,八成是服务名写错或者两个容器不在同一个网络里。
第三层,Prometheus 的配置对不对。看 Targets 页面上的实际抓取地址,有时候relabel把地址改坏了,页面上显示的和你在配置里写的不一样。也顺便看一下Last Scrape的时间,如果一直不更新,说明任务压根没被调度。
第四层,看错误类型。connection refused是端口没开或进程挂了;context deadline exceeded是能连上但响应太慢,可能目标负载高或者 exporter 的采集逻辑卡住了;403或401是认证问题;server returned HTTP status 404是路径不对,很多 exporter 的指标路径不是默认的/metrics。
这个分层法的价值在于:它能让你在五分钟内定位到问题归属,而不是在 Prometheus 的各种配置里瞎改。
5.3 容器化特有的几个坑
时间问题。Prometheus 对时间极其敏感,容器默认继承宿主机时钟。如果宿主机没有做时间同步,长期运行会累积偏移,轻则时间戳不准,重则触发out of bounds。我的做法是把时间同步作为宿主机的基础要求写进部署清单,而不是在容器里折腾。
文件描述符限制。抓取目标多的时候,并发连接数会上去。容器默认的 fd 限制可能不够,表现是日志里出现too many open files。在编排文件里加ulimits配置可以解决,具体值按目标数量估,一千个目标给到 65536 比较稳妥。
日志无限增长。默认日志驱动会把容器日志写到宿主机磁盘,Prometheus 的日志量不大但也架不住长期累积。建议在编排文件里限制日志大小,比如logging段配置max-size: 10m和max-file: 3,避免哪天磁盘被日志写满。
数据目录的备份姿势。直接cp正在运行的 TSDB 目录是不安全的,可能拷到写了一半的块。正确做法是先调 Prometheus 的快照接口(需要开启管理 API),或者停容器再拷。如果对数据完整性要求没那么高,我更倾向于接受“监控数据可丢”,把精力花在配置和规则的版本管理上,收益更高。
时区与展示。Prometheus 内部统一用 UTC 存储,界面上按浏览器时区展示。写告警描述时不要在文本里硬编码时区,容易误导。这件事不影响功能,但会影响值班同学判断时间,属于体验层面的细节。
6. 从能跑到好用:落地时的几个关键决定
6.1 数据持久化与容量管理的实际取舍
很多人纠结监控数据要不要长期保留。我的观点是:先明确你用它干什么。如果只是看最近一周的趋势和排障,15 天足够;如果要做容量规划、同比分析,那就需要更长的保留期,同时得考虑降采样或者远程存储,单机 TSDB 硬扛长期数据不划算。
容量管理上我有个习惯:给data目录单独挂一块盘或者单独的分区,不要和系统盘混在一起。原因很现实——Prometheus 写满磁盘会拖垮整台机器上的其他服务,隔离之后最坏情况只是监控自己挂掉。同时在编排里加上retention.size作为硬约束,双保险。
数据卷的权限和属主前面说过,这里补一个细节:如果你用docker compose down再up,数据目录会保留,因为它是 bind mount 到宿主机的。但如果手滑写了down -v,具名卷会被删掉,bind mount 则不受影响。这也是我倾向 bind mount 的原因之一,误操作的成本低一些。
6.2 资源限制怎么设才不背锅
容器资源限制不是设得越紧越好。Prometheus 在启动时要做 WAL 回放,在块压缩时会短时间占用大量内存和 IO,限制太紧会导致 OOM Kill 或者压缩失败。我的经验是:内存限制按估算值的 1.5 到 2 倍给,CPU 限制可以宽松一些,因为它是突发型负载。
deploy: resources: limits: memory: 2G cpus: "2.0" reservations: memory: 512M注意deploy.resources在非 Swarm 模式下,docker compose新版本是支持的,但老版本可能忽略。如果你的环境不生效,换成mem_limit和cpus这种顶层写法。这个差异坑过不少人——配置写了以为生效了,实际压根没限制。
还有一个容易被忽略的点:不要把 Prometheus 和其他重负载服务放在同一台机器上。它的 IO 模式是持续小写入加周期性大块合并,和数据库这类服务抢磁盘会很惨。条件允许就独立部署,条件不允许至少给它独立的磁盘或者限制 IO。
6.3 后续扩展的几条路径
跑通单机之后,往下走通常有三个方向。第一是补可视化,加一个 Grafana 容器,数据源指向 Prometheus,用现成的看板模板起步,别自己从零画。第二是补告警通道,规则在 Prometheus 里评估,通知交给 Alertmanager,配置分组、抑制、静默,避免告警风暴。第三是规模上去之后的方案,目标多了用服务发现替代静态配置,数据量大了考虑远程写入或者分片,但这些都属于另一个量级的问题,不要过早引入。
我个人的建议是:先把单机这套跑稳三个月,把抓取目标、告警规则、容量曲线都摸清楚,再考虑加组件。监控系统本身也是需要被监控的,prometheus_tsdb_head_series、prometheus_target_scrape_pool_targets、process_resident_memory_bytes这几个指标,从第一天就该加到看板上,它们会在出问题时第一时间告诉你。
提示:每次改完配置,养成“校验、重载、看 Targets、查一个指标”的四步习惯,比出事之后再回滚要省事得多。