认识容器:从一个 nginx 容器看透 Namespace 与 Cgroup
2026/7/25 15:28:17 网站建设 项目流程

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

系列开篇| 容器技术底层原理深度实操系列

实验环境:华为云 FlexusX (8vCPU/16GiB) · Ubuntu 24.04 Server · 内核 6.8.0-106-generic · Docker 29.1.3 · Cgroup v2

本文所有命令输出均为真机实录。

一、为什么容器问题的答案不在 docker 命令里

如果你用容器的时间够长,一定遇到过这些"灵异事件":

  • 容器里kill -9 1,1 号进程纹丝不动;
  • 容器内存明明没用满,进程却被 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--format"table {{.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-shim(PID 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 是容器 ID
ipc4026532498 (独立)System V IPC/消息队列容器间共享内存互不可见
pid4026532499 (独立)进程号空间nginx 在容器里是 1 号进程
cgroup4026532500 (独立)cgroup 根视图容器里看/proc/1/cgroup0::/
net4026532501 (独立)网卡/路由/iptables容器有自己的 eth0
time4026531834 (共享)系统时钟与宿主机相同(NPROCS=211)
user4026531837 (共享)uid/gid 映射默认与宿主机共享(这是安全模块的重点)

注意两个细节:

  1. 6 个 namespace 是独立的(NPROCS=9,只有容器里的 9 个进程),而time 和 user namespace 默认与宿主机共享(NPROCS=211,全机进程)。这就是为什么容器里改系统时间会影响宿主机、容器里的 root 默认就是宿主机 root——后面安全篇会专门讲这个坑。
  2. 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: eth0@if9:<BROADCAST,MULTICAST,UP,LOWER_UP>mtu1500qdisc noqueue state UP inet172.17.0.3/16 brd172.17.255.255 scope global eth0

-t 18285 -n表示进入目标进程的 net namespace。看到了容器的172.17.0.3,而且注意eth0@if9——容器的 eth0 其实是一个veth 设备对的一端,另一端 if9 插在宿主机 docker0 网桥上(网络篇会顺着这根"网线"完整排查一次网络不通问题)。

所以记住:docker exec = nsenter 进入全部 namespace + 执行命令。当容器里没有调试工具时(比如上面 nginx 镜像里连 ps 都没有:sh: 1: ps: not found),nsenter只进入部分 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 --cpus=1 -m 512m干的事情,就是把这两个文件分别改成100000 100000536870912。仅此而已——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,容器启动时在顶上加一层可写 upperdir,OverlayFS 把它们"叠"成一个完整的根文件系统。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 entryforkey"GraphDriver"

如果你在新版本 Docker 上照老教程找/var/lib/docker/overlay2/找不到东西,原因就在这。存储篇会手工mount -t overlay复现整个 Copy-on-Write 过程。

最后验证服务本身当然是正常的:

$curl-s-o/dev/null-w"HTTP %{http_code} in %{time_total}s\n"http://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 rootfs

docker 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 → 定位内核机制 → 解决。

小结

  1. 容器是宿主机上的普通进程,不是轻量级虚拟机;
  2. Namespace 隔离视图(默认 6 独立 + time/user 共享),Cgroup 限制资源,OverlayFS 提供分层 rootfs;
  3. docker inspect --format "{{.State.Pid}}"+nsenter+/sys/fs/cgroup是容器排障三件套;
  4. Ubuntu 24.04 已是 Cgroup v2 + containerd snapshotter 时代,老教程的路径要更新了。

思考题:既然容器进程对宿主机可见,那在宿主机上直接kill -9 18285会发生什么?容器会退出吗?重启策略会拉起它吗?欢迎在评论区讨论,答案在进程篇揭晓。


本文是"容器技术底层原理深度实操"系列开篇,全系列 16+ 篇,覆盖进程、内存、存储、网络、安全五大模块与 perf/ftrace/eBPF 内核调试工具专题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询