「容器不是真的」:他自写了一个容器运行时,拆开了 Docker 的五件风衣
一位工程师为好玩自己写容器运行时,顺手把 Docker 拆了:容器只是带限制的进程,镜像只是文件夹,runc 的关键部分是 C 写的,连 Docker 创始人都现身评论区答疑。本期访谈聊风衣里的五个内核特性、Docker 真正的护城河,以及为什么「重写一遍」是最好的学习方式。

codeandcake.dev 的作者最近做了个好玩的决定:自己写一个容器运行时(container runtime)。刚开工没多久,他就忍不住写了篇短文,标题就一句话——Docker is weird,Docker 很奇怪,列的全是动手过程中学到的怪事。这篇短文上了 HN 首页,400 多条评论里最妙的一条来自 Docker 创始人 Solomon Hykes 本人:"Hi, I'm the creator of Docker",说看到别人琢磨自己当年设计的系统很有意思,还主动提出可以答疑。我请来老朋友陆远——容器基础设施工程师,天天和 runc、cgroup 打交道——聊聊这件"风衣"里到底装了什么。
本文由每日自动管线生成:选题与素材来自 Hacker News 公开讨论,访谈内容由 AI 基于原文整理,嘉宾为角色设定,观点不代表任何真实人物。
小听: 先问个扎心的。作者开篇第一句话就是"Containers aren't real"——容器不是真的。你天天和容器打交道,听到这句话第一反应是点头,还是翻白眼?
陆远: 点头,而且是苦笑那种。容器和虚拟机最大的区别就在这儿:虚拟机是真的有一台"电脑"在跑,容器从头到尾就是宿主机上的一个普通进程,内核照样在调度它。你甚至能在宿主机的 /proc 里找到这个进程,扒开看它的真面目。所以"容器"这个词本身就是个障眼法——它描述的不是一个东西,而是一组限制条件。
小听: 作者的比喻更狠:Linux 里根本不存在"容器"这个单一特性,容器是"五个 Linux 内核特性穿着一件风衣"(5 different linux kernel features in trenchcoat)。评论区有人说这是全文最妙的一句。
陆远: 这个比喻准得让人想鼓掌。风衣里装的是:namespaces 负责"藏"——让容器看不见宿主机和其他容器的东西;cgroups 负责"限"——CPU、内存用超了就掐;再加上一整套作者自己都承认没搞懂的网络魔法。每一件拆开看都不神秘,合在一起穿出门,你就得到了一个"看起来像独立机器"的东西。Docker 的样子,就是这么穿出来的。

风衣里装的每一件都不神秘,神秘的是穿上之后的样子。
小听: 第二个怪事更颠覆直觉:镜像(image)只是文件夹。你 docker run -it ubuntu /bin/bash 的时候,Docker 干的事就是把镜像从 Docker Hub 下载下来,放到某个路径,然后 chroot 过去,再执行那个路径下的 /bin/bash。
陆远: 对,镜像没有魔法,就是个打包好的根文件系统目录,外加几层元数据。Dockerfile 的分层构建、缓存复用,本质上都是在管理"文件夹的增量"。很多人第一次听说会幻灭——我 pull 下来的 ubuntu,居然只是个文件夹?但反过来想,这恰恰是容器轻量的原因:没有虚拟硬件,没有 guest 内核要启动,就是换了个根目录跑进程。
小听: 说到这儿,评论区几条高赞都在纠正一个误解:Docker 真正伟大的从来不是容器技术本身。有人说他 20 年前就用过 chroot,照样好用;Docker 的三板斧是 Dockerfile(单文件描述、分层缓存、CI 友好)、镜像仓库(把全世界的软件装进一个分发网络),以及时机和势能——一堆以前部署起来要人命的软件,突然都有了现成的 docker 镜像。
陆远: 这话说到根上了。容器技术是 LXC、Solaris Zones 传下来的老手艺,Docker 的发明是"把老手艺包装成开发者体验"。Dockerfile 让构建可重复、可缓存,registry 让分发变成一条命令。技术史里这种例子很多:赢的往往不是发明者,而是把复杂东西变简单、还赶上生态爆发的那个人。
小听: 第三个怪事技术含量最高:Docker 的容器运行时 runc 是 Go 写的,而 Go 对容器运行时来说"有点糟糕"。Go 的 runtime 天生多线程,但容器启动需要的很多 Linux 系统调用必须在单线程环境里做,所以 runc 不得不在 Go runtime 初始化之前,先跑一个"疯狂的 C 模块"。
陆远: 更妙的是评论区现身说法:一位 2014 到 2019 年在 Docker 工作过的工程师留言,说"最重要的部分其实是 C 写的"——他本来写了一大段想纠正作者"runc 是 Go 写的"这个说法,写到一半发现作者下一段自己就把 C 模块讲清楚了,还自嘲"下次一定先读完再评论"。这种剧情,HN 评论区每年都要上演几次。
小听: 还有 Go 没有 fork 这个梗。fork 只 fork 主线程,在 Go 这种多线程 runtime 里就是个脚枪。可容器启动的标准流程偏偏是 fork → 设好一堆限制 → exec 目标程序。runc 的绕法是:重新 exec /proc/self/exe,带一个特殊参数标记自己是子进程,用这种方式"近似"出 fork 的效果。
陆远: 对,自己 exec 自己。第一次看到的人都会愣一下:还能这样?这就是系统编程的日常——优雅的设计遇到现实的语言限制,最后都长成这种"居然能跑"的样子。runc 的代码里藏着好几处这种"居然能跑",每一处背后都是一次妥协。
小听: 第四个怪事:Docker 需要 root。作者说他在 macOS 上用 Docker 时完全没意识到这点——很多容器运行时依赖的内核 API 需要 root 权限,所以 Docker daemon 通常跑在 root 下。"只能想象容器安全是份多难的工作。"
陆远: 这条评论区补充得很实在:rootless 容器不是不行,但代价是"在用户态把整个网络栈重实现一遍"。有人推荐了 rootless Podman 的路线——无 root、无 daemon、无 socket,fork-exec 启动容器,思路更干净。这其实是个 trade-off 光谱:Docker 选了"daemon + root 换功能完整",Podman 选了"干净换实现成本",没有银弹,只有取舍。

优雅的设计遇到现实,最后都长成"居然能跑"的样子。
小听: 作者的收尾很真诚:才刚开工,就已经觉得学到了超多,"亲手重写一个你天天用的工具,是理解它的最好方式"。评论区有人贴了 bocker——"用大约 100 行 bash 实现的 Docker",说想搞懂容器,看这个比看文档管用。
陆远: 这就是全文最值得带走的一句。评论区还有一条"泼冷水"的:有人说文章里每句话都让他想问"这哪里奇怪了",在熟悉演化史的人眼里一切都很自然。但我觉得这恰恰是"奇怪"的价值——奇怪是外行人的视角,也是学习的起点。你天天 docker run 的东西,拆开看居然是 chroot 加风衣加自己 exec 自己,这种"祛魅"时刻,每个工程师都该定期来一次。
小听: 总结一下?
陆远: 三句话。第一,容器是假的,但"假"得很有用——五个内核特性穿风衣,演出了一台机器。第二,Docker 赢的不是技术,是体验和生态:Dockerfile、registry、时机。第三,想真懂一个工具,就重写它一遍;100 行 bash 的 Docker,比 100 页文档更诚实。