起因#
第一次接触 Docker 是 2017 年,在深圳网警机房驻场做审核项目。机房完全没有外网,模型、依赖、运行环境全靠 U 盘拷贝 docker image 来传输。后来做模型转换系统,再到量化行业负责机器学习平台,和 Docker、K8s 打了不少交道。但距离上一次系统梳理 namespace、cgroup 这些底层机制,已经过去好几年了。
最近重新回顾这些概念,发现真正值得重新记住的,不是 Docker 的命令或者具体实现细节,而是几个非常核心的系统设计思想。
有一个说法很常见:「Docker 是一种轻量级虚拟机。」
作为直觉类比,这个说法不算完全错误。但如果真的用它来理解 Docker,反而容易错过最关键的东西。
一个 sleep 进程的真相#
运行一条命令:
1docker run ubuntu sleep 10000直觉上好像是「启动了一台 Ubuntu 机器,上面跑了一个 sleep」。
但在宿主机 kernel 看来,并没有凭空多出一台 Ubuntu 机器。真正出现的核心实体,只是一个普通的 sleep 进程。
这个进程仍然:
- 被宿主机 Linux kernel 调度
- 使用宿主机的 CPU 和 memory
- 进行 system call 时进入宿主机 kernel
- 可以从宿主机用
ps看到
Container is a process, not a machine。
Docker 做的事情,不是复制一套 kernel,而是让这个进程看到一个被重新组织过的系统视图。
这个区别,是理解后面所有机制的起点。
环境不应该依赖机器状态#
在讨论 Docker 怎么做到「隔离」之前,先退一步想:它到底在解决什么问题。
传统的部署模型是这样的:
1Application
2→ 登录目标机器
3→ 安装依赖
4→ 修改配置
5→ 让机器变成 application 需要的状态问题在于 machine state 是 mutable 的。不同机器经过不同历史操作之后,会积累大量隐式状态——某个包被手动升级过,某个环境变量被改过,某个配置文件被另一个项目覆盖过。这些 drift 很难追踪,也很难复现。
Docker 做了一个关键的转变:
不再反复修改目标机器来适配 application,而是把 application 需要的 userspace environment 打包成一个独立的 artifact。
这个思路后面还会展开。先记住这个前提:Docker 要解决的核心问题,是让运行环境不再依赖目标机器的当前状态。
Machine abstraction vs Process abstraction#
回到「Docker 不是虚拟机」这个话题。
VM 和 container 的区别,经常被描述为「VM 更重,container 更轻」。这个说法没错,但停在这里会错过真正重要的东西。
真正的区别在于抽象层级。
VM 做的事情是:
virtualize a machine
它模拟出一整套虚拟硬件,在上面运行一个完整的 guest kernel 和 userspace。应用跑在 guest OS 里,和 host OS 之间隔了一层 hypervisor。
Container 做的事情是:
isolate a process environment
它不模拟硬件,也不启动新 kernel。所有 container 里的进程,共享同一个 host kernel。Docker 只是给不同进程构造不同的系统视图。
1VM: Container:
2
3Application Application
4Userspace (guest) Userspace
5Guest Kernel ─── (shared) ───
6Virtual Hardware Host Kernel
7Hypervisor
8Host KernelDocker 不是找到了一种「更小的虚拟机」,而是发现很多场景根本不需要虚拟化一台机器。 如果大家共享同一个 Linux kernel,那么只需要给不同进程构造不同的系统视图就够了。
这就引出了下一个问题:怎么给一个进程构造「不同的系统视图」?
Namespace:同一个 kernel,不同的世界#
答案是 Linux namespace。
Namespace 最核心的思想可以用一句话概括:
同一个 kernel,可以让不同进程看到不同的世界。
Namespace 不是 Docker 发明的,它是 Linux kernel 提供的 primitive。Docker 在其上做了工程封装。
具体来说,Linux 提供了多种 namespace,每种隔离一类系统资源的可见性。
PID Namespace#
核心问题:这个进程能看到哪些进程?
在宿主机上,进程列表可能是:
1PID 1 systemd
2PID 300 sshd
3PID 5231 python在 container 内部,同一个 python 进程看到的是:
1PID 1 python这不是两份 python。这是同一个 process,在不同 PID namespace 中具有不同的 PID。在 host namespace 里它是 PID 5231,在 container namespace 里它是 PID 1。
一个很直观的事实:container 默认看不到 host 上的其他进程,但 host 可以看到 container 里的进程。
这再次说明 container process 从未离开 host kernel。
Mount Namespace#
核心问题:这个进程看到哪棵 filesystem tree?
运行 docker run ubuntu 之后,container 里看到的是:
1/bin
2/etc
3/usr
4/lib
5...这看起来像一个完整的 Ubuntu 系统。但实际上并没有启动一个 Ubuntu OS。
更准确的描述是:给这个 process 提供了一套 Ubuntu userspace 的 root filesystem,并通过 mount namespace 让它把这棵树当成自己的 /。
Host 上的 / 和 container 里的 / 是不同的 mount point,各自独立。进程在自己的 mount namespace 里只能看到属于自己的 filesystem tree。
顺便澄清一点:filesystem isolation 并不是和 namespace、cgroup 完全并列的一套独立 primitive。它主要涉及 mount namespace、rootfs(root filesystem)和 mount 操作。Docker image 里打包的 filesystem layers 通过 OverlayFS 等机制组合成最终的 rootfs,再通过 mount namespace 挂载给进程。OverlayFS 的实现细节不在本文范围,这里只需要知道:container 看到的文件系统,是 mount namespace + rootfs 共同构造的结果。
Network Namespace#
核心问题:这个进程看到哪套 network stack?
每个 network namespace 可以拥有自己独立的:
- network interface
- IP 地址
- 路由表
- port 空间
所以两个 container 可以各自监听 :80,互不冲突。因为它们在不同的 network namespace 里,各自有独立的端口空间。
Docker bridge、iptables、NAT 这些实现细节不在本文范围。核心仍然是同一件事:给 process 构造一套独立的 network view。
UTS Namespace#
控制 hostname 和 domain name。
这就是为什么每个 container 可以有自己的 hostname,而不影响 host 或其他 container。
IPC Namespace#
隔离 System V IPC 资源:shared memory、semaphore、message queue。
不同 container 的进程无法通过这些 IPC 机制互相通信。
汇总#
Namespace 的统一思想是给进程构造不同的 resource view:
| Namespace | 隔离的 view |
|---|---|
| PID | 进程视图 |
| Mount | 文件系统视图 |
| Network | 网络视图 |
| UTS | 主机名 |
| IPC | 进程间通信 |
所有这些 namespace 做的都是同一件事:改变一个进程看到的世界。
cgroup:看不到彼此,不代表抢不到资源#
Namespace 已经让两个 process 互相看不到对方。但这并不意味着它们不会互相影响。
一个 process 即使在独立的 namespace 里,仍然可以:
- 跑满所有 CPU core
- 吃光整台机器的 memory
- 打满磁盘 IO
因为 namespace 隔离的是 view,不是 resource 本身。
于是 Linux 还需要第二个维度:resource control。这就是 cgroup(control group)。
Cgroup 的核心 abstraction 是:把一组 process 组织在一起,对它们做资源 accounting 和资源限制。
最常见的控制维度:
- CPU:限制一组进程能用多少 CPU 时间
- Memory:限制一组进程能用多少物理内存,超限可以触发 OOM kill
- IO:限制磁盘读写带宽
cgroup v1/v2 的差异、controller 参数、filesystem 接口,都是实现层面的细节,这篇不涉及。
这一节最重要的是形成这对 pair:
1Namespace → isolation of view (能看到什么)
2cgroup → resource control (能用多少)Namespace 回答 “what can you see”,cgroup 回答 “how much can you use”。 两者配合,才构成了 container isolation 的基础。
Container 的最小 mental model#
到这里,可以给出一个非常简洁的公式:
1Container = Linux Process + Namespaces + cgroups + rootfs这不是严格意义上的完整 implementation definition——真实的 container runtime 还涉及 seccomp、capabilities、apparmor 等安全机制。但作为用于理解 container 的 mental model,这四个要素已经足够。
一个 container,本质上就是一个被 namespace 隔离了系统视图、被 cgroup 限制了资源用量、被赋予了一套独立 rootfs 的 Linux 进程。
顺便澄清一个常见的说法:「one process per container」。
这个说法容易被误读为「Docker 容器只能运行一个进程」,这不准确。Container 通常有一个作为 PID 1 的主进程,container 的 lifecycle 与这个主进程绑定——主进程退出,container 就停止。但这个主进程当然可以 fork 出 worker 或 child process。
Docker 鼓励的实际是 one primary responsibility per container——每个 container 承担一个主要职责,而不是 technically 只有一个 Unix process。
Docker 真正改变的事情#
Namespace、cgroup、chroot 这些机制都不是 Docker 发明的。Linux container 的技术基础在 Docker 出现之前就已经存在了(LXC、cgroups 等)。
Docker 真正改变行业的地方,很大程度上不在于 container 隔离本身,而在于它把这些 Linux primitives 包装成了一套极其好用的 application packaging 和 distribution model。
这套模型的核心流水线:
flowchart LR
A[Dockerfile] -->|docker build| B[Image]
B -->|docker push| C[Registry]
C -->|docker pull| D[Image]
D -->|docker run| E[Container]
以前交付一个应用,交付的是一份「如何配置机器」的说明书——装什么版本的 Python,改哪个配置文件,设置哪些环境变量。
Docker 之后交付的是一个已经构建好的 runtime artifact。
这个转变可以类比编译:
1Source Code ──compile──▶ Binary
2
3Source Code
4+ dependencies
5+ userspace filesystem ──docker build──▶ Image编译把源码变成可执行文件。Docker build 把源码连同依赖、配置、整个 userspace 环境一起,变成一个可运行的 image。
于是 image 天然具有这些属性:
- Immutable:build 完成后不可修改
- Versioned:可以打 tag,追踪版本
- Distributable:可以推到 registry,任何地方拉取
- Reproducible:同一份 image,在任何机器上运行行为一致
Docker 把 runtime environment 本身变成了 immutable artifact。 这是工程范式的变化。
回想 2017 年在网警机房,靠 U 盘拷贝 docker image 到离线环境——本质上做的就是把一个完整的 runtime artifact 通过物理介质分发。不需要在目标机器上重新安装任何依赖,docker load 之后直接 docker run。environment 不再依赖目标机器的状态,因为 environment 已经在 image 里了。
这个思想后来成为现代 deployment 和 orchestration 的重要基础。
结论#
Container is a process, not a machine。 Container 中的 process 仍然只是 host kernel 上的普通 Linux 进程,没有独立的 kernel,没有虚拟硬件。
Namespace 构造进程看到的世界,cgroup 控制进程能消耗多少真实资源。 这两个 Linux primitive 配合,构成了 container isolation 的基础。
Docker 不只是隔离进程。它还把 application runtime environment 变成了一个可以 build、version、distribute、run 的 artifact。 这才是 Docker 真正重要的工程价值。
参考资料#
- Namespaces in operation, part 1: namespaces overview - LWN.net
- cgroups - Linux manual page
- Docker Overview - Docker Documentation
- What even is a container: namespaces and cgroups - Julia Evans