为什么 Docker 在 MicroVM 中会出问题(第 1 部分):你不知道自己依赖的 Linux 假设
Source: Dev.to
初始失败
我们尝试在 microVM 中运行 Docker,结果在第一个容器启动之前就失败了:
cgroup mountpoint does not exist
在普通的 EC2 实例上,同样的 Docker 二进制文件可以正常工作。问题并不是 Docker 本身或内核 bug——而是更微妙的东西:我们依赖了在 microVM 中不存在的 Linux 部分。
Cgroup 假设
Docker 的错误信息提到了 cgroup,于是我们检查了文件系统:
ls /sys/fs/cgroup
没有输出。
mount | grep cgroup
同样,什么也没有。
在典型的 Linux 系统中,/sys/fs/cgroup 会因为启动时有程序挂载而自动存在。但在 microVM 中,这一步从未发生,导致 Docker 试图创建自己的 cgroup 层次结构时,内核返回“这里没有接口”。
我们手动挂载了层次结构:
mount -t cgroup2 none /sys/fs/cgroup
Docker 继续前进了一点,但随后又遇到了下一道墙。这让我们学会了问 “Docker 现在假设存在哪些东西?” 而不是单纯地问 “Docker 为什么会失败?”。
Systemd 与启动过程
完整的 Linux 发行版(带有 systemd)在你登录之前会完成许多任务:
- 挂载
/proc、/sys、/dev - 设置 cgroup
- 初始化网络
- 准备运行时环境
microVM 并不会自动完成这些——除非你显式加入 systemd,否则它们都不存在。因此,缺失或不完整的 /proc、/sys 挂载永远得不到后续修复,你实际上必须自己重新实现启动过程。
容器内部的网络栈
在修复了基本挂载后,Docker 开始初始化容器,但网络出现了问题。弄清容器如何访问互联网后,问题便迎刃而解。
容器网络命名空间
- 每个容器都有自己的 IP(例如
172.17.x.x)。 - 它 不 与宿主机的网络接口共享。
- 它运行在自己的网络命名空间中。
Docker 的网络设置
- Docker 为容器创建一个新的网络命名空间。
- 它创建一对 veth 设备:一端留在宿主机,另一端移入容器。
- veth 的宿主机端连接到
docker0桥,使容器之间能够互相通信。 - 对离开容器的报文进行 NAT,将源 IP 改写为宿主机的 IP,从而实现外部连通性。
MicroVM 中的额外层
在我们的场景里,容器运行在一个 microVM 内,而该 microVM 本身又有一个由宿主机上的 tap 设备提供的虚拟 NIC。出站流量的完整路径如下:
container → bridge (docker0) → VM eth0 → virtual NIC → host → internet
两层堆叠的网络环境意味着,只要任意一层缺失关键环节,报文就会无法到达目的地,而产生的错误往往难以直观判断。
iptables 与报文过滤
Docker 最终报错:
iptables: Failed to initialize nft: Protocol not supported
此错误根源于更底层的内核能力:
- 内核中报文过滤的实现方式。
iptables与该实现的交互方式。- 内核编译时使用的 netfilter(nftables)后端。
要解决它,需要了解这些底层细节。
思维模型的转变
最大的变化不是技术层面的,而是概念层面的。我们不再把 microVM 当作普通机器来对待,而是采用了全新的思路:
- 不假设任何东西已经存在。
- 每一层都必须自行验证。
- 每一次修复都会揭示下一个依赖。
我们不再是“调试 Docker”,而是 在发现“一个可工作的 Linux 环境到底由哪些组成”。
第 2 部分将探讨这种思维模型如何在解决 iptables 失败时发挥作用,尤其是在面对更复杂的情况时。