Heath Kang

Docker 学习

刚开始用 Docker 时,我最困惑的其实不是 docker run 的参数,而是那句常见的话:“Docker 比虚拟机轻。” 轻在哪里?它省掉了什么?既然容器里看上去也有 /bin/usr 和一个独立 IP,它和一台虚拟机的能力边界又在哪里?

后来一点点拆开看,我把 Docker 暂时理解成三个层次的产品设计:借 Linux 内核做进程隔离与资源控制;借 Linux 网络把进程接到网络里;借分层文件系统分发应用,并把数据从容器生命周期中抽出来。Docker 并没有重新发明这些内核能力,但把它们包装成了镜像、容器、网络、卷这些可交付的对象。

先和虚拟机划一条线:Docker 没有自己的 Linux kernel

虚拟机通常先由 hypervisor 虚拟一组硬件,再在其中启动一个客体操作系统和它自己的 kernel。Docker 容器则是宿主机上的一组普通进程:它有独立视图和资源约束,却仍通过宿主机 kernel 执行系统调用。

虚拟机:应用 → Guest OS → Guest kernel → 虚拟硬件 → Host kernel
容器:  应用 → 容器用户空间 ────────────────→ Host kernel

少启动一个操作系统和 kernel,意味着镜像更小、进程启动更快、同一台机器能放下更多工作负载。可这也不是免费的“轻量虚拟机”:容器共享内核,不能随意携带另一个 Linux kernel;内核漏洞、特权配置和过宽的 capability 也会让隔离边界变弱。

这也解释了我当时容易忽略的一点:在 Linux 主机上运行 Linux 容器,Docker 可以直接调用 Linux 内核的能力;在 macOS 或 Windows 上运行 Linux 容器,通常仍需要先有一台 Linux VM 提供那个 kernel。Docker 镜像带来的主要是用户空间和应用依赖,不是一整个操作系统。

1. namespace 与 cgroup:让一组进程“像在自己的机器里”

Docker 启动容器时,核心工作不是把程序变成某种新二进制,而是给它安排 namespace、cgroup、根文件系统与安全限制。namespace 负责隔离“它看见什么”,cgroup 负责约束“它最多用多少”。

Docker 将容器运行时接到 Linux kernel 的 namespace、cgroup、Netfilter 和安全机制上

上图更适合被看成一张“能力来源图”:Docker 和不同运行时的具体实现会演进,但容器隔离最终仍要落到 Linux kernel 提供的 namespace、cgroup、capability、SELinux/AppArmor、Netfilter 等机制上。图中的 libcontainer、LXC 等名称带有较早时期的实现背景;今天的 Docker 运行链路常会经过 containerd、runc,不过这条从运行时到内核能力的主线没有变化。

机制直观问题例子
PID namespace我能看见哪些进程?容器内的 PID 1,在宿主机上其实是另一个 PID
Mount namespace我能看见哪些挂载?容器有自己的 //proc 与 volume 挂载点
Network namespace我有哪些网卡、路由和端口?两个容器都可监听各自的 8080
UTS / IPC / User namespace我叫什么、能与谁 IPC、我是谁?独立 hostname;容器内 UID 可映射为宿主机非特权 UID
cgroup我能用多少资源?--memory--cpus、PIDs 和 I/O 控制

所以一个容器并不是真的“拥有了一台机器”,而是一组被内核赋予独立视图的进程。cgroup 也不只是限制 CPU 和内存:它会记账、参与调度,并在内存超过上限时触发回收甚至 OOM。设置资源限制是在安排故障域和资源竞争,不等于保证应用永远有稳定性能。

这正是 Docker 的能力边界:它很适合把 Linux 进程及其依赖做成可重复运行的交付物;但对不同内核、需要完整硬件虚拟化,或需要极强安全隔离的场景,虚拟机或 Kata、gVisor 一类额外隔离层仍有价值。

2. bridge 与 veth:给每个容器接上一小段网线

刚开始看到容器 IP 时,我以为 Docker “虚拟出了一张网卡”。更贴切的理解是:它利用 Linux network namespace、veth pair 和 bridge,把容器网络接回宿主机。veth 是成对出现的虚拟网线,两端分别在容器和宿主机网络命名空间里;Linux bridge 则像一台二层交换机。

两个容器通过 veth pair 接入 Docker0 bridge

