news 2026/10/15 9:19:04

容器越跑越多,谁在悄悄吃 CPU 和内存?用 cgroup 指标起一个只读资源看板,cpolar 单入口给异地同事看负载

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
容器越跑越多,谁在悄悄吃 CPU 和内存?用 cgroup 指标起一个只读资源看板,cpolar 单入口给异地同事看负载

容器越跑越多,谁在悄悄吃 CPU 和内存?用 cgroup 指标起一个只读资源看板,cpolar 单入口给异地同事看负载

先说场景。我本地用 Colima 跑了一堆容器,时间一长就变成一团:有的是随手docker run起来忘了关的,有的是做实验留着的,还有几个是常驻服务。等到风扇狂转、笔记本发热,我才打开docker stats,一屏刷下来眼睛都花了,而且这个命令一关终端数字就没了,别人想看也看不到。

更麻烦的是异地同事。他不在我旁边,也没法连到我本机,每次问"你那几个容器现在谁在吃 CPU",我只能截图发过去,过一会儿数据又变了。

所以我想要的东西其实很朴素:把容器资源指标读出来,渲染成一个只读页面,再用 cpolar 开一个单入口,让异地同事自己打开看。不给他任何操作权限,他只能看,不能停容器、不能 exec、不能删东西。

这篇就是把这个小实验的完整过程写下来。先说明一句:本文是隔离环境中的可执行实验提纲,这一轮没有实际读取真实宿主机的 cgroup、没有启动看板、没有起隧道,所有命令和骨架都给全,但示例数值都标注为「示例/模拟值」,不虚构实测结果。

1 这篇里每个东西分别负责什么

别被标题里的名词吓到,拆开看就三件事。

cgroup负责提供数据源。Linux 把每个容器的 CPU、内存、进程数、IO 都记在 cgroup 的层级目录里,Docker 容器在 cgroup v2 下对应/sys/fs/cgroup/下的一串路径,读出cpu.stat、memory.current、memory.max、pids.current这些文件,就拿到了"谁在吃资源"的原始依据。

只读看板负责把数据变成人看得懂的东西。一个最小采集脚本按周期读指标,聚合成"容器列表 + 资源排行",渲染成网页,绑定在本机127.0.0.1:8106。它只读,页面上没有任何按钮能让容器动起来。

cpolar负责把本机页面变成异地可达的入口。看板只绑在 127.0.0.1,同事在外网是打不开的,需要 cpolar 把这个端口映射成一个公网地址。这次走单一入口:要么用 http 隧道,要么上固定二级子域名,让地址稳定下来。

三者的分工很清楚:cgroup 给数,看板给看,cpolar 给路。

2 环境准备:先确认 cgroup 版本和 Docker 状态

别急着写脚本,先确认两件事,不然读到一半发现没有数据。

2.1 确认 cgroup 是 v2

在 Linux 上执行:

stat -fc %T /sys/fs/cgroup/

看到cgroup2fs就是 cgroup v2。如果是tmpfs,说明是 v1,目录结构和文件名不一样,这篇按 v2 走。macOS 上 Colima 本质是跑一个 Linux 虚拟机,容器在 VM 里的 cgroup v2 上,所以采集要在 VM 内部或通过宿主路径读,这点后面会说清楚。

2.2 确认 Docker 可用

docker version docker info --format '{{.CgroupVersion}}'

docker info里的CgroupVersion直接告诉你用的是 v1 还是 v2。这里如果报连不上 daemon,先确认 Colima 起来了:

colima status

没起来就colima start。这一步别跳过,后面所有采集都建立在 Docker 正常运行之上。

2.3 起几个示例容器

为了让看板有东西可看,先起几个不同负载的容器。骨架示例,自己按需替换:

