Docker 挺怪的
作者为了好玩自己写了个容器运行时,写下六件让他觉得 Docker 很怪的事:容器既不是虚拟机也不是内核功能,镜像是文件夹,runc 因为 Go 没有 fork 只能 exec /proc/self/exe 假装 fork,跑容器还要 root。
中文
复制
我出于好玩决定自己写一个容器运行时。虽然还处在早期阶段,但我已经体会到 Docker 有多怪。下面是我了解到的几件 Docker 的怪事。
容器不是真实存在的
和虚拟机不同,容器只是一些被施加了特殊限制的进程。运行这些进程的仍然是你的内核!只要知道该去哪里找,你甚至能在 /proc 里找到这些进程。
容器真的不是真实存在的
Linux 里其实也没有哪个功能就叫“容器”。容器其实是五个不同的 Linux 内核功能合穿一件风衣,假扮成的一个东西。
namespace 决定宿主机和容器各自能看到什么,cgroup 限制容器的资源用量,还有一大堆我搞不懂的网络魔法,用来隔离容器的网络。
镜像就是文件夹
镜像就是一些文件夹,Docker 把它们放在某个地方,然后调用 chroot(或 pivot_root)把这个文件夹变成容器进程的新根目录。所以如果你运行 docker run -it --rm ubuntu /bin/bash,Docker 会:
- 从 Dockerhub 下载 ubuntu 镜像。
- 把这个镜像放到某个地方。
- 运行
/some/weird/docker/path/bin/bash
Go 对容器运行时来说算是挺糟糕的语言
Docker 的容器运行时 runc 是用 Go 写的,而 Go 的一些设计选择让它不太适合做容器运行时。Go 的运行时天生就是多线程的。这是个很酷的特性,但在 runc 里会带来一些麻烦。
容器运行时需要发起的许多 Linux 系统调用必须在单线程环境中执行,所以 runc 里有一个疯狂的 C 模块,它在 Go 运行时初始化之前运行,好让这些系统调用能发出去。
Go 里也没有 fork!fork 只复制主线程,所以在 Go 运行时这样的环境里,它就是个容易打到自己脚的坑。但容器运行时非常需要 fork、设置一堆限制,然后 exec 你真正放进 docker run 里的进程。为了绕开这一点,runc 会 exec /proc/self/exe,并带上一个特殊的命令行参数,把它标识为子进程,以此近似实现 fork。
Docker 需要 root 权限!
如果你和我一样在 macOS 上跑 Docker,这个问题很容易被忽略。容器运行时用到的许多 Linux 内核 API 要求(至少过去要求)以 root 身份运行,所以 Docker daemon 通常以 root 运行。容器安全有多难做,我只能靠想象。
现在情况似乎好多了。最大的例外是网络。如果你用的是 rootless 容器运行时,看起来它得在用户态把整个网络栈重新实现一遍,才能真正提供相同的功能。
学习很有趣!
我自己的容器运行时才刚起步,就已经觉得对 Docker 了解了很多。给自己在用的工具写一个自己的版本,是了解它的好办法。