跳过正文
  1. Posts/

从进程视角重新理解 Docker

·3858 字·8 分钟
目录

起因
#

第一次接触 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 Kernel

Docker 不是找到了一种「更小的虚拟机」,而是发现很多场景根本不需要虚拟化一台机器。 如果大家共享同一个 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 真正重要的工程价值。

参考资料
#

🏛️ 111qqz 的技术博客 · 15 年博客历史 (2011 - 2026)
发布于 2026-09-10

💡 觉得有启发?欢迎互动交流!

如果你在阅读、编译运行或系统优化中有任何疑问、思考或更好的解法,欢迎在下方发表评论,或通过邮件直接探讨。

本文链接:https://111qqz.com/2026/09/docker-process-not-machine/ 知识共享署名-非商业性使用 4.0 国际许可 (CC BY-NC 4.0)

相关文章

docker network 与 本地 network 网段冲突

·605 字·2 分钟
起因: # 公司部署在hk的爬虫服务器突然挂掉了。后来发现只是在深圳办公区无法访问。排查后发现原因是docker的网络(包括docker network的subnet或者是某个容器的ip)与该host在内网的ip段相同,导致冲突。