# 一个轻负载的常驻容器 docker run -d --name demo-web nginx:alpine # 一个吃 CPU 的忙碌容器(示例,跑 busybox 死循环) docker run -d --name demo-busy busybox sh -c 'while true; do :; done' # 一个占内存的容器(分配一块内存挂着) docker run -d --name demo-mem busybox sh -c 'dd if=/dev/zero of=/dev/null bs=1M & sleep 3600' # 一个基本空闲的容器 docker run -d --name demo-idle alpine sleep 3600

docker ps应该能看到这四个。demo-busy会明显拉高 CPU,demo-web和demo-idle基本闲着——这样排行才有对比,一眼看出谁在吃资源。

3 找到容器的 cgroup 路径

这是整个实验里最容易卡住的一步,单独拎出来讲。

每个容器有一个完整 ID,cgroup v2 下它的目录通常长这样:

/sys/fs/cgroup/system.slice/docker-<容器完整ID>.scope

先拿到 ID 和对应的路径:

# 列出容器 ID 和名字 docker ps --no-trunc --format '{{.ID}} {{.Names}}' # 用 docker inspect 反查 cgroup 路径(更稳,不用自己拼) docker inspect -f '{{.Name}} => {{.HostConfig.CgroupParent}}' demo-busy

如果没有CgroupParent,就用 systemd 那条固定规则自己拼:/sys/fs/cgroup/system.slice/docker-<ID>.scope。容器 ID 一定要用完整 ID,短 ID 拼出来的路径不存在,会直接 stat 失败。如果读不到,先检查是不是用了短 ID、或者是不是 cgroup v1。

4 采集脚本:读 cgroup 指标并聚合

下面是一个最小可运行脚本骨架,用来演示"怎么读、怎么算",不是最终成品。它读的是 cgroup v2 的文件,把每个容器的 CPU 使用率、内存占用、进程数算出来。

#!/usr/bin/env python3 # 骨架示例:读取 cgroup v2 指标,输出容器资源排行 # 仅在 Linux / Colima VM 内可直接读取 /sys/fs/cgroup 时运行 import os import time import subprocess CGROUP_ROOT = "/sys/fs/cgroup" SAMPLE_INTERVAL = 3 # 采样间隔(秒) def list_containers(): """返回 [(容器名, cgroup路径)]""" out = subprocess.check_output( ["docker", "ps", "--no-trunc", "--format", "{{.ID}}\t{{.Names}}"], text=True ) result = [] for line in out.strip().splitlines(): if not line.strip(): continue cid, name = line.split("\t") path = os.path.join(CGROUP_ROOT, "system.slice", f"docker-{cid}.scope") result.append((name, path)) return result def read_int(path): try: with open(path) as f: return int(f.read().strip()) except (FileNotFoundError, ValueError): return None def read_cpu_usage_usec(cgroup_path): """从 cpu.stat 读累计 CPU 时间(微秒)""" stat_path = os.path.join(cgroup_path, "cpu.stat") try: with open(stat_path) as f: for line in f: k, _, v = line.partition(" ") if k == "usage_usec": return int(v) except FileNotFoundError: pass return None def collect_once(): rows = [] for name, path in list_containers(): cpu_usec = read_cpu_usage_usec(path) mem_cur = read_int(os.path.join(path, "memory.current")) mem_max = read_int(os.path.join(path, "memory.max")) pids = read_int(os.path.join(path, "pids.current")) rows.append({ "name": name, "cpu_usec": cpu_usec, "mem_current": mem_cur, "mem_max": mem_max, "pids": pids, }) return rows def sample(): """采样两次计算 CPU 使用率(%)""" first = collect_once() time.sleep(SAMPLE_INTERVAL) second = collect_once() first_map = {r["name"]: r for r in first} result = [] for r in second: prev = first_map.get(r["name"], {}) cpu_pct = None if r["cpu_usec"] and prev.get("cpu_usec"): delta = r["cpu_usec"] - prev["cpu_usec"] cpu_pct = round(delta / (SAMPLE_INTERVAL * 1_000_000) * 100, 1) mem_mb = round(r["mem_current"] / 1024 / 1024, 1) if r["mem_current"] else None result.append({ "name": r["name"], "cpu_pct": cpu_pct, "mem_mb": mem_mb, "pids": r["pids"], }) result.sort(key=lambda x: (x["cpu_pct"] or 0), reverse=True) return result if __name__ == "__main__": for row in sample(): print(row)

