Docker is weird
TL;DR · AI 摘要
Docker的实现机制与常见认知存在显著差异,容器本质是受限进程,镜像仅是文件夹,Go语言在容器运行时存在设计缺陷。
核心要点
- 容器本质是受限进程,通过Linux命名空间和cgroups实现隔离
- Docker镜像实际是文件夹结构,通过chroot实现根目录切换
- runc容器运行时因Go语言多线程特性需特殊处理系统调用
结构提纲
按章节快速跳转。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- Docker实现机制解析
- 容器本质
- 受限进程
- Linux命名空间
- cgroups
- 镜像结构
- 文件夹结构
- chroot机制
- 技术挑战
- Go语言限制
- root权限需求
- 网络栈重构
金句 / Highlights
值得收藏与分享的关键句。
容器不是虚拟机,而是受限进程,可通过/proc查看实际进程
镜像本质是文件夹,docker run命令实际执行的是镜像中的bin/bash
Go语言的多线程特性导致runc需要C模块处理单线程系统调用
rootless容器需在用户空间重构网络栈,实现复杂度显著增加
Code and Cake - Docker很奇怪
Code and Cake
关注
关于我
Docker很奇怪
2026年10月22日
出于兴趣,我决定自己动手实现一个容器运行时。尽管目前还处于早期阶段,但我已经发现Docker有很多奇怪的地方。以下是我学到的一些关于Docker的奇怪之处。
容器并不是真实的
与虚拟机不同,容器只是受到特殊限制的进程。你的内核仍然在运行这个进程!如果你知道在哪里查找,甚至可以在/proc中找到这个进程。
容器真的不是真实的
实际上Linux内核也没有专门用于"容器"的功能。相反,容器实际上是用"trenchcoat"包装的五个不同的Linux内核特性。
命名空间(Namespaces)隐藏了主机和容器可见的内容,cgroups限制容器的资源使用,还有一些我完全不理解的网络魔法,可以让容器网络实现隔离。
镜像只是文件夹
镜像只是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,设置很多限制,然后执行你实际在docker run中指定的进程。为了解决这个问题,runc通过向/proc/self/exe传递一个特殊命令行参数来模拟fork,这个参数标识它是一个子进程。
Docker需要root权限!
如果你像我一样在MacOS上运行Docker,这一点很容易被忽略。容器运行时使用的许多Linux内核API都需要(或至少以前需要)以root身份运行,因此Docker守护进程通常以root身份运行。我只能想象容器安全一定是个非常困难的工作。
如今情况似乎有所改善。最大的例外似乎是网络。如果你运行无root容器运行时,看起来必须在用户空间中重新实现整个网络栈,才能给你提供相同的功能!
学习是有趣的!
我刚刚开始自己的容器运行时开发,但已经感觉对Docker了解了很多。自己实现一个工具的版本是学习它的绝佳方式!