认识容器:从一个 nginx 容器看透 Namespace 与 Cgroup

📅 2026/7/25 15:28:21
认识容器:从一个 nginx 容器看透 Namespace 与 Cgroup
认识容器从一个 nginx 容器看透 Namespace 与 Cgroup系列开篇| 容器技术底层原理深度实操系列实验环境华为云 FlexusX (8vCPU/16GiB) · Ubuntu 24.04 Server · 内核 6.8.0-106-generic · Docker 29.1.3 · Cgroup v2本文所有命令输出均为真机实录。一、为什么容器问题的答案不在 docker 命令里如果你用容器的时间够长一定遇到过这些灵异事件容器里kill -9 11 号进程纹丝不动容器内存明明没用满进程却被 OOM Killer 干掉了加了 CPU 限制容器还是卡得像蜗牛改了/proc/sys/net下的参数重启容器就失效。翻遍 Docker 文档也找不到答案——因为容器不是虚拟机它只是 Linux 内核几种机制组合出来的进程包装。所有这些问题的根源都在内核的 Namespace、Cgroup、OverlayFS 里。一句话概括容器的本质容器 Namespace(隔离视图) Cgroup(限制资源) 联合文件系统(打包 rootfs)这篇开篇文章我们就用一个最普通的 nginx 容器把这三板斧逐一解剖给你看。二、容器进程就是宿主机上的普通进程先启动一个 nginx$dockerrun-d--nameintro-nginx-p8080:80 nginx 18a43df07a8cceeb88207c4493ad281d68bbcd53d2e14140e76c87544ee89a81 $dockerps--formattable {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}NAMES IMAGE STATUS PORTS intro-nginx nginx Up2seconds0.0.0.0:8080-80/tcp,[::]:8080-80/tcp很多人以为容器像虚拟机一样里面跑着一个操作系统。我们直接在宿主机上找到这个容器的主进程$PID$(dockerinspect intro-nginx--format{{.State.Pid}})$echo$PID18285$ps-opid,ppid,cmd-p18285PIDPPIDCMD1828518262nginx: master process nginx-gdaemon off;看到了吗所谓容器在宿主机看来就是 PID 18285 这个普普通通的 nginx 进程父进程是 containerd-shimPID 18262。没有虚拟化层没有 Guest OS就是一个进程。这是理解一切容器问题的第一性原理你排查容器问题本质上是在排查一个组Linux 进程的问题。三、第一板斧Namespace——让进程看不见彼此既然是普通进程为什么容器里看不到宿主机的其他进程、网卡、文件答案是 Namespace。用lsns看看 PID 18285 拥有哪些 namespace$ lsns-p18285NS TYPE NPROCS PIDUSERCOMMAND4026531834time2111root /sbin/init noibrs4026531837user2111root /sbin/init noibrs4026532496mnt918285root nginx: master process nginx-gdaemon off;4026532497uts918285root nginx: master process nginx-gdaemon off;4026532498ipc918285root nginx: master process nginx-gdaemon off;4026532499pid918285root nginx: master process nginx-gdaemon off;4026532500cgroup918285root nginx: master process nginx-gdaemon off;4026532501net918285root nginx: master process nginx-gdaemon off;这个输出信息量很大逐行拆解Namespace编号隔离了什么在容器里的体现mnt4026532496 (独立)挂载点/文件系统视图容器有自己的/看不到宿主机文件uts4026532497 (独立)主机名容器里 hostname 是容器 IDipc4026532498 (独立)System V IPC/消息队列容器间共享内存互不可见pid4026532499 (独立)进程号空间nginx 在容器里是 1 号进程cgroup4026532500 (独立)cgroup 根视图容器里看/proc/1/cgroup是0::/net4026532501 (独立)网卡/路由/iptables容器有自己的 eth0time4026531834 (共享)系统时钟与宿主机相同NPROCS211user4026531837 (共享)uid/gid 映射默认与宿主机共享这是安全模块的重点注意两个细节6 个 namespace 是独立的NPROCS9只有容器里的 9 个进程而time 和 user namespace 默认与宿主机共享NPROCS211全机进程。这就是为什么容器里改系统时间会影响宿主机、容器里的 root 默认就是宿主机 root——后面安全篇会专门讲这个坑。namespace 本质是内核里的一组数据结构进程的task_struct-nsproxy指向它们。clone()时传入CLONE_NEWPID等 flag 就能创建新 namespace——Docker 干的就是这件事。验证 PID Namespace容器内的1 号进程$dockerexecintro-nginxcat/proc/1/cgroup0::/在容器里nginx 自己就是 1 号进程而且它看到的 cgroup 路径是0::/根——但我们马上会在宿主机看到真相。验证 Net Namespace不进容器也能进入容器网络docker exec不是什么黑魔法nsenter就能手工进入任何 namespace$ nsenter-t18285-nip-4addr show eth02: eth0if9:BROADCAST,MULTICAST,UP,LOWER_UPmtu1500qdisc noqueue state UP inet172.17.0.3/16 brd172.17.255.255 scope global eth0-t 18285 -n表示进入目标进程的 net namespace。看到了容器的172.17.0.3而且注意eth0if9——容器的 eth0 其实是一个veth 设备对的一端另一端 if9 插在宿主机 docker0 网桥上网络篇会顺着这根网线完整排查一次网络不通问题。所以记住docker exec nsenter 进入全部 namespace 执行命令。当容器里没有调试工具时比如上面 nginx 镜像里连 ps 都没有sh: 1: ps: not foundnsenter只进入部分 namespace就能用宿主机的工具排查容器——这是容器排障最重要的技巧没有之一。四、第二板斧Cgroup——给进程戴上紧箍咒Namespace 管看不见Cgroup 管用不多。Ubuntu 24.04 默认使用Cgroup v2统一层级容器对应的 cgroup 目录在$cat/proc/18285/cgroup0::/system.slice/docker-18a43df07a8ccee...89a81.scope $ls/sys/fs/cgroup/system.slice/docker-18a43df07a8c*.scope/|head-12cgroup.controllers cgroup.events cgroup.freeze cgroup.kill cgroup.max.depth cgroup.max.descendants cgroup.pressure cgroup.procs cgroup.stat cgroup.subtree_control cgroup.threads cgroup.type刚才容器里看到的0::/和宿主机看到的/system.slice/docker-xxx.scope是同一个 cgroup 的两个视角——cgroup namespace 把路径根化了。这个容器没加任何资源限制所以$cat.../cpu.max max100000# 不限 CPU每 100ms 周期内可用时间无上限$cat.../memory.max max# 不限内存docker run --cpus1 -m 512m干的事情就是把这两个文件分别改成100000 100000和536870912。仅此而已——Docker 的资源限制参数全都是 cgroup 文件的搬运工。Cgroup v2 与老教程里 v1 最大的区别Cgroup v1Cgroup v2 (本系列环境)层级结构每个子系统一棵树 (/sys/fs/cgroup/cpu,/memory…)统一一棵树CPU 限制cpu.cfs_quota_us/cpu.cfs_period_uscpu.max一个文件两个值内存限制memory.limit_in_bytesmemory.max磁盘限速blkio.throttle.*io.max且支持 buffered IO 限速这个差异会贯穿整个系列——网上大量容器教程还停留在 v1照着敲会找不到文件。五、第三板斧联合文件系统——镜像分层的真相容器的 rootfs 从哪来看挂载$mount|grepoverlay|grep18a43df07a8c overlay on /var/lib/docker/rootfs/overlayfs/18a43df07a8c...typeoverlay(rw,relatime,lowerdir/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/25/fs:.../snapshots/24/fs:.../snapshots/23/fs:.../snapshots/22/fs:.../snapshots/21/fs:.../snapshots/20/fs:.../snapshots/15/fs:.../snapshots/13/fs,upperdir.../snapshots/26/fs,workdir.../snapshots/26/work,nouserxattr)三个关键角色容器看到的 / (merged 合并视图) ┌───────────────────────────────┐ │ upperdir (snapshots/26) │ ← 可写层容器所有写操作落在这 ├───────────────────────────────┤ │ lowerdir (snapshots/25) │ ← nginx 镜像第 8 层只读 │ lowerdir (snapshots/24) │ ← 第 7 层只读 │ ... │ │ lowerdir (snapshots/13) │ ← Debian 基础层只读 └───────────────────────────────┘nginx 镜像的 8 个 layer 是 8 个只读 lowerdir容器启动时在顶上加一层可写 upperdirOverlayFS 把它们叠成一个完整的根文件系统。10 个容器共享同一份镜像层每个只多一层薄薄的可写层——这就是容器比虚拟机轻的核心原因。一个值得注意的时代变化这台机器的 Docker 29.1.3 已经把镜像层交给containerd snapshotter管理路径在/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/docker inspect里甚至没有了经典的GraphDriver字段$dockerinspect intro-nginx--format{{json .GraphDriver.Data}}template parsing error: map has no entryforkeyGraphDriver如果你在新版本 Docker 上照老教程找/var/lib/docker/overlay2/找不到东西原因就在这。存储篇会手工mount -t overlay复现整个 Copy-on-Write 过程。最后验证服务本身当然是正常的$curl-s-o/dev/null-wHTTP %{http_code} in %{time_total}s\nhttp://127.0.0.1:8080 HTTP200in0.000629s六、把三板斧拼起来宿主机 Linux 内核 (6.8.0) ──────────────────────────────────────────────────── │ ├─ dockerd ── containerd ── containerd-shim (18262) │ │ clone(CLONE_NEWPID|NEWNS|NEWNET|...) │ ▼ │ nginx master (宿主机视角 PID 18285) │ nginx workers ×8 │ ├─ Namespace: pid/mnt/net/uts/ipc/cgroup 独立 │ time/user 与宿主机共享 (注意!) │ ├─ Cgroup v2: /system.slice/docker-id.scope │ cpu.max / memory.max / io.max ... │ └─ OverlayFS: lower(镜像8层,只读) upper(可写层) → merged rootfsdocker run 的本质解包镜像 → overlay 挂载 rootfs → clone 带 namespace flag 的进程 → 写 cgroup 文件 → chroot/pivot_root 到 merged 目录 → exec 你的 ENTRYPOINT。七、这个认知能帮你解决什么问题有了容器进程的心智模型本系列后面每一个问题都有了统一的排查框架问题现象对应内核机制系列文章kill 不掉 1 号进程PID namespace 内核信号特权进程篇僵尸进程堆积init 进程的 wait 责任进程篇CPU 限了还是慢CFS bandwidth throttle / D 状态进程篇容器被莫名杀死Memory Cgroup OOM内存篇内存总在临界点Page Cache 计入 memory.current内存篇写文件变慢OverlayFS copy-up存储篇磁盘被容器写满可写层无配额存储篇改内核参数不生效/proc/sys 的 netns 归属与只读挂载网络篇网络不通veth → 网桥 → iptables 链路网络篇privileged 滥用Capabilities安全篇排查路径永远是现象 → 找到宿主机上的进程 → 看它的 namespace/cgroup → 定位内核机制 → 解决。小结容器是宿主机上的普通进程不是轻量级虚拟机Namespace 隔离视图默认 6 独立 time/user 共享Cgroup 限制资源OverlayFS 提供分层 rootfsdocker inspect --format {{.State.Pid}}nsenter/sys/fs/cgroup是容器排障三件套Ubuntu 24.04 已是 Cgroup v2 containerd snapshotter 时代老教程的路径要更新了。思考题既然容器进程对宿主机可见那在宿主机上直接kill -9 18285会发生什么容器会退出吗重启策略会拉起它吗欢迎在评论区讨论答案在进程篇揭晓。本文是容器技术底层原理深度实操系列开篇全系列 16 篇覆盖进程、内存、存储、网络、安全五大模块与 perf/ftrace/eBPF 内核调试工具专题。