几个关键点,读的时候留意:

  • cpu.stat里的usage_usec是累计值,两次采样相减、再除以时间间隔,才是这一段的 CPU 使用率。直接读一个数把它当"当前 CPU 占用"是错的。
  • memory.max的取值是数字或字符串max(max表示不限制内存),所以read_int里做了 try 保护,遇到非数字就返回 None,别让脚本崩掉。
  • 单位换算别搞混:memory.current是字节,除以 1024 两次才是 MB。

跑一下:

python3 collect.py

你会看到一行行字典,cpu_pct最高的那个就是吃 CPU 最凶的。示例输出长这样(模拟值):

{'name': 'demo-busy', 'cpu_pct': 98.4, 'mem_mb': 1.2, 'pids': 2} {'name': 'demo-web', 'cpu_pct': 0.3, 'mem_mb': 8.6, 'pids': 5} {'name': 'demo-mem', 'cpu_pct': 0.1, 'mem_mb': 32.4, 'pids': 3} {'name': 'demo-idle', 'cpu_pct': 0.0, 'mem_mb': 1.1, 'pids': 2}

这个数字是模拟的,别当真——真实环境里demo-busy会稳定占到接近一个核,demo-web其实会高一些,具体看你的机器。

数据这一步通了,说明链路的前半段就没问题。哪一步读出来是 None,就回到第 3 节确认路径对不对。

5 渲染成只读看板并绑定本机端口

采集逻辑有了,接下来把它套上一个最小的 HTTP 服务。用 Python 标准库就够,不用装框架。

#!/usr/bin/env python3 # 骨架示例:只读资源看板,绑定 127.0.0.1:8106 # 只提供 GET 渲染指标,无任何容器操作接口 from http.server import BaseHTTPRequestHandler, HTTPServer from collect import sample # 复用第 4 节的采集函数 HOST, PORT = "127.0.0.1", 8106 def render_html(rows): body = "".join( f"<tr><td>{r['name']}</td><td>{r['cpu_pct']}%</td>" f"<td>{r['mem_mb']} MB</td><td>{r['pids']}</td></tr>" for r in rows ) return f"""<!doctype html> <html lang="zh"> <head><meta charset="utf-8"><title>容器资源只读看板</title></head> <body> <h1>容器资源只读看板</h1> <p>数据每 3 秒刷新一次,仅展示,不可操作。</p> <table border="1" cellpadding="6"> <tr><th>容器</th><th>CPU</th><th>内存</th><th>进程数</th></tr> {body} </table> </body></html>""" class Handler(BaseHTTPRequestHandler): def do_GET(self): if self.path not in ("/", "/index.html"): self.send_error(404, "Not Found") return html = render_html(sample()) payload = html.encode("utf-8") self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(len(payload))) self.end_headers() self.wfile.write(payload) def do_POST(self): # 只读服务,任何写操作一律拒绝 self.send_error(405, "Read Only") if __name__ == "__main__": server = HTTPServer((HOST, PORT), Handler) print(f"看板已启动:http://{HOST}:{PORT}") server.serve_forever()

启动:

python3 dashboard.py

本机打开http://127.0.0.1:8106,能看到一张表:容器名、CPU、内存、进程数,按 CPU 从高到低排。这一步看到什么很关键——如果表格是空的,回到第 3 节查 cgroup 路径;如果数字不刷新,看sample()是不是没被调用到。

只读边界在这里落死:do_POST直接返回 405,路由只认/,任何其他路径 404。页面上没有任何表单、按钮、链接指向操作接口。这一点别偷懒,只要留了一个写入口,后面 cpolar 一暴露就等于把口子开到公网了。

