Prometheus监控Docker容器:cAdvisor部署与Grafana可视化实践
1. 为什么监控层面要单独给Docker容器开一个“分战场”先把场景还原一下。你大概率已经用Prometheus把宿主机层面的CPU、内存、磁盘、网络都收上来了Node Exporter跑得很欢Grafana面板上也确实能看到整个集群的负载趋势。但等到业务真正容器化之后你会发现宿主机那一套指标根本不够用——你看到了“这台机器CPU 80%”却说不清是哪个容器在抢你看到“内存快满了”却不知道是该扩哪个服务的副本你想给某个容器的异常退出配告警Node Exporter根本提供不了这个维度。这就是监控Docker容器的核心动机容器是一种比进程更粗、比宿主机更细的调度单元必须有对应的中间层监控。而Prometheus体系里Docker容器的监控并不是靠Node Exporter完成的它需要一套专门的数据采集器把Docker Daemon暴露出来的容器运行时数据拉出来再转换成Prometheus能识别的指标格式。这套数据采集器主流方案就是cAdvisor再加上Grafana做可视化整套链路就完整了。这篇文章是“PrometheusGrafana构建云原生分布式监控系统”系列的第八篇专门讲Docker容器监控的落地。整体思路围绕几件事展开用什么采集器、采集哪些指标、怎么接入Prometheus、Grafana怎么配面板、告警规则怎么写、以及我在实际部署中踩过的坑。假设读者已经有一个能用的Prometheus和Grafana环境还没有的话可以顺着系列前面的文章把基础搭起来这一篇的实操部分默认基础环境已在运行。在动手之前需要明确一个边界问题Docker容器监控和Kubernetes集群监控是两码事。如果你的环境是K8s那应该用kubelet内置的cAdvisor kube-state-metrics那套组合如果是裸Docker或者Docker Compose部署直接用cAdvisor来覆盖所有容器即可。这篇文章的场景设定是后者——裸Docker环境因为很多中小团队和测试环境其实根本还没上K8s但容器化已经开始了。2. 容器监控的数据来源cAdvisor和Docker Daemon之间的关系要理解cAdvisor是干什么的得先知道Docker容器运行时暴露数据的几种途径。第一种是Docker Daemon本身的metrics接口。Docker从1.11版本开始内置了一个实验性的metrics端点开启之后Daemon会把容器数量、镜像数量、网络收发等自身运行状态暴露出来。但这个指标面比较窄主要是Docker服务自身的健康度不是容器维度的资源消耗数据。第二种是Linux内核的cgroup数据。每个容器在宿主机上对应一组cgroup目录里面以伪文件的形式记录了CPU、内存、PID、IO等真实消耗。Docker Daemon自身不主动暴露这些数据给监控系统需要有一个组件去读取并转换。第三种就是cAdvisor干的活。cAdvisor全称Container Advisor是Google开源的一个容器资源分析工具。它本质上是一个sidecar式的数据采集进程运行在宿主机上读取cgroup、/proc、/sys等内核接口结合Docker Socket里获取的容器元数据把两者关联起来然后通过HTTP端点对外提供Prometheus格式的指标。举一个直观的例子一个容器在cgroup里的CPU统计是一个累计计数器cAdvisor把它读出来后转成container_cpu_usage_seconds_total这样的Prometheus指标再打上name、image、id这些跟容器元数据相关的标签。这样一来Prometheus的rate()函数才能对计数器做速率计算告诉你这个容器每秒实际耗了多少CPU时间。没有cAdvisor这一层转换原生cgroup数据根本没法直接用PromQL做时间序列分析。cAdvisor的架构很轻它本身是一个Go写的单二进制程序以容器方式部署的时候需要挂载宿主机的/、/var/run、/sys、/var/lib/docker等目录。它的工作模式是主动拉取Docker Socket 被动读取cgroup文件所以要求它必须跑在宿主机上不能像普通业务容器一样扔到远端——这一点很多人都搞错过把cAdvisor容器部署在一台机器上去监控另一台机器结果指标全是空的。另一个关键点是cAdvisor的指标粒度。它默认暴露两类指标一类是容器级指标前缀是container_比如container_cpu_usage_seconds_total、container_memory_usage_bytes、container_network_receive_bytes_total另一类是宿主机级指标前缀是machine_比如machine_cpu_cores、machine_memory_bytes。在裸Docker环境里cAdvisor可以同时替代一部分Node Exporter的职责但对于磁盘容量这类块设备指标Node Exporter的信息更完整实际部署时建议两者并存各管一段。在设计采集架构时我倾向于把cAdvisor看作Docker环境的“容器侧Node Exporter”。它的部署密度是每台运行Docker的宿主机一个通过Prometheus的静态服务发现或者Consul这类注册中心进行target管理。如果业务量不大全部塞进一个Prometheus实例即可如果宿主机数量多就按机房或业务域拆分多个Prometheus实例cAdvisor接入各自的采集任务。3. cAdvisor的落地部署Docker方式启动与Prometheus抓取配置3.1 用Docker方式启动cAdvisor的完整命令裸Docker环境下最常见的部署方式就是让cAdvisor自己也是一个容器。下面是我在生成环境里用的一版启动命令参数逐一拆解给你看不建议直接照抄然后什么都不管每个参数都有它存在的理由。docker run -d \ --namecadvisor \ --restartalways \ --publish8080:8080 \ --device/dev/kmsg \ --volume/:/rootfs:ro \ --volume/var/run:/var/run:ro \ --volume/sys:/sys:ro \ --volume/var/lib/docker/:/var/lib/docker:ro \ --volume/cgroup:/cgroup:ro \ gcr.io/cadvisor/cadvisor:latest几个关键挂载点的作用/:/rootfs:ro让cAdvisor能读取宿主机根文件系统用于统计各容器在宿主机的存储占用情况。注意这个挂载是只读的非常安全不会改动宿主机任何文件。/var/run:/var/run:ro让cAdvisor能访问Docker Socket。Docker Socket是一个Unix Domain SocketcAdvisor通过它向Docker Daemon查询容器列表、镜像信息、启动参数等元数据。/sys:/sys:rocgroup v1/v2的文件系统挂载点需要通过/sys访问cgroup子系统的统计信息。/var/lib/docker/:/var/lib/docker:roDocker的数据目录用于读取镜像分层和容器文件系统的空间占用。如果容器使用了overlay2存储驱动这个目录能提供容器可写层的占用量。/cgroup:/cgroup:ro有些Linux发行版比如系统使用cgroup v1会把cgroup挂载在/cgroup下挂上这个目录可以兼容不同发行版。--device/dev/kmsg是很多人容易漏掉的参数。在较新的内核上cAdvisor需要读取内核日志来确认一些系统事件如果不挂载这个设备部分版本的cAdvisor会报Failed to start container manager: unable to create cgroup path之类的错误。启动之后可以用curl http://localhost:8080/metrics验证cAdvisor是否正常工作。输出里应该能看到大量container_cpu_usage_seconds_total开头的指标行。如果你的环境网络访问Docker Hub没问题直接把镜像拉下来跑就行如果服务器在数据中心内部需要先把镜像推到内网仓库再拉取。3.2 Prometheus采集任务配置cAdvisor起来之后接下来就是让Prometheus去拉取指标。在Prometheus的配置文件prometheus.yml的scrape_configs段下新增一个job这里我给出两个示例一个基于静态配置一个基于文件发现。静态配置方式适用于机器数量固定、IP不会漂移的小规模环境scrape_configs: - job_name: docker-cadvisor static_configs: - targets: - 192.168.1.10:8080 # 宿主机A的cAdvisor - 192.168.1.11:8080 # 宿主机B的cAdvisor labels: env: prod region: cn-east-1基于文件发现的方式适用于需要后续自动化扩缩容的场景。先把节点IP写进一个独立文件cadvisor_targets.yml然后在prometheus.yml里引用这个文件scrape_configs: - job_name: docker-cadvisor file_sd_configs: - files: - /etc/prometheus/cadvisor_targets.yml refresh_interval: 60scadvisor_targets.yml的内容格式- targets: - 192.168.1.10:8080 - 192.168.1.11:8080 labels: env: prod无论用哪种发现方式最终目的都是让Prometheus每15秒默认scrape_interval向每一台宿主机的cAdvisor端点发起一次HTTP请求。采集频率不需要调得太快15秒对容器监控来说完全足够因为cAdvisor内部缓存本身就有一定延迟调成5秒并不会提高指标精确度反而会增加Prometheus和cAdvisor双方的压力。检查采集是否成功可以在Prometheus的“Targets”页面看到这个job下的endpoint状态。如果显示UP说明抓取正常如果显示DOWN优先检查防火墙是否放行了8080端口以及cAdvisor容器是否在正常运行。另外建议在prometheus.yml里对cadvisor加上honor_labels: true因为cAdvisor的指标里已经包含了instance标签默认是宿主机IP:8080如果Prometheus再做一次标签覆写会导致Grafana面板上无法正确区分不同宿主机。3.3 采集一下Docker Daemon自身的运行指标如果说cAdvisor是监控“容器内资源”那么Docker Daemon的运行指标就是监控“容器调度器本身”。这个选项不是必须的但确实有它的价值——比如容器启动非常慢、镜像拉取频繁超时这类问题单看cAdvisor没法定位需要看Daemon自己的状态。开启Docker Daemon metrics的方式是在Docker配置文件/etc/docker/daemon.json里加两个字段{ metrics-addr: 0.0.0.0:9323, experimental: true }然后重载Docker服务sudo systemctl reload docker验证方式curl http://localhost:9323/metrics这个端点暴露的指标包括engine_daemon_container_states_containers按状态统计的容器数量、engine_daemon_engine_infoDocker版本信息、builder_*镜像构建相关指标等。然后在Prometheus里再加一个job来抓取scrape_configs: - job_name: docker-daemon static_configs: - targets: - 192.168.1.10:9323注意开启experimental字段在Docker长期支持版本上是安全的不会影响正常功能但如果你所在企业有严格的安全合规要求建议先确认一下是否允许开启experimental特性。如果不想开启experimentalDocker也提供了metrics-addr单独配置的方式但不同版本的Docker对参数的支持程度有差异最稳妥的做法就是按上面两个字段一起配。4. Grafana侧的可视化数据源接入、仪表盘选择与关键面板改造4.1 接入Prometheus数据源这一步属于基础配置简单提一下。打开Grafana的“Configuration” - “Data Sources”点击“Add data source”选择Prometheus在“URL”里填Prometheus服务的访问地址比如http://192.168.1.100:9090点击“Save Test”显示绿色提示即可。要注意的是如果Grafana和Prometheus都跑在Docker里这里填的URL不能是localhost或127.0.0.1因为容器之间的网络是隔离的。推荐的做法是使用Docker Compose时把两个服务放在同一个自定义网络里然后数据源地址填服务名比如http://prometheus:9090如果是跨宿主机就填Prometheus所在宿主机的内网IP。4.2 选对仪表盘模板14282和196是怎么用的Grafana社区里有现成的cAdvisor仪表盘模板不需要从零开始画面板这是效率最高的一条路。我实际用过两个比较主流的ID: 14282名字叫“Cadvisor exporter”这个模板的面板布局非常紧凑覆盖了CPU使用率、内存使用量、网络收发速率、磁盘读写速率等核心指标适合直接观察单台宿主机上的容器资源消耗。ID: 196名字叫“Docker monitoring”这个模板更老牌当年适配的是cAdvisor早期的指标格式部分面板到现在还能用但会有一部分面板因为指标名变更而显示空数据。导入方式Grafana左侧“Dashboards” - “Import”输入模板ID选择Prometheus数据源点击Import即可。这里有一个坑导入之后大概率会有几个面板是空的不要慌原因通常是模板里引用的指标名和当前cAdvisor版本暴露的指标名不一致或者模板里用了$host之类的变量而你的label命名跟模板不匹配。解决方式是逐个空面板检查它的查询语句改成实际存在的指标名。4.3 按需改造面板以“容器内存使用量”和“网络速率”两个面板为例我拿两个最常见的面板改造场景来说明让你感受一下改查询语句的思路。场景一容器内存使用量。很多模板默认的查询语句是container_memory_usage_bytes{name~.}这个指标反映的是容器当前RSS内存加缓存的总量但它会包含page cache导致显示值偏高。业务方问“为什么容器内存监控显示500MB容器内看free才用了200MB”原因就在这。更贴近实际内存占用的指标是container_memory_working_set_bytes它排除了不活跃的缓存部分container_memory_working_set_bytes{name~.}建议在面板上同时保留两个查询一个用container_memory_usage_bytes显示总量一个用container_memory_working_set_bytes显示实际活跃内存这样排查内存问题的时候能清楚知道缓存占了多少。场景二网络收发速率。常见模板写法rate(container_network_receive_bytes_total[1m])这个没问题。但需要注意cAdvisor的容器网络指标包含所有网络接口其中eth0是业务流量veth*是容器与宿主机之间的虚拟网卡lo是回环。如果业务方看到“网卡流量大”但实际面板上可能汇总了多个接口导致数字虚高。建议加一个过滤条件只统计eth0rate(container_network_receive_bytes_total{name~., interfaceeth0}[1m])在Grafana面板的查询编辑里把interfaceeth0加进去刷新一下图表就能反映出真实业务网卡流量。这个细节在排查“容器带宽打满”问题的时候非常有用。4.4 面板排版建议不加筛选条件的仪表盘没有灵魂导入模板之后最值得做的改造是加“下拉筛选变量”。Grafana支持在仪表盘级别定义Template变量然后在面板查询里引用。我通常会加两个变量host基于label_values(container_cpu_usage_seconds_total, instance)生成用于选择宿主机。container基于label_values(container_cpu_usage_seconds_total{instance~$host}, name)生成用于选择具体容器。这样在做业务容器排障时可以快速在仪表盘顶部切到目标宿主机和容器而不是在全量图表里手工找那条曲线。注意name标签在有Docker Compose项目的情况下可能会被命名为/docker-compose名_服务名_序号筛选时需要用正则去匹配。比如你想筛选某个Compose项目下的所有容器可以写成/myapp_.*。5. 告警规则怎么写从容器“状态类”到“资源类”的全套告警示例监控不只是“看到”更重要的是“提前知道”。这一部分我直接给出几组可复用的告警规则全部基于Prometheus内置的Alertmanager规则语法。在写规则之前先明确Docker容器告警的几个核心维度按优先级排序大概是容器是否存活、容器是否频繁重启、CPU是否长期打满、内存是否即将耗尽、磁盘是否有写满风险、网络流量是否异常。5.1 容器存活与重启状态告警第一个规则容器退出。groups: - name: docker-container-status rules: - alert: ContainerDown expr: time() - container_last_seen{name~.} 60 for: 1m labels: severity: critical annotations: summary: 容器 {{ $labels.name }} 已经停止上报指标超过60秒 description: 容器 {{ $labels.name }} 在宿主机 {{ $labels.instance }} 上可能已停止或异常退出。container_last_seen表示cAdvisor最后一次看到该容器的时间戳。如果容器正常退出停止上报这个时间差会持续增长超过阈值就触发告警。注意这个规则对“主动停止的容器”也会告警如果你有一个容器是按需启动、跑完就退的批处理任务需要单独排除方法是在表达式里加正则过滤把任务型容器排除在外。第二个规则容器频繁重启。- alert: ContainerRestarting expr: changes(container_start_time_seconds{name~.}[15m]) 3 for: 5m labels: severity: warning annotations: summary: 容器 {{ $labels.name }} 在15分钟内重启超过3次 description: 容器 {{ $labels.name }} 近期出现频繁重启请检查应用崩溃或健康检查失败的原因。container_start_time_seconds是容器启动时刻的时间戳changes()函数统计它在15分钟窗口内变化了几次。如果超过3次说明容器在反复退出拉起通常意味着应用自己有bug或健康检查探针配置不对。5.2 CPU、内存与磁盘容量告警第三个规则CPU使用率过高。- name: docker-container-resource rules: - alert: ContainerHighCPU expr: sum(rate(container_cpu_usage_seconds_total{name~., container!POD}[5m])) by (name, instance) / on(instance) sum(machine_cpu_cores) by (instance) 0.8 for: 10m labels: severity: warning annotations: summary: 容器 {{ $labels.name }} CPU使用率超过80% description: 容器 {{ $labels.name }} 在宿主机 {{ $labels.instance }} 上已持续10分钟CPU占用超过80%。这里面的关键点在于分母。container_cpu_usage_seconds_total是容器消耗的CPU时间除以5分钟窗口的rate得到的是“消耗了多少个CPU核心的算力”。如果宿主机有8个核一个容器用了4核那么结果是0.5而不是50%。所以要除以machine_cpu_cores把结果归一化成百分比。这个地方不除以机器总核数告警阈值就会完全失真——一个4核容器在8核机器上跑满显示值是0.5而不是100%。第四个规则内存即将耗尽。- alert: ContainerMemoryNearLimit expr: container_memory_working_set_bytes{name~.} / (container_spec_memory_limit_bytes{name~.} 0) 0.85 for: 5m labels: severity: warning annotations: summary: 容器 {{ $labels.name }} 内存使用超过限额的85% description: 容器 {{ $labels.name }} 当前工作集内存使用已达限额的{{ $value | humanizePercentage }}。这里用container_spec_memory_limit_bytes作为分母它是容器的内存Limit值。没有设置Limit的容器这个指标值会是0表达式里 0就是排除这类容器。注意如果容器没有内存限制建议结合宿主机内存总量来算避免一个容器把整台机器吃垮。第五个规则容器可写层磁盘占用过高。- alert: ContainerDiskFill expr: container_fs_usage_bytes{name~.} / (container_fs_limit_bytes{name~.} 0) 0.85 for: 5m labels: severity: warning annotations: summary: 容器 {{ $labels.name }} 磁盘使用超过容量的85% description: 容器 {{ $labels.name }} 的可写层磁盘使用已超过85%请检查是否有大量日志写入或临时文件堆积。container_fs_usage_bytes和container_fs_limit_bytes来自cAdvisor对Docker overlay2存储驱动的统计。容器持续写日志、或者在容器里写临时大文件都会让这个数值不断上涨。5.3 告警通知要落地Alertmanager基础配置规则文件写好后需要在Prometheus主配置里引用rule_files: - /etc/prometheus/rules/*.yml重启Prometheus后在“Alerts”页面能看到规则加载。真正让告警发出去需要一个已经配置好的Alertmanager。Alertmanager的配置这里不展开全流程只给一个关键的webhook配置示意方便对接钉钉或企业微信机器人route: receiver: default group_by: [alertname, name] receivers: - name: default webhook_configs: - url: http://127.0.0.1:9100/alertgroup_by设置很重要它决定了告警分组的方式。Docker环境下我建议按[alertname, name]分组也就是“同一容器的同类告警合并成一条”避免一个容器内存持续超阈值每分钟刷屏几十条告警。不加分组的话Alertmanager默认可以做到按告警名称聚合但加上按容器名称分组负责业务的人能更直观地看到“我的xx服务出问题了”而不是在一堆告警里大海捞针。6. 从零到一的踩坑清单裸Docker环境下监控容器最常见的五个问题这个部分是我在实际部署和后期维护中反复碰到的问题如果你在配置过程中遇到相似现象可以直接对照排查。第一个坑cAdvisor容器起不来报Failed to create manager。最常见的原因是宿主机内核的cgroup版本和挂载路径不匹配。解决办法是按内核情况补充挂载参数如果你的系统用cgroup v1把/cgroup:/cgroup:ro这个挂载加上如果是cgroup v2CentOS Stream 9、Ubuntu 22.04之后的新内核默认一般不需要这个参数但需要确认/sys/fs/cgroup被正确挂载。另外部分云厂商的虚拟机内核里cgroup的挂载方式和标准发行版不一样遇到报错时先用mount | grep cgroup看一下实际挂载路径再对着调整。第二个坑Grafana仪表盘上“容器名称”显示为一大堆乱码路径比如/docker/abc123...而不是服务名。这是因为cAdvisor的name标签取自Docker容器ID只有配合Docker Socket查询元数据后才能映射成容器名。如果cAdvisor容器在启动时没有挂载/var/run目录它就获取不到容器名只能用ID兜底。解决方案就是确保--volume/var/run:/var/run:ro这个参数的存在然后重启cAdvisor。第三个坑Prometheus scrape指标太多导致资源占用飙升。cAdvisor默认暴露的指标非常多每台宿主机上有几十个容器时一个target就能pull回来上万条时间序列。如果Prometheus是单机部署而且内存有限可以在cAdvisor启动参数里裁剪指标集比如用--docker_onlytrue只采集Docker容器的指标过滤掉系统级容器。也可以在Prometheus的采集配置里用metric_relabel_configs把不需要的指标丢弃保留container_cpu_*、container_memory_*、container_network_*、container_fs_*和container_start_time_seconds这几个核心指标族。第四个坑告警规则的for参数设置不合理导致误报和漏报并存。for: 5m意味着指标必须连续5分钟满足阈值才触发告警。对CPU告警来说5分钟的持续期能有效过滤瞬时峰值但对内存告警来说如果容器是“每6分钟内存涨一次每次持续4分钟”5分钟的for就会漏掉这个周期性问题。更合理的做法是分场景设置CPU用10分钟内存用2-3分钟磁盘用15分钟重启检测用5分钟。告警不是越灵敏越好而是要在“及时发现问题”和“避免噪声疲劳”之间找平衡。第五个坑Docker Daemon容器分布在多台宿主机但Grafana图上的instance标签显示混乱。如果你同时用Prometheus抓取了两个不同宿主机的cAdvisor且这两个目标返回的instance标签恰好都是“宿主机IP:8080”Grafana面板就会出现曲线重叠。解决方法是在Prometheus的scrape配置里加relabel_configs为不同宿主机的target打上明确的hostname标签scrape_configs: - job_name: docker-cadvisor static_configs: - targets: [192.168.1.10:8080] labels: hostname: docker-host-01 - targets: [192.168.1.11:8080] labels: hostname: docker-host-02然后在Grafana模板变量里改用hostname而非instance做筛选。这个小改动在宿主机数量变多之后能省掉很多“这个曲线是哪台机器”的疑惑。7. 监控数据的长期价值从“看指标”到“用指标做容量规划”容器监控搭好之后不只是用来开告警、画好看的大屏。如果你坚持采集3个月以上这些数据会变成容量规划和成本分析的重要依据。举个例子我们曾经在排查一个线上服务频繁OOM时发现它配置的内存Limit是2GB但通过container_memory_working_set_bytes的历史趋势看到它平时的真实使用只有800MB只是每隔一段时间会因为GC前的对象积压冲到1.9GB。如果只依赖“当前是否超过限额”这种瞬时判断根本找不到问题但有了历史曲线就能看到整个周期的增长节奏然后针对性调整JVM参数。另一个场景是容器副本数和单副本资源配比。你可以在Prometheus里用下面这条查询按镜像维度聚合出平均每个容器实际消耗的CPU和内存辅助判断“分配1核心512MB是否合理”avg by (image) (rate(container_cpu_usage_seconds_total{name~.}[24h]))avg by (image) (container_memory_working_set_bytes{name~.})这两条查询能直观地展示同一镜像的容器在一天内的平均资源消耗。在每周业务复盘的时候我会对照这个数据和实际的Limit配置找出“申请了4核但实际只用0.2核”的资源浪费容器然后把配额降下来省出来的资源分配给真正有压力的服务。在这个基础上还能用Grafana的“Alerting” - “Contact points”接入邮件或IM通知让容器异常直接推送到团队的聊天群。监控系统真正发挥价值靠的不是某一块面板有多好看而是能否在业务方还没感知到问题之前先把异常信号递到对的人手里。我在实际运维中的体感是Docker容器的监控和数据采集链路本身搭建起来并不难半天就能跑通难点在于后续的告警粒度调优、面板改造和指标理解——这三件事需要结合自己业务的特点去持续打磨。如果你是从这一篇才开始接触容器监控不要急着把告警规则一次配满先让cAdvisor Grafana跑两到三天摸清自己容器指标的“正常范围”再上告警阈值才不容易拍脑袋。等这一整套稳定下来再考虑往K8s迁移的时候你会发现自己对容器运行时指标的理解已经足够平滑过渡到kubelet那套采集体系了。