
先破除一个直觉
开发者 smismith 在博客里写了一件很多人嘴上知道、心里没真的接受的事:容器不是虚拟机,容器连「一个新的东西」都不是。它就是宿主机上的一个普通进程,只不过被一堆内核机制捆住了手脚。宿主机内核还在跑它,你甚至能在 /proc 里把它翻出来。
Linux 里也没有一个叫「容器」的功能。所谓容器,是五样内核特性套在一件风衣里:命名空间(namespace,管「看得见什么」)、控制组(cgroup,管「能用多少资源」)、网络隔离,再加上文件系统。作者的原话很实在:网络那块魔法他自己也没完全搞懂。
镜像就是一个文件夹
这一条最容易让人恍然。镜像不是某种特殊格式的包,它就是一堆文件夹,Docker 把它丢在某个路径下,然后调 chroot(或者 pivot_root),把这个文件夹变成容器进程眼中的根目录。你敲 docker run -it --rm ubuntu /bin/bash,实际发生的是:下载 ubuntu 镜像、找个地方放、然后执行 /某个很怪的/docker/路径/bin/bash。
懂了这一层,很多日常困惑就通了:为什么容器里的路径看着像另一台机器,为什么挂载目录能直接穿透进去,为什么删了容器镜像还占着盘。
用 Go 写容器运行时,为什么别扭
Docker 底层的运行时叫 runc,是 Go 写的。Go 有两个设计,放到容器运行时这个场景里就成了麻烦:
一是它的运行时天生多线程。可容器运行时需要的不少系统调用,必须在单线程环境里发起。于是 runc 塞了一个用 C 写的模块,抢在 Go 运行时初始化之前先跑,把这些调用办完。
二是 Go 没有真正的 fork。fork 只会复制主线程,在 Go 里用起来容易踩脚。而容器运行时恰恰最想要「fork 一份、加上限制、再 exec 成用户进程」这套动作。绕法是:带一个特殊命令行参数去 exec /proc/self/exe,自己再启动一次自己,用这个办法模拟出 fork。
作者没有贬低 Go 的意思,他说的是「别扭」——语言特性和任务需求没对齐,就得靠工程手段硬掰。
root 这件事,比想象中难清
在 Mac 上用 Docker 的人基本感觉不到:Docker 守护进程在 Linux 上通常是 root 跑的,因为很多内核接口要么要求 root、要么「曾经要求」root。于是他感慨,做容器安全是件很难的活。
现在有无根模式(rootless,不用 root 跑容器),但代价不小:网络这块基本得在用户态重新实现一遍协议栈,才能换来和原来一样的功能。这就是「享受了好处、也得接下成本」。
他为什么建议你也写一个
文章结尾就一句话:亲手实现一遍你天天在用的工具,是学它最快的方式。作者自己的运行时才刚起步,但已经感觉学到了太多。
对做企业数字化/小团队的实际启发
第一,给客户解释「容器化」时,别再说「轻量级虚拟机」。准确的说法是「进程加隔离」。这句话一旦说对,客户对隔离强度、对是否能跑互不信任的程序,就会有更合理的预期。
第二,排查「容器里跑不起来」这类问题时,脑子里要有三张图:根目录被换了、进程视图被藏了、资源被限了。问题九成出在这三处,而不是代码。
第三,安全边界别全押在容器上。容器共享宿主机内核,这是它的效率来源,也是它的风险来源。真要隔离互不信任的代码,虚拟机或专门的沙箱运行时才够。
第四,团队里每周留半天「自己实现一遍常用工具」。不用交付,只为了把黑盒变成白盒。这类投入不会体现在报表上,但会体现在故障处理的速度上。


评论交流
欢迎留下你的想法