6 用 cpolar 开单一入口给异地同事

本地跑通之后,才轮到 cpolar。看板绑的是127.0.0.1,外网访问不到,需要映射出去。

先装 cpolar。macOS 上推荐按官网下载页的安装包走,安装后在浏览器登录后台,Web UI 默认在http://127.0.0.1:9200,登录后 token 会自动写入;也可以命令行手动绑定:

cpolar authtoken <你的authtoken>

确认 cpolar 服务起来了,9200能打开、隧道列表在线,再开隧道。

6.1 临时验证:http 随机隧道

cpolar http 8106

前台会输出一个 Forwarding 公网地址,形如https://xxxxxxxx.r3.cpolar.cn -> http://localhost:8106,把这个地址发给异地同事,他就能打开看板。

注意:免费版的随机地址在会话结束时失效,重启隧道地址就变,24 小时内不保证稳定。如果你只是拉同事临时代看一下,这样够了;如果同事要隔三差五回来看,每次都发新地址会很烦。

6.2 稳定入口:固定二级子域名

想要地址固定下来,用固定二级子域名。这里要写准口径:

  • 固定二级子域名需要基础套餐及以上
  • 固定 TCP 地址和自定义域名需要专业套餐及以上

在 cpolar 后台创建预留的二级子域名,然后在隧道配置里指定用它,地址就不再随会话变化。这样同事收藏一次,之后直接打开就行——这就是本题说的"单一入口"。

配置写成 YAML 的骨架示例(路径以本机实际为准):

authtoken: <你的authtoken> tunnels: dashboard: addr: 8106 proto: http region: cn_vip subdomain: your-reserved-subdomain

改完重启 cpolar 服务生效。YAML 缩进敏感,不能有 TAB,这点坑过很多人。

一个提醒:cpolar 只负责把本地端口映射到公网,它不替代应用自身的鉴权。我们这个看板本身是只读的,风险已经被压到很低,但如果你后面往里加了别的接口,暴露之前一定要想清楚。

7 让异地同事核对,并验证只读边界

隧道起来后,让同事在外网打开地址,核对两件事:

  1. 看板能不能打开,表里的容器名和本机docker ps是否一致;
  2. 排行对不对——本机观察到demo-busy吃 CPU 最多,页面上它应该排在前面。

然后做一次边界验证,确认"只读"不只是嘴上说说:

# 试一个不存在的路径,应该 404 curl -i https://<你的公网地址>/admin # 试一个写请求,应该 405 curl -i -X POST https://<你的公网地址>/

预期返回 404 和 405。如果写请求居然成功了,说明服务里有没关掉的写入口,立刻停掉隧道,回去查路由。

再确认一点:看板全程没有碰 Docker socket。它是通过读/sys/fs/cgroup文件拿数据的,只要暴露的是那个 HTTP 页面,Docker 的管理能力就没有对外开出去。这一条是本题的安全底线。

验证完,关掉隧道和示例容器,清理干净:

# Ctrl+C 关掉前台隧道 docker rm -f demo-web demo-busy demo-mem demo-idle

8 总结

到这里,一个能用的只读资源看板就成型了:本机把容器 cgroup 指标读出来聚合,渲染成一张带排行的网页,绑在127.0.0.1:8106,再用 cpolar 开一个单入口给异地同事看。同事能在任何地方打开浏览器看到"谁的 CPU 高、谁的内存大",但他停不了容器、exec 不进去、也删不了东西,路径只有一条,就是看。

关键步骤回顾:

  • 定数据源:确认 cgroup v2,用完整容器 ID 拿到/sys/fs/cgroup/system.slice/docker-<ID>.scope,读cpu.stat/memory.current/memory.max/pids.current;
  • 定采集口径:CPU 用两次采样的累计值相减算,内存注意字节换算,别把累计值当瞬时值;
  • 定边界:看板只读,路由只认/,写请求一律 405,不碰 Docker socket;
  • 定入口:临时看用免费 http 隧道(随机地址 24h 会变),长期稳定用固定二级子域名(基础套餐及以上)。

