你大概写过无数遍 docker run,看着一个环境在毫秒之间起好,心里默认"容器就是个轻量级虚拟机"。这个默认认知,恰恰是理解容器最大的障碍。
State & Flow 频道那支《How Docker Containers Actually Work — Namespaces, Cgroups, and Union Filesystems》之所以值得反复看,是因为它干了一件大多数 Docker 教程刻意回避的事:把"容器"这个词从内核里彻底删掉,让你看见底下真正在转的三样东西——namespace、cgroup、联合文件系统。
结论先抛出来:Linux 内核里根本没有"container"这个对象。你 ps 出来的那个进程,和你 docker exec 进去看到的进程,是同一个进程,跑在同一个内核上,挤在同一颗 CPU 上。它之所以显得"被关起来了",纯粹是因为内核对它撒了一系列谎——告诉它"你是 PID 1""这台机器只有一张网卡""根目录长这样"。容器不是被造出来的,是被裁剪出来的。
理解了这一点,你才会明白为什么容器启动只要几毫秒、为什么跨内核架构跑不了、为什么"隔离"和"限流"是两件根本不相干的事。这篇文章就把视频里的内核三件套拆开揉碎,再补上视频没讲透的工程细节,最后落到通信人最关心的场景:5G 核心网为什么非要把网元塞进容器。
| 项目 | 内容 |
|---|---|
| 视频标题 | How Docker Containers Actually Work — Namespaces, Cgroups, and Union Filesystems |
| 频道 | State & Flow(专注云原生与系统底层原理解读) |
| 时长 | 深度讲解长视频,节奏紧凑、代码与图示穿插 |
| 播放量 | 技术圈高口碑,持续被引用为"最该补的一课" |
| 核心主题 | 容器不是虚拟机,本质是 namespaces + cgroups + union filesystem |
这是全片最反直觉的一句话,也是整个容器技术被发明的起点。你去翻 Linux 内核源码,搜 container,搜不到一个叫"容器"的系统调用。内核只提供了一批原语(primitive):clone()、unshare()、setns()、cgroup 文件系统、OverlayFS……所谓"容器",是用户态把这些原语按特定方式组合后涌现出来的效果。
Aqua Security 的 Liz Rice 在那场著名的《Containers from Scratch》演讲里,用大约 100 行 Go 代码就"造"出了一个能跑的容器。核心就一句:调用 syscall.Clone 时带上 CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET | CLONE_NEWIPC 这些标志位,再 pivot_root 换个根目录。就这些。没有魔法,没有 hypervisor,就是几个系统调用。
这个认知的价值在于:它把"容器"从一个黑盒名词,还原成了一组可解释、可调试、可单独开关的开关。下次你排查"为什么容器里看不到宿主机进程",你就知道是 PID namespace 在干活,而不是去怀疑 Docker 出 bug。
很多人把容器类比成"瘦身版虚拟机",这个比喻方便但有害。两者的分野只有一条:谁拥有内核。
虚拟机走的是硬件级虚拟化:hypervisor(KVM、ESXi)模拟出一套硬件,每个 VM 里跑一个完整的客户机内核(guest kernel)。一个 Ubuntu VM 里,那个 systemd、那套网卡驱动、那套调度器,全都是真实存在的第二份拷贝。所以 VM 动辄几个 GB,启动要几十秒——它真的在"开机"。
容器走的是操作系统级虚拟化:所有容器共用宿主机的同一个内核。容器里那个"内核"根本不存在,它调用的系统调用,直接被宿主机内核接住处理。所以一个 Alpine 镜像可以只有 5MB,容器可以毫秒级启动——因为它只是 fork 出一个进程,给它换了一副"眼镜",根本没开机。
这条分野有一个非常硬核的推论,也是面试常考的点:容器必须和宿主机共享内核架构。你不能在 Linux 宿主机上原生跑一个 Windows 容器,因为内核对不上。这恰恰反证了"容器=轻量VM"是错的——如果真有独立的内核,架构本不该是约束。通信圈做 5G 云核心网时,所有 NF(Network Function)都必须基于同一套 Linux 内核镜像,根源就在这里。
如果说容器是一场骗局,namespace 就是骗术本身。它干的事情用一句话概括:控制一个进程能"看见"什么。
Linux 内核到现在一共提供了 8 种 namespace,每一种都对应一种"世界观"的裁剪:
/ 其实是宿主机某个目录被"换根"后的视图。veth 虚拟以太网对)、独立的路由表、独立的 iptables、独立的端口空间。两个容器都能监听 80 端口而不冲突,就靠它。注意一个被反复忽略的事实:这 8 种 namespace 是相互独立的。你可以只给一个进程套上 net namespace(让它有独立网络),却不给它 pid namespace(它能看见宿主机所有进程)。实际上 ip netns 创建网络命名空间、Kubernetes 里 hostNetwork: true、Docker 的 --network host 都是在做"部分隔离"的取舍。namespace 不是非开即关的开关组,而是一个 8 维的布尔向量。
一个最直观的验证:你在容器里 ps aux 看到自己是 PID 1,但跑到宿主机上 ps aux,能找到同一个进程,只是它的 PID 是 4567。进程只有一个,只是两副眼镜看到的编号不同。 这就是"被骗的进程"的字面含义。
namespace 解决了"看得见什么",但完全没解决"能用多少"。一个只套了 namespace 的进程,理论上可以吃光宿主机全部内存、占满所有 CPU、fork 出十万个子进程把系统拖垮。这就是 cgroup(control group)登场的理由。
cgroup 干的是资源记账与限制:把进程分组,对每组施加配额。它的控制器(controller)按资源类型拆开:memory 限内存、cpu/cpuacct 限和统计 CPU、blkio(v2 里叫 io)限磁盘 IO、pids 限进程数、devices 控制能访问哪些设备文件、net_cls/net_prio 给网络流量打标签。你在 Docker 里写 --memory=512m --cpus=0.5,本质就是往 cgroup 文件系统里写两个数字。
cgroup 有 v1 和 v2 两代,区别不小:v1 每种资源一棵独立的层级树,资源之间没法联动(一个进程在内存树和 CPU 树里是两套路径,常打架);v2 改成统一层级(unified hierarchy),一棵树管所有控制器,2014 年合入内核,到现在 Docker/K8s 才基本完成迁移。这事工程上坑极多,是很多"为什么我的 cgroup 限制不生效"问题的根源。
这是我这篇解读最想强调、也是视频点破的一个概念性洞见:namespace(隔离)和 cgroup(限流)是正交的两条轴,谁也不包含谁。
把它们摆在一个二维坐标系里就清楚了:
一个真正的生产容器,必须两条轴都拉满:既看不见宿主机别的进程,也不能吃光内存。但现实中很多人把它们混为一谈,说"Docker 提供了资源隔离"——这句话是错的,或者说至少是不精确的。Docker 提供隔离(namespace)+限制(cgroup),这是两件事。
正交性带来的实战推论很有意思:
- 只隔离不限流:容器能看到的世界很小,但一个失控的 Java 进程照样能 OOM 整台宿主机,把隔壁容器一起带走(这是 K8s 早期"邻居噪音"问题的根)。
- 只限流不隔离:一个进程被限了 CPU,但它能 cat /etc/passwd 读到宿主机所有用户、能看到所有进程的网络连接——限制毫无安全意义。
通信云核心网里,UPF(用户面功能)转发流量大、内存敏感,cgroup 限流是性能 SLA 的命根子;而 AMF/SMF 处理信令、涉及用户数据,namespace 隔离是安全合规的底线。同一种技术,两条轴的权重完全不同。理解了正交性,你才会针对性地配 resource request/limit 和 securityContext,而不是无脑复制 YAML。
namespace 和 cgroup 解决了"进程"的问题,但容器还得有个"文件系统"——这就是视频标题里第三个关键词 Union Filesystem(联合文件系统)出场的地方。它解释了一个常被忽略的奇迹:为什么 100 个容器基于同一个镜像,磁盘上只存一份。
OverlayFS(以及早期的 AUFS、device-mapper thin pool)的核心是分层 + 写时复制(Copy-on-Write)。一个容器运行时的文件系统视图,由几层拼出来:
关键机制是 copy_up:当容器要修改一个来自 lowerdir 的文件时,OverlayFS 不会去动只读层,而是先把这个文件复制到 upperdir,再在副本上改。这就是写时复制。
这套机制带来三个工程后果,每一个都和你日常相关:第一,镜像可以共享层,pull 一个新镜像时已存在的层会跳过,所以拉镜像才那么快。第二,容器是临时的,删掉容器,upperdir 里的改动全没了——这就是为什么"别把数据写进容器,要用 volume"。第三,写性能有额外开销,第一次修改一个只读层文件会触发 copy_up,IO 密集场景要留意。Docker 镜像之所以设计成一层层 RUN 指令,本质就是在和 OverlayFS 的分层模型对齐,把变动频繁的层放上面,提升共享率。
namespace+cgroup+unionfs 已经能跑出一个像样的容器,但安全上还有两个洞,视频点到为止,我展开讲。
第一个洞:系统调用没拦。Linux 有大约 300 个系统调用,其中不少是给特权操作用的——reboot 重启机器、ptrace 窥探别的进程、mount 挂载文件系统。即便容器里的 root 映射成了宿主机的 nobody,有些 syscall 仍可能被滥用。seccomp(secure computing mode) 就是来堵这个洞的:它用 BPF 过滤器在内核里规定"这个进程只能调哪些 syscall"。Docker 默认的 seccomp profile 大约屏蔽了 44 个危险 syscall。
第二个洞:root 权限太粗。传统 Unix 的 root 是个二进制开关,要么全能要么无能。Linux capabilities 把 root 拆成了约 40 个细粒度权限,比如 CAP_NET_BIND_SERVICE(绑定 1024 以下端口)、CAP_SYS_ADMIN(几乎等于万能,尽量别给)、CAP_NET_RAW(发原始网络包)。容器安全的核心原则是 drop all, then add only needed——先把所有 cap 删掉,再按需加回去。K8s 的 Pod Security Standards 里 restricted 级别干的就是这件事。
把 seccomp、capabilities 和前面三件套叠在一起,才是一个真正生产可用的容器安全模型。这也是为什么"容器逃逸"(container escape)一直是云安全的高危话题:只要这些原语里任何一层配错,进程就可能突破谎言、看到真实世界。
把前面所有零件串起来,你执行 docker run nginx 时,背后是这么一条链:
flowchart TD
A["docker run nginx<br/>(CLI 命令)"] --> B["dockerd<br/>(守护进程,解析参数)"]
B --> C["containerd<br/>(高层运行时,管生命周期)"]
C --> D["runc<br/>(OCI 低层运行时,符合 runtime-spec)"]
D --> E["Linux 内核原语"]
E --> F["namespaces<br/>裁剪视野"]
E --> G["cgroups<br/>限制资源"]
E --> H["OverlayFS<br/>分层文件系统"]
E --> I["seccomp + capabilities<br/>收窄权限"]
F & G & H & I --> J(("一个'被骗'的普通进程<br/>= 容器"))
style J fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
style E fill:#fff3e0,stroke:#ef6c00
这条链对应一个行业标准 OCI(Open Container Initiative),它定义了两份规范:image-spec(镜像长什么样)和 runtime-spec(容器实例长什么样)。runc 是 reference implementation。Docker、containerd、Kubernetes 的 containerd runtime、CRI-O,底层最后都汇聚到 runc,再汇聚到那几个内核原语。理解了这条链,你就理解了为什么"换容器运行时"(比如 Docker 换 containerd)对应用透明——因为 OCI 把接口标准化了。
flowchart LR
subgraph 神话["大众认知的'容器'"]
M["一个黑盒<br/>≈ 轻量虚拟机"]
end
subgraph 真相["内核里真实存在的原语"]
N["namespaces<br/>8种 · 管视野/隔离<br/>'看不见'"]
C["cgroups<br/>v1/v2 · 管资源/限制<br/>'用不了'"]
U["unionfs/OverlayFS<br/>分层+写时复制<br/>'磁盘省一份'"]
S["seccomp + capabilities<br/>收窄syscall与特权<br/>'干不了坏事'"]
end
M -.被还原为.-> N
M -.被还原为.-> C
M -.被还原为.-> U
M -.被还原为.-> S
N --> P["正交的两条轴"]
C --> P
P --> R(("容器 = 普通进程<br/>+ 一组精心组合的内核谎言"))
style M fill:#ffebee,stroke:#c62828
style R fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
style P fill:#e3f2fd,stroke:#1565c0
对通信工程师(这是写给你看的): 5G 核心网从 Release 16 起全面云原生化,AMF、SMF、UPF、PCF 全部以容器形态部署在 Kubernetes 上(即所谓的 CBA,Cloud-native Based Architecture)。理解 namespace+cgroup,你才看得懂为什么 UPF 要单独做资源隔离(cgroup 限 CPU/内存保转发 SLA),为什么不同租户的 NF 要强 namespace 隔离(多租户安全),为什么容器网络(CNI、Calico/VXLAN)本质是在 net namespace 之间架 veth pair 和路由。容器不是通信人可以跳过的"互联网技术",它就是下一代核心网的运行底座。
对后端/云原生开发者: 第一,排查问题换个坐标系——容器里 OOM 了,先去宿主机看那个真实 PID 的 cgroup memory.usage_in_bytes,而不是只看应用日志。第二,写 Dockerfile 时脑子里要有 OverlayFS 分层图,把变动少的放底层、变动多的放上层,合并 RUN 减少层数,缩小镜像。第三,做安全加固时走"默认拒绝"——cap drop all、seccomp default、非 root 用户运行,按需放开,而不是反过来。
对所有写代码的人: 这套"内核不提供容器,只提供原语;容器是原语组合的涌现"的设计哲学,是 Unix 哲学的极致体现——不要造大而全的新概念,要提供正交、可组合的小原语。你设计自己的系统时,与其定义一个"插件"巨对象,不如想清楚有哪几条独立的轴,让用户自由组合。这正是容器能横扫整个软件交付链的根本原因:它没有发明任何新东西,只是把内核已有的几块乐高,拼出了一个所有人都想要的形状。
lizrice/containers-from-scratch):100 行 Go 手搓容器,看完"容器"祛魅彻底完成。Documentation/cgroups/:前者是底层位运算圣典,后者是 cgroup v1→v2 迁移权威说明。create.go 更快的路。Linux 内核里从来没有"容器",只有一个被 namespace 捏造了世界观、被 cgroup 限制了配额、靠 OverlayFS 省下磁盘的普通进程——理解了这场精心设计的"骗局",你就拿到了云原生时代的底层源代码。
本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
