如果你问一个程序员"什么是 Docker 容器",十有八九会得到这样的答案:"一种轻量级的虚拟机"。这个回答听起来很合理,但它恰恰是整个云原生时代最大、也最危险的误解。容器根本不是虚拟机,它甚至没有任何"虚拟"的成分——它就是一个普普通通的 Linux 进程,只是被悄悄加上了几道枷锁。
Docker 官方频道上有一个播放量近 26 万、时长 54 分钟的演讲《Cgroups, namespaces, and beyond: what are containers made from?》,演讲者是容器圈赫赫有名的 Jérôme Petazzoni(网名 jpetazzo,前 Docker 员工)。这个视频之所以值得反复看,不是因为它炫酷,恰恰相反——它的乐趣在于亲手把魔术拆穿。Petazzoni 在台上不用 Docker,只用 Linux 自带的 unshare、mount、cgcreate 几个命令,徒手"捏"出一个能跑的容器,向你证明:所谓容器,没有任何黑魔法,它只是把内核里早就存在的两个特性,巧妙地拼在了一起。
这篇解读,我们就沿着这条"拆魔术"的路线,把容器打回它的原形。
| 项目 | 内容 |
|---|---|
| 视频标题 | Cgroups, namespaces, and beyond: what are containers made from? |
| 频道 | Docker(官方) |
| 演讲者 | Jérôme Petazzoni(jpetazzo) |
| 时长 | 54 分 25 秒 |
| 播放量 | 约 26 万(261,048) |
| 发布日期 | 2015 年 12 月 3 日 |
| 核心主题 | 容器的内核本质:namespaces + cgroups + 联合文件系统 |
这是整场演讲的破题之笔,也是你重新理解容器的起点。要理解这个反差,先得搞清楚虚拟机(VM)到底在干什么。
虚拟机的本质是模拟一整套硬件。你的物理机上跑着一个叫 Hypervisor(如 VMware、KVM)的软件,它在你的 CPU、内存之上,凭空造出一套"假的"主板、CPU、网卡,然后在上面安装一个完整的客户机操作系统(Guest OS)。一个 VM 里面,有自己的内核、自己的 init 进程、自己的一整套设备驱动。启动一个 VM,就是在启动一台真正的电脑,要经过完整的开机自检、内核加载、服务初始化——慢则几十秒,快也要十几秒。
容器完全不是这条路线。容器共享宿主机的内核,根本没有自己的内核,也没有 Hypervisor。当你在终端敲下 docker run,Docker 做的事情,本质上是向宿主机内核发起一次 fork,创建出一个新进程——和你平时写的 ./my_program 没有任何结构上的区别。这个进程和你电脑上的浏览器、微信、终端,共享同一个内核、同一套系统调用。
Petazzoni 反复强调一句话:"Containers are just regular processes."(容器就是普通进程)。VM 隔离靠的是"再造一台机器",容器隔离靠的是"给同一个内核里的进程,戴上一副特殊的眼镜",让它以为世界只有自己。一个 VM 跑起来要几 GB 内存装系统,一个容器往往只要几十 MB——差距的根源就在这里:容器没有"第二套操作系统"这个昂贵的包袱。
既然容器是共享内核的普通进程,那它是怎么做到"互相看不见"的?答案就是 Linux 的 namespaces(命名空间) 机制。
Namespace 干的事情,可以用一个词概括:视角隔离。它让一个进程看到一个"被裁剪过的系统视图",让它产生"这台机器是我一个人的"这种错觉。Linux 内核里内置了好几种 namespace,每一种负责隔离一类资源。容器组合使用它们,层层叠加出隔离效果:
/(根目录)和宿主机完全不同——它看到的是镜像提供的文件树,而不是宿主机真实的磁盘内容。这里有个关键细节(视频原话之外我的补充):这些 namespace 不是一次性都有的,而是分批进入 Linux 内核的。最早的是 mount namespace,2002 年随 Linux 2.4.19 进入;网络 namespace 到 2009 年(2.6.29)才完整;而让"非 root 用户也能安全跑容器"的 user namespace,直到 2013 年(内核 3.8)才姗姗来迟。也就是说,容器所需的底层能力,内核社区花了十几年时间,一块一块地"凑"齐——这正是为什么 Docker 直到 2013 年才诞生,技术上的所有零件其实早已就位。
Namespace 解决了"隔离"——让进程互相看不见。但它不管"限量"。这是初学者最容易踩的坑:如果只有 namespace,一个容器完全可以发起 fork 炸弹、占满宿主机全部 CPU 和内存,把整台机器拖死,因为它和宿主机共享内核、共享物理资源。
补上这个缺口的,是 Linux 的第二根支柱——cgroups(control groups,控制组)。如果说 namespace 管的是"看见什么",那 cgroup 管的就是"能用多少"。
Cgroup 让你把一组进程归到一个"组"里,然后对这个组施加资源限制:最多能用几个 CPU 核心、最多吃多少内存、读写磁盘的速度上限是多少、能不能访问某个硬件设备。它还顺带做记账(accounting)——记录这个组实际消耗了多少资源,这正是云厂商能按容器精确计费的基础。
一个完整的容器,必须是 namespaces(隔离) + cgroups(限制) 的组合拳,缺一不可。Petazzoni 在演讲里用了一个很精准的对应关系:namespace 给的是"幻觉",cgroup 给的是"配额"。只给幻觉不给配额,就是一个能看不能限的"逃逸型"容器;只给配额不给幻觉,进程之间还是会互相串扰。
补充一点历史:cgroups 是 2007 年由 Google 的工程师(Paul Menage 等人)提交给 Linux 内核的,2008 年随 2.6.24 合入。Google 内部早在 Docker 出现之前,就靠"namespace + cgroup"这套组合,支撑了百万级服务器的 Borg 系统。换言之,容器技术最先被一家搜索巨头在内部磨砺了五六年,Docker 只是把这套"内部工程师才会用"的能力,包装成了人人能上手的产品。
光有隔离和限制,还凑不出 Docker 那个让你 docker pull 一下就能用的"镜像"体验。这块拼图叫联合文件系统(UnionFS,Union File System),Docker 里最常用的是 overlayfs(早期是 aufs)。
普通文件系统是一个层级清晰的真实目录树。而联合文件系统的精髓是"分层叠加":它把多个目录(层,layer)像透明幻灯片一样叠在一起,对外呈现成一个统一的文件系统视图。下层是只读的,最上面盖一层"可写层"。
Docker 的镜像就是按这个思路构造的:一个 Ubuntu 基础层(只读)+ 你 apt install 装的软件层(只读)+ 你的应用代码层(只读)……所有这些只读层叠在一起构成镜像。当你 docker run,Docker 在最上面再盖一层可写层(container layer)——容器运行时所有文件的修改,都写进这一层,下面的镜像层一个字节都不动。
这个设计带来两个巨大的好处:第一,极致复用。10 个容器都基于同一个 Ubuntu 镜像,宿主机磁盘上只存一份 Ubuntu 层,10 个容器共享它,省下了海量存储。第二,镜像可以像积木一样继承。你 FROM python:3.12,就是站在别人已经叠好的 Python 层之上继续往上盖。如果没有联合文件系统,容器生态里"镜像仓库、镜像分层、镜像复用"这一整套优雅的体系根本无从建立。
如果你只能记住这场演讲的一个画面,应该是这个:Petazzoni 关掉 Docker,打开一个裸的 Linux 终端,然后当着台下几百人的面,用最朴素的内核工具,一步步手工拼出一个容器。
大致过程是这样的:他用 unshare 命令创建一个全新的 PID/mount/network namespace,让随后的进程"掉进"这个隔离的世界;用 mount 挂载一个 overlayfs,给这个隔离世界提供一个根文件系统;用 cgcreate/cgexec 把进程塞进一个 cgroup,限制它的 CPU 和内存。最后,他 chroot 进去(或者用 pivot_root),一个活生生、能跑命令、和网络宿主隔离、资源被限定的"容器"就诞生了——全程没有调用任何一行 Docker 代码。
这一幕的震撼之处在于:它把"容器"从一个神秘的黑盒,还原成了几个 Unix 命令的组合。看完你会恍然大悟——Docker 那个庞然大物,底层做的事情,和 Petazzoni 在终端里敲的这几行,本质完全一样。Docker 的伟大不在于发明了什么新内核机制,而在于把这套需要内核专家才能驾驭的晦涩操作,封装成了一条 docker run。这才是产品力的真正定义:不是创造新原理,而是让已有的原理变得人人可用。
很多团队把"容器隔离"等同于"安全隔离",这是一个会酿成事故的致命误解。这是视频精神延伸出来、但极其重要的一个认知。
Namespace 提供的是逻辑上的视角隔离,不是安全上的强隔离。原因很直接:容器和宿主机共享同一个内核。这意味着,一旦容器内的进程通过某个内核漏洞(比如这几年屡屡爆出的各类 CVE),找到了从 namespace 里"跳出来"的路径,它就能直接接触到宿主机内核,进而控制整台物理机——这就是所谓的容器逃逸(container escape)。历史上这类漏洞层出不穷,容器逃逸一度是云安全攻防的热门课题。
正因为共享内核的隔离是"软"的,对安全要求极高的场景,业界发展出了更强的方案:Kata Containers 和 gVisor。Kata 给每个容器套上一个微型虚拟机,用 VM 级别的硬件强隔离来弥补容器隔离的薄弱;gVisor 则在容器和内核之间塞一个用户态的"假内核"(用 Go 写的),拦截并重写系统调用。你可以把传统容器的隔离强度理解为"一堵薄墙",而 VM 是"一整条护城河"。理解了这个差异,你才知道什么时候该用普通容器、什么时候必须上强隔离运行时。
理解容器,离不开一条贯穿四十年的技术演进线。Docker 远不是凭空出现的发明,它是这条线上最会做产品的一站。
最早的隔离思想可以追溯到 1979 年的 chroot 系统调用——它能让一个进程看到的根目录被"换"到一个子目录里,从而把它困在一个文件系统的笼子(chroot jail)里。2000 年,FreeBSD 把这个思想发扬光大,做出了 jail,隔离了更多东西。进入 Linux 后,前文说的 namespaces 从 2002 年起分批进入内核,cgroups 于 2007 年由 Google 提交。2008 年,LXC(Linux Containers)把这些能力整合成一套可用的工具。直到 2013 年 3 月,一家叫 dotCloud 的小公司发布了 Docker,把这套东西重新包装,才引爆了整个行业。
这条脉络给出的最大启示是:真正改变世界的技术突破,往往不是"发明新原理",而是"重新组合已有的原理,并把它做到易用"。namespaces 和 cgroups 在 Docker 诞生前,已经在 Linux 内核里躺了五六年,却一直只有 Google 这样的巨头在内部用。Docker 做的,是给这套"工程师专属"的能力,套上 Dockerfile、镜像仓库、一条命令部署的外壳。这和当年 GUI 把命令行变成大众产品、HTTP 把互联网变成万维网,是同一种创新逻辑——降低使用门槛,比提升技术上限更有颠覆力。
视频是 2015 年录的,但到今天,容器底层的"两根支柱"——namespaces 和 cgroups——一个字节都没有变。变的只是外面那层壳。
Docker 之后,容器生态迅速标准化:OCI(Open Container Initiative) 制定了镜像格式和运行时的统一标准。今天的"容器运行时"早已分化——runc 是最底层的 OCI 参考实现,containerd 是更上层的、被 Kubernetes 直接调用的运行时,CRI-O 是红帽主导的专为 K8s 设计的运行时。Docker 本身的地位,也从"唯一选择"退化为众多运行时之一。而在编排层,Kubernetes 成了事实标准,把成千上万个容器调度到成千上万台机器上。
但无论你用的是 containerd 还是 CRI-O,是 Docker 还是 K8s,每一个容器落到最后一层,依然是 unshare 出来的 namespace、cgcreate 配出来的 cgroup、overlayfs 叠出来的文件系统。Petazzoni 在 2015 年徒手敲的那几行命令,到 2026 年依然是所有云原生基础设施的物理基础。这就是理解"底层原理"的价值——框架每两三年换一茬,原理十年不变。
flowchart TB
subgraph VM["虚拟机 (VM)"]
A1["App"] --> A2["Guest OS 完整内核"]
A2 --> A3["Hypervisor 模拟硬件"]
end
subgraph CT["容器 (Container) — 共享宿主内核"]
B1["App 进程"] --> B2["可写层 overlayfs"]
B2 --> B3["只读镜像层"]
end
A3 -.共享.-> K["宿主机 Linux 内核"]
B3 --> K
K --> NS["支柱一: Namespaces 视角隔离"]
K --> CG["支柱二: Cgroups 资源限制"]
NS --> N1["PID / Mount / Network<br/>UTS / IPC / User"]
CG --> C1["CPU / 内存 / IO 限制 + 记账"]
NS -.组合.-> BOTH["容器 = 隔离 + 限制"]
CG -.组合.-> BOTH
HIST["历史脉络<br/>1979 chroot → 2000 jail<br/>2002 namespaces → 2007 cgroups<br/>2008 LXC → 2013 Docker"] --> BOTH
BOTH --> SEC["安全边界: namespace≠安全<br/>高安全用 Kata / gVisor 强隔离"]
理解了"容器就是受限制的进程",对你日常开发有一连串立竿见影的好处。
第一,调试容器不再是黑盒。 当一个容器里的程序行为诡异,你不必把容器当成"一个神秘的小型电脑"。docker exec -it 进去,本质上就是用 nsenter 把你的 shell"塞进"那个进程的 namespace 里。理解了这点,你就知道为什么容器里看不到宿主机的进程、为什么 ps aux 只列出几个进程——你是在一个被裁剪过的 PID namespace 里。出了容器,回到宿主机 ps,你依然能看到那个容器进程,它就在那儿,带着真实的 PID 和真实的资源占用。
第二,资源限制要主动配置。 很多人 docker run 完就不管了。但如 Petazzoni 所言,没有 cgroup 限制的容器,理论上能吃光整台机器。生产环境务必显式设置 --cpus、--memory 上限,并配置 OOM(内存溢出)策略。否则一个有内存泄漏的容器,能拖垮宿主机上所有邻居——这是共享内核架构的固有风险。
第三,构建更小的镜像。 既然镜像是分层叠加的只读层,每一层都会增加体积和攻击面。去掉不必要的依赖、用多阶段构建(multi-stage build)、选择 alpine 或 distroless 这类极简基础镜像,能让你的镜像从几百 MB 缩到几十 MB——更小的镜像意味着更快的拉取、更少的漏洞、更低的成本。
第四,安全模型要心里有数。 别把容器当成坚不可摧的沙箱来运行不信任的代码。如果是要跑第三方、不可信的负载,请用 gVisor 或 Kata 这类强隔离运行时。理解"namespace 隔离是软隔离"这一条,能让你在设计多租户系统时少踩大坑。
第五,破除对工具的迷信。 Docker 不是魔法,K8s 也不是。它们都是建立在几十年 Unix 哲学和 Linux 内核特性之上的封装。带着"这底下到底在跑什么进程、调了什么系统调用"的好奇心去用工具,你会比同龄人更快地定位问题、更快地学会新技术——因为你看穿了表象,直抵了那层十年不变的内核原理。
unshare --pid --fork --mount-proc /bin/sh,亲手体验"一秒进入隔离世界"。Documentation/admin-guide/cgroup-v2.rst 和 namespaces 的 man page,权威且不算太难读。容器没有任何黑魔法——它就是一个被 namespaces 隔离、被 cgroups 限制、跑在 overlayfs 上的普通 Linux 进程,Docker 的全部天才,只在于把这套内核特性包装成了一条人人能敲的命令。
本文由「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 等场景全覆盖。