回到最开始那个问题:容器越跑越多,谁在悄悄吃 CPU 和内存?与其每次截图发给同事,不如把这个只读页面挂在一个固定入口上,谁来问,直接把地址给他。数据是实时的,同事不用等你,你也不用一遍遍解释。要扩展的话,后面可以加采样历史、加上限告警,或者把入口收到自己的域名下——但第一步,先把这条只读链路跑通。


说明:本文是隔离环境中的可执行实验提纲。这一轮未实际读取真实宿主机 cgroup、未启动看板、未起隧道,文中命令与代码均为可运行骨架,示例数值为模拟值,不构成实测结论。正式执行时需在隔离环境按本文步骤实跑并记录采集口径、容器清单与样本指标。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 9:18:47

App性能测试脚本开发实战:从adb采集到工程化落地

简介&#xff1a;面向移动应用测试与性能测试初学者&#xff0c;文档以LoadRunner 11.0为工具&#xff0c;系统说明手机App性能测试脚本的开发流程。内容从前提条件讲起&#xff0c;包括已安装LoadRunner、准备待录制APP、电脑开启WiFi热点、必须安装LR_03105_Patch4补丁等&…

作者头像 李华
网站建设 2026/10/15 9:17:52

MediaPlayer 屏幕常亮机制与 WakeLock 调用链

晚上躺床上看长视频&#xff0c;屏幕突然熄了&#xff0c;你伸手点亮继续看&#xff0c;没过多久又熄了——被这个场景折磨过的人&#xff0c;大概率不是一个合格的多媒体开发。做视频播放器&#xff0c;有一项基础到不能再基础的需求&#xff1a;视频在播&#xff0c;屏幕就不…

作者头像 李华
网站建设 2026/10/15 9:17:48

大模型推理性能瓶颈:CUDA、Triton与torch.compile选型实战指南

1. 项目概述&#xff1a;为什么“大模型算子框架选型”不是技术选型题&#xff0c;而是工程生死线“大模型算子框架选型”这八个字&#xff0c;听上去像一个技术评审会上的PPT小节标题&#xff0c;但在我过去三年深度参与六个千卡级大模型推理平台建设、亲手调优过从A100到H100…

作者头像 李华
网站建设 2026/10/15 9:16:27

Java线程等待与通知机制详解

1. 线程等待与通知机制概述在并发编程中&#xff0c;线程间的协调是保证程序正确性的关键。当多个线程需要按照特定顺序执行或共享资源时&#xff0c;单纯的锁机制往往不够高效。Java内置的等待/通知机制&#xff08;wait/notify&#xff09;提供了一种线程间通信的优雅解决方案…

作者头像 李华
网站建设 2026/10/15 9:15:57

PPT翻页背后:从ISA到中断的软硬件协作全链路解析

你大概率被人问过这句话&#xff1a;请描述一下PPT完成翻页时&#xff0c;电脑软硬件是如何协作的。别小看它&#xff0c;它把一个通用计算机最关键的“输入—处理—输出”全链路都卷了进来。这一问之所以经典&#xff0c;一方面是因为它横跨操作系统、计算机体系结构、编译原理…

作者头像 李华
网站建设 2026/10/15 9:15:53

2026年CCF期刊目录更新:AI与网络安全领域重大调整

1. 2026年CCF期刊目录更新解读今天上午&#xff0c;中国计算机学会&#xff08;CCF&#xff09;正式发布了2026年版推荐期刊目录&#xff0c;这是继2023年更新后的又一次重要调整。作为计算机领域科研工作者最关注的学术评价标准之一&#xff0c;这份目录直接影响着国内高校和研…

作者头像 李华