47丨绝不仅仅是安全:KataContainers与gVisor

思考并回答以下问题:

在上一篇文章中,我为你详细地讲解了kubelet和CRI的设计和具体的工作原理。而在讲解CRI的诞生背景时,我也提到过,这其中的一个重要推动力,就是基于虚拟化或者独立内核的安全容器项目的逐渐成熟。

使用虚拟化技术来做一个像Docker一样的容器项目,并不是一个新鲜的主意。早在Docker项目发布之后,Google公司就开源了一个实验性的项目,叫作novm。这,可以算是试图使用常规的虚拟化技术来运行Docker镜像的第一次尝试。不过,novm在开源后不久,就被放弃了,这对于Google公司来说或许不算是什么新鲜事,但是novm的昙花一现,还是激发出了很多内核开发者的灵感。

所以在2015年,几乎在同一个星期,IntelOTC(OpenSourceTechnologyCenter)和国内的HyperHQ团队同时开源了两个基于虚拟化技术的容器实现,分别叫做IntelClearContainer和runV项目。

而在2017年,借着Kubernetes的东风,这两个相似的容器运行时项目在中立基金会的撮合下最终合并,就成了现在大家耳熟能详的KataContainers项目。由于KataContainers的本质就是一个精简后的轻量级虚拟机,所以它的特点,就是“像虚拟机一样安全,像容器一样敏捷”。

而在2018年,Google公司则发布了一个名叫gVisor的项目。gVisor项目给容器进程配置一个用Go语言实现的、运行在用户态的、极小的“独立内核”。这个内核对容器进程暴露Linux内核ABI,扮演着“GuestKernel”的角色,从而达到了将容器和宿主机隔离开的目的。

不难看到,无论是KataContainers,还是gVisor,它们实现安全容器的方法其实是殊途同归的。这两种容器实现的本质,都是给进程分配了一个独立的操作系统内核,从而避免了让容器共享宿主机的内核。这样,容器进程能够看到的攻击面,就从整个宿主机内核变成了一个极小的、独立的、以容器为单位的内核,从而有效解决了容器进程发生“逃逸”或者夺取整个宿主机的控制权的问题。这个原理,可以用如下所示的示意图来表示清楚。

而它们的区别在于,KataContainers使用的是传统的虚拟化技术,通过虚拟硬件模拟出了一台“小虚拟机”,然后在这个小虚拟机里安装了一个裁剪后的Linux内核来实现强隔离。

而gVisor的做法则更加激进,Google的工程师直接用Go语言“模拟”出了一个运行在用户态的操作系统内核,然后通过这个模拟的内核来代替容器进程向宿主机发起有限的、可控的系统调用。

接下来,我就来为你详细解读一下KataContainers和gVisor具体的设计原理。

首先,我们来看KataContainers。它的工作原理可以用如下所示的示意图来描述。

我们前面说过,KataContainers的本质,就是一个轻量化虚拟机。所以当你启动一个KataContainers之后,你其实就会看到一个正常的虚拟机在运行。这也就意味着,一个标准的虚拟机管理程序(VirtualMachineManager,VMM)是运行KataContainers必备的一个组件。在我们上面图中,使用的VMM就是Qemu。

而使用了虚拟机作为进程的隔离环境之后,KataContainers原生就带有了Pod的概念。即:这个KataContainers启动的虚拟机,就是一个Pod;而用户定义的容器,就是运行在这个轻量级虚拟机里的进程。在具体实现上,KataContainers的虚拟机里会有一个特殊的Init进程负责管理虚拟机里面的用户容器,并且只为这些容器开启MountNamespace。所以,这些用户容器之间,原生就是共享Network以及其他Namespace的。

此外,为了跟上层编排框架比如Kubernetes进行对接,KataContainers项目会启动一系列跟用户容器对应的shim进程,来负责操作这些用户容器的生命周期。当然,这些操作,实际上还是要靠虚拟机里的Init进程来帮你做到。

而在具体的架构上,KataContainers的实现方式同一个正常的虚拟机其实也非常类似。这里的原理,可以用如下所示的一幅示意图来表示。

可以看到,当KataContainers运行起来之后,虚拟机里的用户进程(容器),实际上只能看到虚拟机里的、被裁减过的GuestKernel,以及通过Hypervisor虚拟出来的硬件设备。

而为了能够对这个虚拟机的I/O性能进行优化,KataContainers也会通过vhost技术(比如:vhost-user)来实现Guest与Host之间的高效的网络通信,并且使用PCIPassthrough(PCI穿透)技术来让Guest里的进程直接访问到宿主机上的物理设备。这些架构设计与实现,其实跟常规虚拟机的优化手段是基本一致的。

相比之下,gVisor的设计其实要更加“激进”一些。它的原理,可以用如下所示的示意图来表示清楚。

gVisor工作的核心,在于它为应用进程、也就是用户容器,启动了一个名叫Sentry的进程。而Sentry进程的主要职责,就是提供一个传统的操作系统内核的能力,即:运行用户程序,执行系统调用。所以说,Sentry并不是使用Go语言重新实现了一个完整的Linux内核,而只是一个对应用进程“冒充”内核的系统组件。