图中的 eth0 位于各自的容器 network namespace,另一端 veth1veth2 留在宿主机,并被接入 docker0。因此两个容器虽然都显示为一张 eth0,但实际上是两组不同的接口和协议栈;docker0 再把它们放到同一个二层广播域中。

container netns                 host netns
eth0 ─ veth pair ─ vethXXX ─ docker0 bridge ─ host eth0 ─ LAN / Internet

默认 bridge 网络通常会为容器分配私网地址和默认路由。出站流量经宿主机 NAT/masquerade 离开;-p 8080:80 这类端口发布则把到达宿主机 8080 的流量,用 NAT/防火墙规则导向容器 80。容器内监听 80 和宿主机监听 80 因而不是同一件事。

如果还想再往下追一层,可以看下面这张 Linux bridge 的转发示意图。网卡被加入 bridge 后,bridge 会像交换机一样学习/查找转发表:目的端在同一 bridge 上时,数据帧从另一个端口送出;需要交给本机协议栈处理时,再向上进入 IP/TCP 等处理路径。这也是为什么排查单机 Docker 网络时,除了看容器 IP,还要看 veth、bridge、路由和防火墙规则。

Linux bridge 在本机协议栈与各端口间转发报文的示意

还有一张图帮助我分清当时常混淆的 tap 与 veth。tap 是用户空间可以通过字符设备读写的虚拟网络接口,常见于虚拟机网络;veth 则没有一个“用户态读写端”,它的一端发出的数据直接成为另一端收到的数据。Docker 的 bridge 模式主要利用的是后者,因而能自然地把容器 network namespace 接到宿主机 bridge 上。

tap 设备与 veth pair 的数据路径对比

这套设计的好处是开发时足够直接:创建网络、把容器接进去、按名称互访、按需发布端口。用户自建 bridge 网络还会提供更好的隔离和容器间 DNS。它的边界也很明确:bridge 主要解决单机容器通信;跨多主机的地址分配、服务发现、网络策略和故障处理,需要 overlay 或 Kubernetes CNI 这样的更高层网络方案。

3. 镜像层、Copy-on-Write 与 volume:程序可复制,数据不要跟着漂

Docker 镜像不是一个完整磁盘镜像,而是一组只读的文件变更层。比如基础系统一层、安装依赖一层、复制应用代码一层;多个镜像可以共享相同的底层。创建容器时,Docker 在这些只读层之上加一个可写层;OverlayFS 将它们合并成容器里看到的一个根目录。

container rootfs(merged view)
├─ writable layer:容器运行时新写入或修改的文件
└─ read-only image layers:app → dependencies → base image

这就是 copy-on-write 的直觉:未修改的文件继续复用镜像层;第一次修改底层文件时,先复制到容器可写层,再在上层修改。它让镜像分发、构建缓存与启动复用变得高效,也是 Docker “构建一次,到处运行”体验的一部分。

但我也需要记住它的反面:容器可写层会随着容器删除而消失,而且写入已有镜像文件可能触发 copy-up。数据库、上传文件、需要长期保存或大量写入的数据,不应寄希望于这个可写层。

这就是 volume 的位置。volume 由 Docker 管理,独立于容器生命周期;bind mount 则把宿主机的指定目录直接挂进去。两者都把数据路径从镜像层和容器可写层中分离出来:

需求更合适的选择
数据库数据、需要持久化的状态volume
开发时挂入本地源码或配置bind mount
容器运行的临时缓存容器可写层或 tmpfs

对我来说,这也是 Docker 产品设计最重要的取舍之一:镜像负责不可变交付,容器负责运行中的进程,volume 负责可变数据。把三者混在一起,容器会很快变成难以复制、难以升级的“小服务器”。

Docker 给我的不是“更小的虚拟机”

回头看,Docker 的产品价值不只是把 namespace、cgroup、bridge 和 OverlayFS 拼起来,而是给开发与运维约定了一套共同语言:镜像描述运行环境,容器描述一次运行,network 描述连接关系,volume 描述数据边界。于是应用能更容易在笔记本、测试环境和服务器间移动。

不过这套抽象没有抹去底层差异。排查容器问题时,最终仍会回到 Linux 进程、路由/NAT、文件系统和权限;知道这三条线,才不会把 Docker 当成一个无所不能的黑盒。

References

Notion 相关零散笔记

  • Docker
  • 虚拟化
  • Filesystem
  • Storage
  • Network
  • NAT

相关文档