将晦涩难懂的技术讲的通俗易懂
分类: LINUX
2019-07-07 20:14:16
参加了今年的KubeCon + CloudNativeCon | Open Source Summit 2019 中国峰会。最大的感受就是“云原生”的概念真的很火,火的一塌糊涂。但究竟什么是云原生,其实也很难有人真的说的清楚,大体思想就是为云而生的应用,也就是应用的开发是基于云的,充分利用云的特点,实现DevOps,微服务,serverless等。但自我感觉这些都是些概念,并没有什么新技术,本质上可以说新瓶装旧酒,当然其中的一些思想还是值得借鉴的。
而云原生的中流砥柱就要算Kubernetes了,而Kubernetes的底层核心又是容器,除了原生支持的docker以外,近些年根据其他一些不同需求也产生了多类变种,如安全容器,富容器等。今天我们就来借用这个机会总结也行当前比较流行的容器技术。当然docker由于太基本而且已经被大家所熟知了,这里就不再展开了。
l 安全容器
为什么需要安全容器?因为你可能需要在容器中运行别人开发的不授信的代码,也可能使用一些存在安全问题的第三方库,甚至当你运行自己开发的代码时,如内核模块,你也不能保证它没有bug,万一出现问题可能导致整个host的crash。而这些问题是当前docket(runC)无法解决的,因此就需要一种隔离性更好的安全容器。
2018年谷歌发布了一种新型沙箱,可以用于为资源占用较少、不需要运行完整 VM 的容器提供安全隔离。gVisor 的核心是一个使用 Go 编写的开源用户空间内核,与现有的容器技术相比,其设计做了不同的权衡,它实现了 Linux 系统表面的主要部分。该项目包含集成了 Docker 和 Kubernetes 的运行时“runsc”。
据 gVisor 项目的 GitHub 介绍,gVisor 是一个作为普通非特权进程运行的内核,支持大多数的 Linux 系统调用。就像在 VM 中一样,在 gVisor 沙箱中运行的应用程序有自己的内核和虚拟设备,与主机和其它沙箱区分开来。通过拦截应用程序并作为客户内核运行,gVisor 提供了强隔离边界,可以将其视为极致,“与完整的 VM 相比,资源占用更灵活,固定成本更低”。不过,这种灵活性牺牲了性能和兼容性:对于频繁进行系统调用的工作负载,gVisor 的性能可能会差一些;虽然 gVisor 实现了 Linux 系统 API 的一大部分(目前 200 个系统调用),但有几个系统调用和参数还不支持(/proc 和 /sys 文件系统的某些部分),也就是说,。
与其他容器技术相比,一个关键的区别是,gVisor 不是简单地把应用程序系统调用重定向给主机内核,而是实现了大多数内核原语(信号量、文件系统、Futex、管道、mm 等),并基于这些原语实现了系统调用处理程序。
为了提供,限制主机系统表面,gVisor 运行时被分成了两个独立的进程。第一个是 Sentry 进程,它包含内核,负责执行用户代码,处理系统调用。Sentry 并不是使用 Go 语言重新实现了一个完整的 Linux 内核,而只是一个对应用进程“冒充”内核的系统组件。在这种设计思想下,我们就不难理解,Sentry 其实需要自己实现一个完整的 Linux 内核网络栈,以便处理应用进程的通信请求。然后,把封装好的二层帧直接发送给 Kubernetes 设置的 Pod 的 Network Namespace 即可。第二个是,它是一个文件系统操作代理,超出沙箱(非内部 proc 或 tmp 文件、管道等)的文件系统操作会通过发送给它。并依靠 seccomp 机制将自己的能力限制在最小集,从而防止恶意应用进程通过 Gofer 来从容器中“逃逸”出去。
而在具体的实现上,gVisor 的 Sentry 进程,其实还分为两种不同的实现方式。这里的工作原理,可以用下面的示意图来描述清楚。
第一种实现方式,是使用 Ptrace 机制来拦截用户应用的系统调用(System Call),然后把这些系统调用交给 Sentry 来进行处理。这个过程,对于应用进程来说,是完全透明的。而 Sentry 接下来,则会扮演操作系统的角色,在用户态执行用户程序,然后仅在需要的时候,才向宿主机发起 Sentry 自己所需要执行的系统调用。这就是 gVisor 对用户应用进程进行强隔离的主要手段。不过, Ptrace 进行系统调用拦截的性能实在是太差,仅能供 Demo 时使用。
而第二种实现方式,则更加具有普适性。它的工作原理如下图所示。
在这种实现里,Sentry 会使用 KVM 来进行系统调用的拦截,这个性能比 Ptrace 就要好很多了。
当然,为了能够做到这一点,Sentry 进程就必须扮演一个 Guest Kernel 的角色,负责执行用户程序,发起系统调用。而这些系统调用被 KVM 拦截下来,还是继续交给 Sentry 进行处理。只不过在这时候,Sentry 就切换成了一个普通的宿主机进程的角色,来向宿主机发起它所需要的系统调用。
可以看到,在这种实现里,Sentry 并不会真的像虚拟机那样去虚拟出硬件设备、安装 Guest 操作系统。它只是借助 KVM 进行系统调用的拦截,以及处理地址空间切换等细节。
值得一提的是,在 Google 内部,他们也是使用的第二种基于 Hypervisor 的 gVisor 实现。只不过 Google 内部有自己研发的 Hypervisor,所以要比 KVM 实现的性能还要好。
需要指出的是,到目前为止,gVisor 的实现依然不是非常完善,有很多 Linux 系统调用它还不支持;有很多应用,在 gVisor 里还没办法运行起来。 此外,gVisor 也暂时没有实现一个 Pod 多个容器的支持。当然,在后面的发展中,这些工程问题一定会逐渐解决掉的。
Kata的工作原理可以用如下所示的示意图来描述。
Kata Containers 的本质,就是一个轻量化虚拟机。所以当你启动一个 Kata Containers 之后,你其实就会看到一个正常的虚拟机在运行。这也就意味着,一个标准的虚拟机管理程序(Virtual Machine Manager, VMM)是运行 Kata Containers 必备的一个组件。在我们上面图中,使用的 VMM 就是 Qemu。
而使用了虚拟机作为进程的隔离环境之后,Kata Containers 原生就带有了 Pod 的概念。即:这个 Kata Containers 启动的虚拟机,就是一个 Pod;而用户定义的容器,就是运行在这个轻量级虚拟机里的进程。在具体实现上,Kata Containers 的虚拟机里会有一个特殊的 Init 进程负责管理虚拟机里面的用户容器,并且只为这些容器开启 Mount Namespace。所以,这些用户容器之间,原生就是共享 Network 以及其他 Namespace 的。
此外,为了跟上层编排框架比如 Kubernetes 进行对接,Kata Containers 项目会启动一系列跟用户容器对应的 shim 进程,来负责操作这些用户容器的生命周期。当然,这些操作,实际上还是要靠虚拟机里的 Init 进程来帮你做到。但到了Kata1.5有了shim v2的支持,Kata不再需要为每个Container启动一个shim了,而是为一个pod启动一个shim v2。如下图所示:
而在具体的架构上,Kata Containers 的实现方式同一个正常的虚拟机其实也非常类似。这里的原理,可以用如下所示的一幅示意图来表示。
可以看到,当 Kata Containers 运行起来之后,虚拟机里的用户进程(容器),实际上只能看到虚拟机里的被裁减过的 Guest Kernel,以及通过 Hypervisor 虚拟出来的硬件设备。
而为了能够对这个虚拟机的 I/O 性能进行优化,Kata Containers 也会通过 vhost 技术(比如:vhost-user)来实现 Guest 与 Host 之间的高效的网络通信,并且使用 PCI Passthrough (PCI 穿透)技术来让 Guest 里的进程直接访问到宿主机上的物理设备。这些架构设计与实现,其实跟常规虚拟机的优化手段是基本一致的。
通过以上的讲述,相信你对 Kata Containers 和 gVisor 的实现原理,已经有一个感性的认识了。另外,你可能还听说过 AWS 在 2018 年末发布的一个叫做 Firecracker 的安全容器项目。这个项目的核心,其实是一个用 Rust 语言重新编写的 VMM(即:虚拟机管理器)。这就意味着, Firecracker 和 Kata Containers 的本质原理,其实是一样的。只不过, Kata Containers 默认使用的 VMM 是 Qemu,而 Firecracker,则使用自己编写的 VMM。所以,理论上,Kata Containers 也可以使用 Firecracker 运行起来。
在性能上,KataContainers 和 KVM 实现的 gVisor 基本不分伯仲,在启动速度和占用资源上,基于用户态内核的 gVisor 还略胜一筹。但是,对于系统调用密集的应用,比如重 I/O 或者重网络的应用,gVisor 就会因为需要频繁拦截系统调用而出现性能急剧下降的情况。此外,gVisor 由于要自己使用 Sentry 去模拟一个 Linux 内核,所以它能支持的系统调用是有限的,只是 Linux 系统调用的一个子集。