宿主机进程反查 Docker 容器:一个高 CPU PID 的溯源实战
宿主机进程反查 Docker 容器:一个高 CPU PID 的溯源实战
摘要:宿主机
top里发现 CPU 飙高的进程,却看不出它属于哪个容器——这是接手容器化环境时最常见的“看得到摸不到”问题。本文从/proc/<pid>/cgroup的底层机制讲起,给出从 PID 反查容器 ID 的多种命令链,覆盖 docker top、docker inspect、cgroup 文件系统三条路径,并延伸到 CPU 限流排查和脚本化巡检,适用于 Docker 主机日常运维与故障定位。
一、场景引入
某天宿主机告警:CPU 使用率持续 90%+。登上去一看:
$ top
PID USER %CPU COMMAND
29656 root 198.7 java
30122 root 35.2 node
一个 java 进程吃了两个核,但你的服务全是容器化部署的,宿主机上根本不应该直接跑业务进程。这个 PID 是谁家容器里的?宿主机有几十个容器在跑,总不能挨个 docker exec 进去找。
反过来还有一个对称问题:容器内看到的 PID 从 1 开始编号,和宿主机 PID 完全对不上。运维在宿主机上 strace、perf 一个容器内进程时,需要知道宿主机侧的 PID 才能挂上工具。
这两个问题共享同一套解法,核心都藏在 /proc/<pid>/cgroup 里。
二、核心机制:/proc/<pid>/cgroup 里的容器指纹
Linux 内核给每个进程维护一个 /proc/<pid>/cgroup 文件,记录它归属的 cgroup 路径。Docker 启动容器时会为它创建专属 cgroup,容器内所有进程的 cgroup 路径里都带着容器 ID——这就是反查的钥匙。
对宿主机上任意一个可疑 PID,先看它是不是容器进程:
$ cat /proc/29656/cgroup
cgroup v1(较老主机)输出形如:
12:blkio:/docker/bf85501b3084601ba76b8cb303917134d58b5e7783c14c1636ff1c56a3d83c1f
...
cgroup v2(Ubuntu 22.04+、Debian 12+ 等新系统)输出形如:
0::/system.slice/docker-bf85501b3084601ba76b8cb303917134d58b5e7783c14c1636ff1c56a3d83c1f.scope
不管哪个版本,路径里那串 64 位十六进制就是完整容器 ID。截取前 12 位即可用于 docker 命令:
$ docker ps --no-trunc | grep bf85501b3084
$ docker inspect --format '{{.Name}}' bf85501b3084
/backend
如果 /proc/<pid>/cgroup 的路径是 / 或不含 docker 字样,说明这是宿主机原生进程,排查方向就变了。
三、三条反查命令链
命令链一:docker ps --no-trunc 粗筛
最直接的一步。先拿到容器的完整 ID:
$ docker ps --no-trunc
CONTAINER ID IMAGE
bf85501b3084601ba76b8cb303917134d58b5e7783c14c1636ff1c56a3d83c1f backend:runtime
把 cat /proc/29656/cgroup 里的 ID 段和列表比对,命中即锁定。容器不多时这招最快。
命令链二:docker inspect 精确反查
如果知道 PID 想反向确认容器,可用 cgroup ID 去匹配。更常用的场景是已知容器、找它的宿主机 PID(用于 strace/perf):
$ docker inspect --format '{{.State.Pid}}' backend
29610
输出即该容器主进程(PID 1)在宿主机上的真实 PID。拿到后:
$ docker top backend -eo pid,ppid,pcpu,comm,args
PID PPID %CPU COMMAND
29610 1 0.1 java -jar app.jar
29656 29610 198.7 java
能看到 29656 确实是这个容器的子进程,父子关系(PPID 指向容器主进程)也验证了归属。
命令链三:cgroup 文件系统直读
不想碰 docker 命令时(比如排查脚本运行在最小化环境里),直接读文件系统:
# cgroup v1
$ CID=$(docker inspect --format '{{.Id}}' backend)
$ cat /sys/fs/cgroup/memory/docker/${CID}/memory.usage_in_bytes
# cgroup v2
$ cat /sys/fs/cgroup/system.slice/docker-${CID}.scope/memory.current
这个路径同时是内存排查的入口,能直接读出容器内存占用、上限、OOM 次数等真实数据。
四、从容器视角到宿主机视角的 PID 映射
容器内 PID 命名空间独立,同一个进程两套编号:
| 视角 | 查看命令 | 输出 |
|---|---|---|
| 容器内 | docker exec backend ps |
从 1 开始的编号 |
| 宿主机 | docker top backend -o pid,cmd |
宿主机真实 PID |
docker top 的输出全部是宿主机视角,可直接用宿主机工具操作。典型组合: |
# 找到容器内某进程的宿主机 PID,直接 strace
$ HOST_PID=$(docker top backend -o pid,cmd | grep "my-process" | awk '{print $1}')
$ sudo strace -p $HOST_PID
这是容器排障的高级技巧——不进容器、不装工具,用宿主机全套调试手段分析容器内进程。
五、反查到容器之后的下一步:CPU 限流排查
锁定容器后,如果它 CPU 高但宿主机整体没满,要检查是不是限流导致(限流会让容器内进程表现为高 CPU,即使宿主机有大量空闲)。
# 先看整体容器资源占用
$ docker stats --no-stream --format \
'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}'
# 下钻到具体容器内的进程
$ docker top backend -eo pid,ppid,pcpu,pmem,comm,args
# 配合日志
$ docker logs --since 10m --tail 100 backend
docker stats 里 CPUPerc 超过 100% 是正常现象(多核叠加)。要确认限流是否真的发生,读 cgroup 的 CPU 统计:
# cgroup v2
$ cat /sys/fs/cgroup/system.slice/docker-${CID}.scope/cpu.stat
关注 nr_throttled 和 throttled_usec——非零说明发生过限流,此时应调高 --cpus 或优化应用逻辑,而不是继续在宿主机侧找“谁在抢 CPU”。
六、封装成巡检脚本
把反查流程固化成脚本,输入 PID、输出容器名:
#!/bin/bash
# 用法: ./pid2container.sh <PID>
PID=$1
CGROUP=$(cat /proc/$PID/cgroup 2>/dev/null)
# cgroup v1 和 v2 都兼容的提取方式
CID=$(echo "$CGROUP" | grep -oE '[0-9a-f]{64}' | head -1 | cut -c1-12)
if [ -z "$CID" ]; then
echo "PID $PID 是宿主机原生进程"
else
docker inspect --format '{{.Name}}' $CID
fi
配合告警系统,在 CPU 飙高时自动执行,运维看到的就是"backend 容器"而不是一串裸 PID——排查起点直接前进了两步。
七、常见坑
坑一:容器已停止,cgroup 文件消失。容器停止后 /proc/<pid>/cgroup 无从谈起,只能在停止前采集,或事后看容器日志(docker logs 在容器删除前都有效)。
坑二:cgroup v1 与 v2 路径差异。v1 是 /docker/<id>,v2 是 /system.slice/docker-<id>.scope,脚本里两种模式都要兼容。
坑三:Docker 套 K8s 环境。K8s 场景下容器 cgroup 路径形如 /kubepods/podxxx/<id>,不是 docker 字样,用 crictl ps 而非 docker ps 查。
坑四:jq 不可用时。直接用 grep 加 cut 提取 ID 段,不依赖额外工具,脚本可移植性更好。
八、总结
PID 反查容器的完整决策链:
| 步骤 | 命令 | 目的 |
|---|---|---|
| 1. 定性 | cat /proc/<pid>/cgroup |
判断是否容器进程 |
| 2. 提取 | 从路径截取 64 位容器 ID | 拿到钥匙 |
| 3. 归属 | docker inspect --format '{{.Name}}' <ID前12位> |
得到容器名 |
| 4. 验证 | docker top <容器名> -o pid,ppid,cmd |
确认父子关系 |
| 5. 下钻 | docker stats + 容器日志 + cpu.stat |
定位根因 |
| 理解“容器进程的 cgroup 路径必带容器 ID”这个底层事实,比死记命令更重要——它能推导出所有反查命令的原理,遇到非标准环境(K8s、Podman、containerd)也能举一反三。 |
- 点赞
- 收藏
- 关注作者
评论(0)