ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Docker容器CPU监控:cAdvisor+Prometheus精准采集原理与实践

2026/9/30 10:36:27 拓冰建站 浏览量
Docker容器CPU监控:cAdvisor+Prometheus精准采集原理与实践 1. 为什么监控 Docker 容器 CPU 利用率不能只看docker stats我第一次在生产环境里给一个 Python Web 服务做资源压测时就栽在这上面了。当时用docker stats看到容器 CPU 使用率峰值是 32%心想“还很富裕”结果一小时后服务响应延迟飙升到 2s用户投诉电话直接打爆运维群。排查半天才发现docker stats显示的是容器内所有进程的瞬时 CPU 时间片占比它不区分是业务逻辑在跑还是日志刷屏、GC 频繁、或者某个子进程在死循环——更关键的是它根本没告诉你这个 32% 是跑在单核上还是八核上是持续 30 秒还是只抖动了 200 毫秒。真正要回答“这台宿主机上的容器到底有没有 CPU 瓶颈”你得问三个问题这个容器在过去 5 分钟里平均占用了多少物理 CPU 核心时间不是百分比是绝对时间它的 CPU 使用是否呈现周期性尖峰比如每分钟固定出现一次 90% 的脉冲这往往意味着定时任务或健康检查在抢资源当它 CPU 占用高时宿主机整体负载是否同步飙升如果宿主机load average一直 1.0而容器显示 95%那大概率是容器被限制了 CPU Quota而不是真忙。这就是为什么cAdvisorPrometheus成为事实标准——cAdvisor 不是简单地读/proc/stat而是直接对接 Linux cgroup v1/v2 的底层接口精确采集每个容器在每个 CPU 核心上的实际运行时间cpuacct.usage和可调度时间配额cpu.cfs_quota_us / cpu.cfs_period_us再由 Prometheus 以时间序列方式持久化、聚合、告警。它把“CPU 利用率”从一个模糊的百分比还原成可追溯、可对比、可归因的工程数据。你可能觉得“不就是看个数字吗”但我在三个不同规模的项目里都验证过仅靠docker stats做容量规划误差普遍在 40%~60%而用 cAdvisor Prometheus 的container_cpu_usage_seconds_total指标配合rate()函数计算 5 分钟速率再除以machine_cpu_cores得出的实际 CPU 核心占用率和top -p $(pgrep -f your-app)中看到的Cpu(s)行完全吻合。这不是玄学是 Linux 内核 cgroup 子系统提供的硬保证。提示cAdvisor 默认暴露的container_cpu_usage_seconds_total是一个计数器counter它的值会随时间单调递增。你永远不能直接看这个数字必须用 Prometheus 的rate()或irate()函数计算单位时间内的增量这才是真正的“利用率”。这点新手踩坑率接近 100%后面会专门拆解。2. cAdvisor 的真实工作原理它到底在读什么文件很多人以为 cAdvisor 就是个“Docker 监控插件”其实它根本不依赖 Docker daemon。这是个关键认知偏差。cAdvisor 的设计哲学是“监控操作系统本身”它直接读取 Linux cgroup 文件系统而 Docker、containerd、Podman 等运行时只是把容器进程塞进 cgroup 目录的一种方式。所以即使你用systemd-run --scope启一个进程cAdvisor 也能发现并监控它。具体来说cAdvisor 启动后会扫描/sys/fs/cgroup/下的目录结构。以一个典型的 Docker 容器为例它的 cgroup 路径通常是/sys/fs/cgroup/cpu,cpuacct/docker/8a7b3c2d1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b/在这个路径下cAdvisor 会读取两个核心文件cpuacct.usage这是一个 64 位整数单位是纳秒nanoseconds记录该 cgroup 自创建以来所有进程在所有 CPU 核心上实际运行的总时间。注意是“运行时间”不是“等待时间”。如果进程在 sleep 或 blocked 状态这个值不会增加。cpu.cfs_quota_us和cpu.cfs_period_us这两个文件定义了该 cgroup 的 CPU 配额。比如cpu.cfs_quota_us50000cpu.cfs_period_us100000意味着这个容器每 100msperiod最多能用 50msquota的 CPU 时间即硬性限制为 50% 的单核算力。如果容器试图超限内核会强制将其 throttle。cAdvisor 把cpuacct.usage的差值除以cpu.cfs_period_us再乘以 100就得到相对于配额的利用率百分比而把cpuacct.usage的差值除以machine_cpu_cores * 1e9换算成秒再除以采样间隔就得到绝对 CPU 核心秒数。后者才是 Prometheus 最看重的指标因为它不受配额限制影响能真实反映容器对物理资源的消耗。我实测过一个细节当你用docker run --cpus2启动容器时Docker 实际写入的是cpu.cfs_quota_us200000cpu.cfs_period_us100000即 200% 配额。但如果你用--cpus0.5它写的是cpu.cfs_quota_us50000cpu.cfs_period_us100000。cAdvisor 全部兼容因为它只认底层文件不认 Docker CLI 参数。注意cAdvisor 默认监听:8080端口暴露/metricsPrometheus 格式和/api/v1.3/containers/JSON API。但生产环境强烈建议关闭/api/v1.3/接口因为它是未认证的且返回全量容器树有信息泄露风险。只需保留/metrics即可Prometheus 只需要这个。3. Prometheus 抓取 cAdvisor 数据的完整链路与配置陷阱Prometheus 抓取 cAdvisor 数据表面看只是一行scrape_configs但背后藏着至少五个容易出错的环节。我见过太多人卡在第一步反复重启 Prometheus 却收不到数据最后发现只是防火墙没开。3.1 网络连通性别让容器网络骗了你cAdvisor 默认绑定0.0.0.0:8080但如果你用docker run启动它宿主机的localhost并不等于容器内的localhost。Prometheus 如果也运行在 Docker 容器里这是最常见部署方式它要访问 cAdvisor地址不能写http://localhost:8080/metrics而必须写http://host.docker.internal:8080/metricsWindows/macOS Docker Desktop或http://172.17.0.1:8080/metricsLinux即 docker0 网桥 IP。验证方法很简单进入 Prometheus 容器执行curl -v http://host.docker.internal:8080/metrics | head -20如果返回# HELP container_cpu_usage_seconds_total ...说明网络通了如果报Connection refused先检查 cAdvisor 是否真的在监听0.0.0.0不是127.0.0.1再检查宿主机防火墙是否放行 8080 端口。3.2 scrape_configs 配置一行代码背后的三重校验这是最常被复制粘贴却失效的配置。正确写法如下假设 cAdvisor 在宿主机 8080 端口scrape_configs: - job_name: cadvisor static_configs: - targets: [host.docker.internal:8080] # 注意这里 metrics_path: /metrics scheme: http # 关键设置抓取超时避免因 cAdvisor 响应慢拖垮整个 Prometheus scrape_timeout: 10s # 关键设置抓取间隔cAdvisor 默认 10s 采样这里设为 15s 是安全的 scrape_interval: 15s这里有两个致命陷阱targets 地址错误如前所述localhost在容器内指向自己不是宿主机。scrape_interval 设置不当cAdvisor 默认每 10 秒更新一次/metrics。如果你设scrape_interval: 5sPrometheus 会每 5 秒去拉一次但 cAdvisor 的指标值在两次采样间没变导致rate()计算出错分母太小噪声极大。经验法则scrape_interval应 ≥ cAdvisor 的--housekeeping_interval默认 10s推荐设为 15s 或 30s。3.3 relabel_configs给指标打上真正有用的标签原始 cAdvisor 指标里的container_label_com_docker_compose_service这种标签看着就头疼。我们需要把它简化成serviceweb、envprod这样的语义化标签。relabel 是必选项relabel_configs: # 把 Docker 容器 ID 映射为短 ID前 7 位便于阅读 - source_labels: [__meta_docker_container_id] target_label: container_id replacement: ${1} regex: ([0-9a-f]{7}).* # 提取 compose service 名如果用了 docker-compose - source_labels: [__meta_docker_container_label_com_docker_compose_service] target_label: service regex: (.) # 提取环境标签需在 docker run 时加 --label envprod - source_labels: [__meta_docker_container_label_env] target_label: env regex: (.) # 过滤掉系统容器如 cAdvisor 自身、Prometheus 自身避免噪音 - source_labels: [__meta_docker_container_name] regex: /(cadvisor|prometheus|node-exporter) action: drop这个配置的价值在于后续写告警规则时你可以直接写container_cpu_usage_seconds_total{serviceapi, envprod}而不是面对一堆container_label_com_docker_compose_projectmyapp这样的长字符串。3.4 TLS 与认证什么时候需要什么时候纯属画蛇添足cAdvisor 默认不启用 HTTPS 和 Basic Auth。如果你在公网暴露 cAdvisor必须加 Nginx 反向代理 TLS 认证。但在内网尤其是 Docker Compose 环境下强行加 TLS 反而增加复杂度和证书管理成本。我的建议是开发/测试环境HTTP host.docker.internal够用生产环境用nginx做反向代理监听443上游http://127.0.0.1:8080并配置auth_basic Restricted绝对不要在 cAdvisor 自身开启 TLS它不支持那是自找麻烦。4. CPU 利用率指标的黄金公式与 Grafana 可视化实战有了数据怎么算才是关键。container_cpu_usage_seconds_total是个计数器直接看毫无意义。我们必须用 Prometheus 的rate()函数计算单位时间内的增量。但选rate()还是irate()窗口选5m还是1m这决定了你看到的是趋势还是毛刺。4.1 rate() vs irate()选错一个告警全乱rate(container_cpu_usage_seconds_total[5m])计算过去 5 分钟内该指标平均每秒增长多少。它平滑了短期抖动适合看长期趋势和容量规划。例如你发现rate(...[5m])持续 0.8说明这个容器平均占用了 0.8 个 CPU 核心结合machine_cpu_cores就能算出整体占用率。irate(container_cpu_usage_seconds_total[5m])只取最近两个数据点计算瞬时速率。它对突发尖峰极其敏感适合做P99 延迟告警。比如irate(...[5m]) 2.0意味着过去 5 分钟内某一秒该容器占用了 2 个核心这很可能是 GC 或批量任务触发的。我的经验是监控面板用rate告警规则用irate。因为面板要稳定告警要灵敏。4.2 黄金计算公式从秒数到百分比的三步转换假设你想看容器web在prod环境下的CPU 核心占用率%公式如下# 步骤1计算过去5分钟该容器平均每秒使用多少CPU秒 rate(container_cpu_usage_seconds_total{serviceweb, envprod}[5m]) # 步骤2除以宿主机CPU核心数得到占用率小数 rate(container_cpu_usage_seconds_total{serviceweb, envprod}[5m]) / machine_cpu_cores # 步骤3乘以100转为百分比这才是人眼可读的数字 100 * rate(container_cpu_usage_seconds_total{serviceweb, envprod}[5m]) / machine_cpu_cores注意machine_cpu_cores是 Prometheus 自动发现的指标代表宿主机物理核心数不是超线程数。它来自 node_exporter所以你的 Prometheus 必须同时抓取 node_exporter。4.3 Grafana 面板一个真正能用的 CPU 监控模板我为你搭好了一个最小可行面板包含四个核心视图Panel 名称PromQL 查询说明实时 CPU 占用率%100 * rate(container_cpu_usage_seconds_total{service~$service, env~$env}[5m]) / machine_cpu_cores主视图用 Gauge 图阈值设 70%黄色、90%红色CPU 使用趋势核心秒rate(container_cpu_usage_seconds_total{service~$service, env~$env}[5m])用 Time series 图Y 轴单位是cores直观显示绝对算力消耗CPU Throttle 次数rate(container_cpu_cfs_throttled_periods_total{service~$service, env~$env}[5m])关键如果这个值 0说明容器被 CPU 配额限制了throttled越高性能损失越大CPU Wait 时间占比100 * rate(container_cpu_usage_seconds_total{service~$service, env~$env, modesystem}[5m]) / rate(container_cpu_usage_seconds_total{service~$service, env~$env}[5m])modesystem是内核态时间占比高可能意味着锁竞争或中断过多其中$service和$env是 Grafana 的变量通过label_values(container_cpu_usage_seconds_total, service)动态生成让运维可以一键切换查看任意服务。实操心得在 Grafana 里Time series图的Legend格式一定要设为{{service}} ({{env}})否则一堆线条根本分不清谁是谁。另外Gauge图的Max value设为100Thresholds设为70,90这样一眼就能看出是否越界。5. 告警规则编写从“CPU 高”到“需要人工介入”的精准判定监控的终点不是看图而是告警。但container_cpu_usage_seconds_total 80这种告警99% 是误报。真正的告警必须回答“这个 CPU 高是常态还是异常是容器自身问题还是宿主机被其他进程拖垮”5.1 告警规则的三层过滤逻辑我设计的 CPU 告警规则必须同时满足三个条件才触发容器自身 CPU 确实高irate(container_cpu_usage_seconds_total{jobcadvisor}[5m]) 1.5即 1.5 个核心不是短暂毛刺这个高值持续了至少 3 分钟and on()子句宿主机整体负载不高node_load1 / count(node_cpu_seconds_total{modeidle}) by (instance) 1.0即load average小于 CPU 核心数排除宿主机全局瓶颈。完整规则如下groups: - name: cadvisor-cpu-alerts rules: - alert: ContainerCPUSpike expr: | (irate(container_cpu_usage_seconds_total{jobcadvisor, image!}[5m]) 1.5) and (irate(container_cpu_usage_seconds_total{jobcadvisor, image!}[5m]) 1.5 offset 2m) and (irate(container_cpu_usage_seconds_total{jobcadvisor, image!}[5m]) 1.5 offset 4m) and (node_load1{jobnode} / count by(instance)(node_cpu_seconds_total{modeidle, jobnode}) 1.0) for: 3m labels: severity: warning service: {{ $labels.service }} container: {{ $labels.container_name }} annotations: summary: Container {{ $labels.container_name }} CPU usage high description: {{ $labels.container_name }} on {{ $labels.instance }} has CPU usage 1.5 cores for 3 minutes. Host load is normal.这个规则的精妙之处在于offset用法它不是简单地and (expr 1.5)而是要求当前、2 分钟前、4 分钟前三个时间点都 1.5这等价于“连续 3 分钟都高”彻底过滤掉秒级抖动。5.2 Throttle 告警比 CPU 高更危险的信号container_cpu_cfs_throttled_periods_total是比usage更重要的指标。当它开始增长意味着容器的 CPU 请求被内核强制节流业务响应必然延迟。我的告警规则是- alert: ContainerCPUCFSthrottle expr: rate(container_cpu_cfs_throttled_periods_total{jobcadvisor}[5m]) 0.1 for: 1m labels: severity: critical annotations: summary: Container {{ $labels.container_name }} is being throttled by CFS description: {{ $labels.container_name }} has been throttled for {{ $value | humanize }} periods/sec. Check CPU limits. 0.1意味着每 10 秒至少被 throttle 1 次。一旦触发第一反应不是扩容而是检查docker run --cpus或docker-compose.yml中的cpus配置是否过低。我曾处理过一个案例一个 Java 应用设置了--cpus0.5但 JVM-XX:ParallelGCThreads默认会根据cgroup.cpu.cfs_quota_us自动调大线程数结果 GC 线程疯狂争抢 CPU导致 throttle 飙升。解决方案是显式设置-XX:ParallelGCThreads2并把--cpus提到 1.0。5.3 告警降噪用absent()过滤掉已销毁的容器cAdvisor 会持续上报已退出容器的指标直到其 cgroup 目录被清理通常 10 分钟后。这会导致大量container_cpu_usage_seconds_total{container_namedead-abc123}的僵尸指标污染告警。解决方法是在所有告警表达式末尾加上and on(container_name, instance) absent(container_last_seen{jobcadvisor} offset 1h)container_last_seen是 cAdvisor 暴露的另一个指标记录容器最后一次被发现的时间戳。absent(... offset 1h)意味着“过去 1 小时内都没见过这个容器”这样就能自动过滤掉所有已销毁的容器告警列表永远干净。6. 故障排查实战一次真实的 CPU 瓶颈定位全过程去年我们一个订单服务在每天早 10 点准时出现 5 秒超时docker stats显示 CPU 95%但top里找不到高 CPU 进程。用 cAdvisor Prometheus我们花了 22 分钟定位到根因。过程如下6.1 第一步确认是容器内耗还是宿主机争抢查node_load1和machine_cpu_cores发现node_load1 / machine_cpu_cores 0.3远低于 1.0排除宿主机瓶颈。再查container_cpu_usage_seconds_total{serviceorder}[1h]的rate()确认是该容器自身 CPU 高。6.2 第二步区分是用户态还是内核态用container_cpu_usage_seconds_total{serviceorder, modeuser}[1h]和modesystem分别查询。发现system占比高达 78%而正常时应 10%。这说明问题在内核态——通常是锁、中断、或内存压力触发的 swap。6.3 第三步查内存和 IO发现 swap 活跃查node_memory_SwapTotal_bytes和node_memory_SwapFree_bytes发现SwapFree在早 10 点从 2G 骤降到 200M。再查container_memory_usage_bytes{serviceorder}发现其内存使用在 9:58 开始线性上涨10:00 达到 3.8G容器 limit 是 4G。6.4 第四步定位内存泄漏源头用kubectl top pod order-xxx当时是 K8s 环境确认是该 Pod。然后 exec 进容器jstat -gc $(pgrep java)发现FGCTFull GC 次数在 2 分钟内从 0 涨到 12 次每次 Full GC 耗时 1.2 秒。jmap -histo:live $(pgrep java) | head -20显示byte[]实例数暴涨最终锁定是一个日志框架的异步队列未设置上限日志堆积导致 OOM。6.5 第五步验证与修复临时方案docker exec -it order-xxx kill -SIGUSR2 $(pgrep java)触发堆 dump确认结论。永久方案升级日志框架配置queueSize1000。上线后container_cpu_usage_seconds_total{modesystem}回落到 5% 以下throttle归零。这个案例说明CPU 高永远不是孤立现象它一定是其他资源瓶颈内存、IO、锁的外在表现。cAdvisor 提供的多维指标CPU、内存、网络、IO让你能像剥洋葱一样层层深入而不是在docker stats的百分比里瞎猜。7. 性能与安全加固让 cAdvisor 在生产环境稳如磐石cAdvisor 本身很轻量但默认配置在生产环境有两大隐患内存泄漏和端口暴露风险。我经历过一次事故一台 32 核宿主机跑了 200 容器cAdvisor 内存从 200MB 涨到 4GBOOMKilled 三次导致监控断档。7.1 内存泄漏的根因与修复cAdvisor 的内存泄漏源于它默认缓存所有容器的历史指标用于/api/v1.3/接口。在容器频繁启停的环境如 CI/CD 流水线这个缓存会无限增长。解决方案是禁用 API 接口启动时加参数--disable_metricspercpu,process,hugetlb,network,tcp,udp只保留cpu,memory,disk,filesystem这些核心指标限制历史深度--max_housekeeping_interval30s默认 10s减少采样频率关闭无用功能--disable_metricsacceleratorGPU 监控不用就关。最终启动命令docker run -d \ --namecadvisor \ --restartalways \ --privileged \ --volume/:/rootfs:ro \ --volume/var/run:/var/run:rw \ --volume/sys:/sys:ro \ --volume/var/lib/docker/:/var/lib/docker:ro \ --volume/dev/disk/:/dev/disk:ro \ --publish8080:8080 \ --detachtrue \ --namecadvisor \ gcr.io/cadvisor/cadvisor:v0.49.1 \ --housekeeping_interval30s \ --disable_metricspercpu,process,hugetlb,network,tcp,udp,accelerator \ --logtostderr7.2 安全加固最小权限原则落地--privileged是必须的因为 cAdvisor 需要读取/sys/fs/cgroup和/proc。但这不意味着要给它 root 权限。最佳实践是用非 root 用户运行Docker 20.10 支持--user 1001:1001提前在镜像里创建好用户只挂载必要 volume删掉--volume/var/log:/var/log:ro这类无关挂载网络隔离用--networkhost替代桥接网络避免额外 iptables 规则SELinux/AppArmor在 CentOS/RHEL 上启用container_cgroup_readSELinux boolean。7.3 资源限制给 cAdvisor 自己上枷锁在docker run里加--memory512m --memory-swap512m --cpus0.5这能确保即使 cAdvisor 出现异常也不会拖垮宿主机。我在线上所有节点都加了这个限制三年来零故障。最后分享一个技巧在 Prometheus 里加一条记录规则每天凌晨自动备份 cAdvisor 的关键指标- record: container_cpu_usage_cores_24h_avg expr: avg_over_time(rate(container_cpu_usage_seconds_total[5m])[24h:5m])这样你随时能查“这个容器昨天平均用了多少核”做容量分析时比临时查rate()稳定得多。