在这种设计思想下,我们就不难理解,Sentry其实需要自己实现一个完整的Linux内核网络栈,以便处理应用进程的通信请求。然后,把封装好的二层帧直接发送给Kubernetes设置的Pod的NetworkNamespace即可。

此外,Sentry对于Volume的操作,则需要通过9p协议交给一个叫做Gofer的代理进程来完成。Gofer会代替应用进程直接操作宿主机上的文件,并依靠seccomp机制将自己的能力限制在最小集,从而防止恶意应用进程通过Gofer来从容器中“逃逸”出去。

而在具体的实现上,gVisor的Sentry进程,其实还分为两种不同的实现方式。这里的工作原理,可以用下面的示意图来描述清楚。

第一种实现方式,是使用Ptrace机制来拦截用户应用的系统调用(SystemCall),然后把这些系统调用交给Sentry来进行处理。

这个过程,对于应用进程来说,是完全透明的。而Sentry接下来,则会扮演操作系统的角色,在用户态执行用户程序,然后仅在需要的时候,才向宿主机发起Sentry自己所需要执行的系统调用。这,就是gVisor对用户应用进程进行强隔离的主要手段。不过,Ptrace进行系统调用拦截的性能实在是太差,仅能供Demo时使用。

而第二种实现方式,则更加具有普适性。它的工作原理如下图所示。

在这种实现里,Sentry会使用KVM来进行系统调用的拦截,这个性能比Ptrace就要好很多了。

当然,为了能够做到这一点,Sentry进程就必须扮演一个GuestKernel的角色,负责执行用户程序,发起系统调用。而这些系统调用被KVM拦截下来,还是继续交给Sentry进行处理。只不过在这时候,Sentry就切换成了一个普通的宿主机进程的角色,来向宿主机发起它所需要的系统调用。

可以看到,在这种实现里,Sentry并不会真的像虚拟机那样去虚拟出硬件设备、安装Guest操作系统。它只是借助KVM进行系统调用的拦截,以及处理地址空间切换等细节。

值得一提的是,在Google内部,他们也是使用的第二种基于Hypervisor的gVisor实现。只不过Google内部有自己研发的Hypervisor,所以要比KVM实现的性能还要好。

通过以上的讲述,相信你对KataContainers和gVisor的实现原理,已经有一个感性的认识了。需要指出的是,到目前为止,gVisor的实现依然不是非常完善,有很多Linux系统调用它还不支持;有很多应用,在gVisor里还没办法运行起来。此外,gVisor也暂时没有实现一个Pod多个容器的支持。当然,在后面的发展中,这些工程问题一定会逐渐解决掉的。

另外,你可能还听说过AWS在2018年末发布的一个叫做Firecracker的安全容器项目。这个项目的核心,其实是一个用Rust语言重新编写的VMM(即:虚拟机管理器)。这就意味着,Firecracker和KataContainers的本质原理,其实是一样的。只不过,KataContainers默认使用的VMM是Qemu,而Firecracker,则使用自己编写的VMM。所以,理论上,KataContainers也可以使用Firecracker运行起来。

总结

在本篇文章中,我为你详细地介绍了拥有独立内核的安全容器项目,对比了KataContainers和gVisor的设计与实现细节。

在性能上,KataContainers和KVM实现的gVisor基本不分伯仲,在启动速度和占用资源上,基于用户态内核的gVisor还略胜一筹。但是,对于系统调用密集的应用,比如重I/O或者重网络的应用,gVisor就会因为需要频繁拦截系统调用而出现性能急剧下降的情况。此外,gVisor由于要自己使用Sentry去模拟一个Linux内核,所以它能支持的系统调用是有限的,只是Linux系统调用的一个子集。

不过,gVisor虽然现在没有任何优势,但是这种通过在用户态运行一个操作系统内核,来为应用进程提供强隔离的思路,的确是未来安全容器进一步演化的一个非常有前途的方向。

值得一提的是,KataContainers团队在gVisor之前,就已经Demo了一个名叫Linuxd的项目。这个项目,使用了UserModeLinux(UML)技术,在用户态运行起了一个真正的LinuxKernel来为应用进程提供强隔离,从而避免了重新实现LinuxKernel带来的各种麻烦。

有兴趣的话,你可以在这里查看这个演讲。我相信,这个方向,应该才是安全容器进化的未来。这比Unikernels这种根本不适合实际场景中使用的思路,要靠谱得多。

本篇图片出处均引自KataContainers的官方对比资料。

思考题

安全容器的意义,绝不仅仅止于安全。你可以想象一下这样一个场景:比如,你的宿主机的Linux内核版本是3.6,但是应用却必须要求Linux内核版本是4.0。这时候,你就可以把这个应用运行在一个KataContainers里。那么请问,你觉得使用gVisor是否也能提供这种能力呢?原因是什么呢?

0%