尽管容器技术在今天越来越被人接受,但是安全性依然是一个绕不开的问题,由于容器采用的是共享内核外加 cgroups 和 namespaces 等黑魔法的方式进行隔离注定了会有很多路径的 bug 导致隔离性问题,安全隐患依然存在。而不使用虚拟机的原因不外乎虚拟机启动太慢,额外开销太高,性能由于多了一层会下降。面对容器和虚拟机这两个极端,容器一方想把容器做的隔离性更好,虚拟化方面想把虚拟机做的更轻,结果 neclab 的一群人居然做到把虚拟机的启动速度做的比 Docker 还快,内存开销比 Docker 还小,这种反常识的事情居然发生了!他们把工作以 paper 的形式发表在了 SOSP'17 上,这篇文章会介绍下他们是动用了什么样的核武器达到了这样的效果。
想要加速就要先有个预期,看一下我们的现状是什么,极限又能到哪里。作者对比了进程启动时间,容器启动时间以及一个虚拟机启动的时间。其中进程启动时间作为一个理论上的极限需要 3.5 ms,docker 容器启动时间需要 200ms,而一个 Debian 虚拟机的启动时间需要大约 2s,其中创建 vm 需要 0.5 s, 启动 vm 需要 1.5s。
而和启动速度相关最重要的一个因素就是镜像体积大小,启动的速度基本上随着镜像的体积线性增加。如下图所示:
vm 通常需要一个完整的操作系统,所以体积会比较大,如果想优化速度就必须要精简操作系统了,作者通过两种方法实现了这种精简。
第一种也是最极端的一种 Unikernels,这可以算是终极核武了。Unikernels 的定义比较复杂,简单来说就是专门为这个应用做一个操作系统内核,这个内核只提供能运行这个服务的最基本功能,除了能跑这个应用别的什么都干不了。应用和内核也是 link 在一起的,你甚至不好说是给这个应用定制了一个内核,还是定制了一个内核具有一定应用功能。这种方法需要为每个特定应用定制一个特定内核,听着就是个不可能完成的任务,不过作者还是做了好几个这样的 unikernels,甚至还做了个能跑 serverless 功能的 unikernels。
当然这种方法太极端了,第二种方法就相对温和一些,不过依然是洲际导弹重量级别的。既然定制 unikernels 太难了,那就拿一个 Linux 内核进行精简化只保留需要的模块和功能,为每个应用编译一个精简化的 Linux 内核,这样就不需要做应用改造了。为了达到这个目的作者还专门做了个 Tinyx 工具,可以根据应用 objdump 出来的信息来自动寻找依赖,并且根据启发性的测试不断地去寻找哪些 linux 模块可以关闭,去除掉那些和应用无关的功能和编译选项。除了缩小镜像体积,另一个带来的好处是会降低 vm 启动占用的内存。经过精简的内核可以只占用 1.8M 内存,要知道 docker 给每个容器开启的 shim 进程也需要占据 4M 左右的内存。
最终的结果是利用 Tinyx 精简内核后镜像大小是 9.5M 的启动时间是 180ms,而 Unikernels 方法镜像大小是 480K,启动时间只需要 3ms,这两种方案都已经比 200ms 的 docker 启动时间还要快了。并且更要命的是他们的内存额外开销比 docker 还要低,从下图可以看出当运行到 3K 容器时宿主机已经无法再创建 docker 容器了,而 Unikernels 可以平稳跑到 8K。
上面介绍的是优化启动速度,主要是精简内核的内容,paper 中还有将近一半的篇幅是介绍如何优化创建 vm 的过程,包括把消息队列的机制改为共享内存机制,虚拟机的预创建以及很多脚本工具 binary 化等很黑科技的优化,对 Xen 进行了大刀阔斧的改进,(毕竟 Xen 也不是用来做这种事情的)。
话说回来,尽管作者真的做到了虚拟机比容器更轻更快,但是这个过程中用到的技术基本上是属于『即使我告诉你原理,你也依旧做不出来』的范畴。这其中需要太多内核和虚拟化的专业技术,一般的应用开发,甚至一般的底层开发都接触不到。而容器和虚拟化两者的融合,我相信依然是未来的一个必然要走的路线,虚拟机会越来越轻量化,容器也越来越会像虚拟机。这篇 paper 的工作也告诉了我们虚拟化还有大量的性能优化空间,而且既然虚拟机都能比容器更轻更快,那么现在的容器在这方面也要努力了。让我们看一下最终是容器长成了虚拟机,还是虚拟机变成了容器呢?