mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2026-05-11 04:04:34 +08:00
mod
This commit is contained in:
100
极客时间专栏/深入剖析Kubernetes/Kubernetes容器运行时/45 | 幕后英雄:SIG-Node与CRI.md
Normal file
100
极客时间专栏/深入剖析Kubernetes/Kubernetes容器运行时/45 | 幕后英雄:SIG-Node与CRI.md
Normal file
@@ -0,0 +1,100 @@
|
||||
<audio id="audio" title="45 | 幕后英雄:SIG-Node与CRI" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/dd/3a/dd28bbef572731c1cc9edaaae166d13a.mp3"></audio>
|
||||
|
||||
你好,我是张磊。今天我和你分享的主题是:幕后英雄之SIG-Node与CRI。
|
||||
|
||||
在前面的文章中,我为你详细讲解了关于 Kubernetes 调度和资源管理相关的内容。实际上,在调度这一步完成后,Kubernetes 就需要负责将这个调度成功的 Pod,在宿主机上创建出来,并把它所定义的各个容器启动起来。这些,都是 kubelet 这个核心组件的主要功能。
|
||||
|
||||
在接下来三篇文章中,我就深入到 kubelet 里面,为你详细剖析一下 Kubernetes 对容器运行时的管理能力。
|
||||
|
||||
在 Kubernetes 社区里,与 kubelet 以及容器运行时管理相关的内容,都属于 SIG-Node 的范畴。如果你经常参与社区的话,你可能会觉得,相比于其他每天都热闹非凡的 SIG小组,SIG-Node 是 Kubernetes 里相对沉寂也不太发声的一个小组,小组里的成员也很少在外面公开宣讲。
|
||||
|
||||
不过,正如我前面所介绍的,SIG-Node以及 kubelet,其实是 Kubernetes整套体系里非常核心的一个部分。 毕竟,它们才是 Kubernetes 这样一个容器编排与管理系统,跟容器打交道的主要“场所”。
|
||||
|
||||
而 kubelet 这个组件本身,也是 Kubernetes 里面第二个不可被替代的组件(第一个不可被替代的组件当然是 kube-apiserver)。也就是说,**无论如何,我都不太建议你对 kubelet 的代码进行大量的改动。保持 kubelet 跟上游基本一致的重要性,就跟保持 kube-apiserver 跟上游一致是一个道理。**
|
||||
|
||||
当然, kubelet 本身,也是按照“控制器”模式来工作的。它实际的工作原理,可以用如下所示的一幅示意图来表示清楚。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/91/03/914e097aed10b9ff39b509759f8b1d03.png" alt=""><br>
|
||||
可以看到,kubelet 的工作核心,就是一个控制循环,即:SyncLoop(图中的大圆圈)。而驱动这个控制循环运行的事件,包括四种:
|
||||
|
||||
<li>
|
||||
Pod 更新事件;
|
||||
</li>
|
||||
<li>
|
||||
Pod 生命周期变化;
|
||||
</li>
|
||||
<li>
|
||||
kubelet 本身设置的执行周期;
|
||||
</li>
|
||||
<li>
|
||||
定时的清理事件。
|
||||
</li>
|
||||
|
||||
所以,跟其他控制器类似,kubelet 启动的时候,要做的第一件事情,就是设置 Listers,也就是注册它所关心的各种事件的 Informer。这些 Informer,就是 SyncLoop 需要处理的数据的来源。
|
||||
|
||||
此外,kubelet 还负责维护着很多很多其他的子控制循环(也就是图中的小圆圈)。这些控制循环的名字,一般被称作某某 Manager,比如 Volume Manager、Image Manager、Node Status Manager等等。
|
||||
|
||||
不难想到,这些控制循环的责任,就是通过控制器模式,完成 kubelet 的某项具体职责。比如 Node Status Manager,就负责响应 Node 的状态变化,然后将 Node 的状态收集起来,并通过 Heartbeat 的方式上报给 APIServer。再比如 CPU Manager,就负责维护该 Node 的 CPU 核的信息,以便在 Pod 通过 cpuset 的方式请求 CPU 核的时候,能够正确地管理 CPU 核的使用量和可用量。
|
||||
|
||||
那么这个 **SyncLoop,又是如何根据 Pod 对象的变化,来进行容器操作的呢?**
|
||||
|
||||
实际上,kubelet 也是通过 Watch机制,监听了与自己相关的 Pod 对象的变化。当然,这个 Watch 的过滤条件是该 Pod 的 nodeName 字段与自己相同。kubelet 会把这些 Pod 的信息缓存在自己的内存里。
|
||||
|
||||
而当一个 Pod 完成调度、与一个 Node 绑定起来之后, 这个 Pod 的变化就会触发 kubelet 在控制循环里注册的 Handler,也就是上图中的 HandlePods 部分。此时,通过检查该 Pod 在 kubelet 内存里的状态,kubelet 就能够判断出这是一个新调度过来的 Pod,从而触发 Handler 里 ADD 事件对应的处理逻辑。
|
||||
|
||||
在具体的处理过程当中,kubelet 会启动一个名叫 Pod Update Worker的、单独的 Goroutine 来完成对 Pod 的处理工作。
|
||||
|
||||
比如,如果是 ADD 事件的话,kubelet 就会为这个新的 Pod 生成对应的 Pod Status,检查 Pod 所声明使用的 Volume 是不是已经准备好。然后,调用下层的容器运行时(比如 Docker),开始创建这个 Pod 所定义的容器。
|
||||
|
||||
而如果是 UPDATE 事件的话,kubelet 就会根据 Pod 对象具体的变更情况,调用下层容器运行时进行容器的重建工作。
|
||||
|
||||
在这里需要注意的是,**kubelet 调用下层容器运行时的执行过程,并不会直接调用 Docker 的 API,而是通过一组叫作 CRI(Container Runtime Interface,容器运行时接口)的 gRPC 接口来间接执行的。**
|
||||
|
||||
Kubernetes 项目之所以要在 kubelet 中引入这样一层单独的抽象,当然是为了对 Kubernetes 屏蔽下层容器运行时的差异。实际上,对于 1.6版本之前的 Kubernetes 来说,它就是直接调用Docker 的 API 来创建和管理容器的。
|
||||
|
||||
但是,正如我在本专栏开始介绍容器背景的时候提到过的,Docker 项目风靡全球后不久,CoreOS 公司就推出了 rkt 项目来与 Docker 正面竞争。在这种背景下,Kubernetes 项目的默认容器运行时,自然也就成了两家公司角逐的重要战场。
|
||||
|
||||
毋庸置疑,Docker 项目必然是 Kubernetes 项目最依赖的容器运行时。但凭借与 Google 公司非同一般的关系,CoreOS 公司还是在2016年成功地将对 rkt 容器的支持,直接添加进了 kubelet 的主干代码里。
|
||||
|
||||
不过,这个“赶鸭子上架”的举动,并没有为 rkt 项目带来更多的用户,反而给 kubelet 的维护人员,带来了巨大的负担。
|
||||
|
||||
不难想象,在这种情况下, **kubelet 任何一次重要功能的更新,都不得不考虑Docker 和 rkt 这两种容器运行时的处理场景,然后分别更新 Docker 和 rkt 两部分代码。**
|
||||
|
||||
更让人为难的是,由于 rkt 项目实在太小众,kubelet 团队所有与 rkt 相关的代码修改,都必须依赖于 CoreOS 的员工才能做到。这不仅拖慢了 kubelet 的开发周期,也给项目的稳定性带来了巨大的隐患。
|
||||
|
||||
与此同时,在2016年,Kata Containers 项目的前身runV项目也开始逐渐成熟,这种基于虚拟化技术的强隔离容器,与 Kubernetes 和 Linux 容器项目之间具有良好的互补关系。所以,**在 Kubernetes 上游,对虚拟化容器的支持很快就被提上了日程。**
|
||||
|
||||
不过,虽然虚拟化容器运行时有各种优点,但它与 Linux 容器截然不同的实现方式,使得它跟 Kubernetes 的集成工作,比 rkt 要复杂得多。如果此时,再把对runV支持的代码也一起添加到 kubelet 当中,那么接下来kubelet 的维护工作就可以说完全没办法正常进行了。
|
||||
|
||||
所以,在2016年,SIG-Node 决定开始动手解决上述问题。而解决办法也很容易想到,那就是把 kubelet 对容器的操作,统一地抽象成一个接口。这样,kubelet 就只需要跟这个接口打交道了。而作为具体的容器项目,比如 Docker、 rkt、runV,它们就只需要自己提供一个该接口的实现,然后对 kubelet 暴露出 gRPC 服务即可。
|
||||
|
||||
这一层统一的容器操作接口,就是 CRI了。我会在下一篇文章中,为你详细讲解 CRI 的设计与具体的实现原理。
|
||||
|
||||
而在有了 CRI 之后,Kubernetes 以及 kubelet 本身的架构,就可以用如下所示的一幅示意图来描述。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/51/fe/5161bd6201942f7a1ed6d70d7d55acfe.png" alt=""><br>
|
||||
可以看到,当 Kubernetes 通过编排能力创建了一个 Pod 之后,调度器会为这个 Pod 选择一个具体的节点来运行。这时候,kubelet 当然就会通过前面讲解过的 SyncLoop 来判断需要执行的具体操作,比如创建一个Pod。那么此时,kubelet 实际上就会调用一个叫作 GenericRuntime 的通用组件来发起创建 Pod 的 CRI 请求。
|
||||
|
||||
那么,**这个 CRI 请求,又该由谁来响应呢?**
|
||||
|
||||
如果你使用的容器项目是 Docker 的话,那么负责响应这个请求的就是一个叫作 dockershim 的组件。它会把 CRI 请求里的内容拿出来,然后组装成 Docker API 请求发给 Docker Daemon。
|
||||
|
||||
需要注意的是,在 Kubernetes 目前的实现里,dockershim 依然是 kubelet 代码的一部分。当然,在将来,dockershim 肯定会被从 kubelet 里移出来,甚至直接被废弃掉。
|
||||
|
||||
而更普遍的场景,就是你需要在每台宿主机上单独安装一个负责响应 CRI 的组件,这个组件,一般被称作CRI shim。顾名思义,CRI shim 的工作,就是扮演 kubelet 与容器项目之间的“垫片”(shim)。所以它的作用非常单一,那就是实现 CRI 规定的每个接口,然后把具体的 CRI 请求“翻译”成对后端容器项目的请求或者操作。
|
||||
|
||||
## 总结
|
||||
|
||||
在本篇文章中,我首先为你介绍了 SIG-Node 的职责,以及 kubelet 这个组件的工作原理。
|
||||
|
||||
接下来,我为你重点讲解了 kubelet 究竟是如何将 Kubernetes 对应用的定义,一步步转换成最终对 Docker 或者其他容器项目的API 请求的。
|
||||
|
||||
不难看到,在这个过程中,kubelet 的 SyncLoop 和 CRI 的设计,是其中最重要的两个关键点。也正是基于以上设计,SyncLoop 本身就要求这个控制循环是绝对不可以被阻塞的。所以,凡是在 kubelet 里有可能会耗费大量时间的操作,比如准备 Pod 的 Volume、拉取镜像等,SyncLoop 都会开启单独的 Goroutine 来进行操作。
|
||||
|
||||
## 思考题
|
||||
|
||||
请问,在你的项目中,你是如何部署 kubelet 这个组件的?为什么要这么做呢?
|
||||
|
||||
感谢你的收听,欢迎你给我留言,也欢迎分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
95
极客时间专栏/深入剖析Kubernetes/Kubernetes容器运行时/46 | 解读 CRI 与 容器运行时.md
Normal file
95
极客时间专栏/深入剖析Kubernetes/Kubernetes容器运行时/46 | 解读 CRI 与 容器运行时.md
Normal file
@@ -0,0 +1,95 @@
|
||||
<audio id="audio" title="46 | 解读 CRI 与 容器运行时" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/61/6b/6151026f3abcb3afbb766c2b38ca086b.mp3"></audio>
|
||||
|
||||
你好,我是张磊。今天我和你分享的主题是:解读 CRI 与 容器运行时。
|
||||
|
||||
在上一篇文章中,我为你详细讲解了 kubelet 的工作原理和 CRI 的来龙去脉。在今天这篇文章中,我们就来进一步地、更深入地了解一下 CRI 的设计与工作原理。
|
||||
|
||||
首先,我们先来简要回顾一下有了 CRI 之后,Kubernetes 的架构图,如下所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/70/38/7016633777ec41da74905bfb91ae7b38.png" alt=""><br>
|
||||
在上一篇文章中我也提到了,CRI 机制能够发挥作用的核心,就在于每一种容器项目现在都可以自己实现一个 CRI shim,自行对 CRI 请求进行处理。这样,Kubernetes 就有了一个统一的容器抽象层,使得下层容器运行时可以自由地对接进入 Kubernetes 当中。
|
||||
|
||||
所以说,这里的 CRI shim,就是容器项目的维护者们自由发挥的“场地”了。而除了 dockershim之外,其他容器运行时的 CRI shim,都是需要额外部署在宿主机上的。
|
||||
|
||||
举个例子。CNCF 里的 containerd 项目,就可以提供一个典型的 CRI shim 的能力,即:将Kubernetes 发出的 CRI 请求,转换成对 containerd 的调用,然后创建出 runC 容器。而 runC项目,才是负责执行我们前面讲解过的设置容器 Namespace、Cgroups和chroot 等基础操作的组件。所以,这几层的组合关系,可以用如下所示的示意图来描述。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/62/3d/62c591c4d832d44fed6f76f60be88e3d.png" alt=""><br>
|
||||
**而作为一个 CRI shim,containerd 对 CRI 的具体实现,又是怎样的呢?**
|
||||
|
||||
我们先来看一下 CRI 这个接口的定义。下面这幅示意图,就展示了 CRI 里主要的待实现接口。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f7/16/f7e86505c09239b80ad05aecfb032e16.png" alt=""><br>
|
||||
具体地说,**我们可以把 CRI 分为两组:**
|
||||
|
||||
<li>
|
||||
第一组,是 RuntimeService。它提供的接口,主要是跟容器相关的操作。比如,创建和启动容器、删除容器、执行 exec 命令等等。
|
||||
</li>
|
||||
<li>
|
||||
而第二组,则是 ImageService。它提供的接口,主要是容器镜像相关的操作,比如拉取镜像、删除镜像等等。
|
||||
</li>
|
||||
|
||||
关于容器镜像的操作比较简单,所以我们就暂且略过。接下来,我主要为你讲解一下RuntimeService部分。
|
||||
|
||||
**在这一部分,CRI 设计的一个重要原则,就是确保这个接口本身,只关注容器,不关注 Pod。**这样做的原因,也很容易理解。
|
||||
|
||||
**第一**,Pod 是 Kubernetes 的编排概念,而不是容器运行时的概念。所以,我们就不能假设所有下层容器项目,都能够暴露出可以直接映射为 Pod 的 API。
|
||||
|
||||
**第二**,如果 CRI 里引入了关于 Pod 的概念,那么接下来只要 Pod API 对象的字段发生变化,那么CRI 就很有可能需要变更。而在 Kubernetes 开发的前期,Pod 对象的变化还是比较频繁的,但对于CRI 这样的标准接口来说,这个变更频率就有点麻烦了。
|
||||
|
||||
所以,在 CRI 的设计里,并没有一个直接创建 Pod 或者启动 Pod 的接口。
|
||||
|
||||
不过,相信你也已经注意到了,CRI 里还是有一组叫作RunPodSandbox 的接口的。
|
||||
|
||||
这个 PodSandbox,对应的并不是 Kubernetes 里的 Pod API 对象,而只是抽取了 Pod 里的一部分与容器运行时相关的字段,比如HostName、DnsConfig、CgroupParent 等。所以说,PodSandbox 这个接口描述的,其实是 Kubernetes 将 Pod 这个概念映射到容器运行时层面所需要的字段,或者说是一个Pod 对象子集。
|
||||
|
||||
而作为具体的容器项目,你就需要自己决定如何使用这些字段来实现一个 Kubernetes 期望的 Pod模型。这里的原理,可以用如下所示的示意图来表示清楚。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d9/61/d9fb7404c5dc9e0b5c902f74df9d7a61.png" alt=""><br>
|
||||
比如,当我们执行 kubectl run 创建了一个名叫 foo 的、包括了 A、B 两个容器的 Pod 之后。这个Pod 的信息最后来到 kubelet,kubelet 就会按照图中所示的顺序来调用 CRI 接口。
|
||||
|
||||
在具体的 CRI shim 中,这些接口的实现是可以完全不同的。比如,如果是 Docker 项目,dockershim 就会创建出一个名叫 foo 的 Infra容器(pause 容器),用来“hold”住整个 Pod 的 Network Namespace。
|
||||
|
||||
而如果是基于虚拟化技术的容器,比如 Kata Containers 项目,它的 CRI 实现就会直接创建出一个轻量级虚拟机来充当 Pod。
|
||||
|
||||
此外,需要注意的是,在 RunPodSandbox 这个接口的实现中,你还需要调用networkPlugin.SetUpPod(…) 来为这个 Sandbox 设置网络。这个 SetUpPod(…) 方法,实际上就在执行 CNI 插件里的add(…) 方法,也就是我在前面为你讲解过的 CNI 插件为 Pod 创建网络,并且把 Infra 容器加入到网络中的操作。
|
||||
|
||||
>
|
||||
备注:这里,你可以再回顾下第34篇文章[《Kubernetes网络模型与CNI网络插件》](https://time.geekbang.org/column/article/67351)中的相关内容。
|
||||
|
||||
|
||||
接下来,kubelet 继续调用 CreateContainer 和 StartContainer 接口来创建和启动容器 A、B。对应到 dockershim里,就是直接启动A,B两个 Docker 容器。所以最后,宿主机上会出现三个 Docker 容器组成这一个 Pod。
|
||||
|
||||
而如果是 Kata Containers 的话,CreateContainer和StartContainer接口的实现,就只会在前面创建的轻量级虚拟机里创建两个 A、B 容器对应的 Mount Namespace。所以,最后在宿主机上,只会有一个叫作 foo 的轻量级虚拟机在运行。关于像 Kata Containers 或者 gVisor 这种所谓的安全容器项目,我会在下一篇文章中为你详细介绍。
|
||||
|
||||
除了上述对容器生命周期的实现之外,CRI shim 还有一个重要的工作,就是如何实现 exec、logs 等接口。这些接口跟前面的操作有一个很大的不同,就是这些gRPC 接口调用期间,kubelet 需要跟容器项目维护一个长连接来传输数据。这种 API,我们就称之为Streaming API。
|
||||
|
||||
CRI shim 里对 Streaming API 的实现,依赖于一套独立的 Streaming Server 机制。这一部分原理,可以用如下所示的示意图来为你描述。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a8/ef/a8e7ff6a6b0c9591a0a4f2b8e9e9bdef.png" alt=""><br>
|
||||
可以看到,当我们对一个容器执行 kubectl exec 命令的时候,这个请求首先交给 API Server,然后 API Server 就会调用 kubelet 的 Exec API。
|
||||
|
||||
这时,kubelet就会调用 CRI 的 Exec 接口,而负责响应这个接口的,自然就是具体的 CRI shim。
|
||||
|
||||
但在这一步,CRI shim 并不会直接去调用后端的容器项目(比如 Docker )来进行处理,而只会返回一个 URL 给 kubelet。这个 URL,就是该 CRI shim 对应的 Streaming Server 的地址和端口。
|
||||
|
||||
而 kubelet 在拿到这个 URL 之后,就会把它以 Redirect 的方式返回给 API Server。所以这时候,API Server 就会通过重定向来向 Streaming Server 发起真正的 /exec 请求,与它建立长连接。
|
||||
|
||||
当然,这个 Streaming Server 本身,是需要通过使用 SIG-Node 为你维护的 Streaming API 库来实现的。并且,Streaming Server 会在 CRI shim 启动时就一起启动。此外,Stream Server 这一部分具体怎么实现,完全可以由 CRI shim 的维护者自行决定。比如,对于Docker 项目来说,dockershim 就是直接调用 Docker 的 Exec API 来作为实现的。
|
||||
|
||||
以上,就是CRI 的设计以及具体的工作原理了。
|
||||
|
||||
# 总结
|
||||
|
||||
在本篇文章中,我为你详细解读了 CRI 的设计和具体工作原理,并为你梳理了实现CRI 接口的核心流程。
|
||||
|
||||
从这些讲解中不难看出,CRI 这个接口的设计,实际上还是比较宽松的。这就意味着,作为容器项目的维护者,我在实现 CRI 的具体接口时,往往拥有着很高的自由度,这个自由度不仅包括了容器的生命周期管理,也包括了如何将 Pod 映射成为我自己的实现,还包括了如何调用 CNI 插件来为 Pod 设置网络的过程。
|
||||
|
||||
所以说,当你对容器这一层有特殊的需求时,我一定优先建议你考虑实现一个自己的 CRI shim ,而不是修改 kubelet 甚至容器项目的代码。这样通过插件的方式定制 Kubernetes 的做法,也是整个 Kubernetes 社区最鼓励和推崇的一个最佳实践。这也正是为什么像 Kata Containers、gVisor 甚至虚拟机这样的“非典型”容器,都可以无缝接入到 Kubernetes 项目里的重要原因。
|
||||
|
||||
# 思考题
|
||||
|
||||
请你思考一下,我前面讲解过的Device Plugin 为容器分配的 GPU 信息,是通过 CRI 的哪个接口传递给 dockershim,最后交给 Docker API 的呢?
|
||||
|
||||
感谢你的收听,欢迎你给我留言,也欢迎分享给更多的朋友一起阅读。
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<audio id="audio" title="47 | 绝不仅仅是安全:Kata Containers 与 gVisor" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/82/24/82703193a84be5389db43f9867223d24.mp3"></audio>
|
||||
|
||||
你好,我是张磊。今天我和你分享的主题是:绝不仅仅是安全之Kata Containers 与 gVisor。
|
||||
|
||||
在上一篇文章中,我为你详细地讲解了 kubelet 和 CRI 的设计和具体的工作原理。而在讲解 CRI 的诞生背景时,我也提到过,这其中的一个重要推动力,就是基于虚拟化或者独立内核的安全容器项目的逐渐成熟。
|
||||
|
||||
使用虚拟化技术来做一个像 Docker 一样的容器项目,并不是一个新鲜的主意。早在 Docker 项目发布之后,Google 公司就开源了一个实验性的项目,叫作 novm。这,可以算是试图使用常规的虚拟化技术来运行 Docker 镜像的第一次尝试。不过,novm 在开源后不久,就被放弃了,这对于 Google 公司来说或许不算是什么新鲜事,但是 novm 的昙花一现,还是激发出了很多内核开发者的灵感。
|
||||
|
||||
所以在2015年,几乎在同一个星期,Intel OTC (Open Source Technology Center) 和国内的 HyperHQ 团队同时开源了两个基于虚拟化技术的容器实现,分别叫做 Intel Clear Container 和 runV 项目。
|
||||
|
||||
而在2017年,借着 Kubernetes 的东风,这两个相似的容器运行时项目在中立基金会的撮合下最终合并,就成了现在大家耳熟能详的 Kata Containers 项目。 由于 Kata Containers 的本质就是一个精简后的轻量级虚拟机,所以它的特点,就是“像虚拟机一样安全,像容器一样敏捷”。
|
||||
|
||||
而在2018年,Google 公司则发布了一个名叫 gVisor 的项目。gVisor 项目给容器进程配置一个用 Go 语言实现的、运行在用户态的、极小的“独立内核”。这个内核对容器进程暴露 Linux 内核 ABI,扮演着“Guest Kernel”的角色,从而达到了将容器和宿主机隔离开的目的。
|
||||
|
||||
不难看到,无论是 Kata Containers,还是 gVisor,它们实现安全容器的方法其实是殊途同归的。这两种容器实现的本质,都是给进程分配了一个独立的操作系统内核,从而避免了让容器共享宿主机的内核。这样,容器进程能够看到的攻击面,就从整个宿主机内核变成了一个极小的、独立的、以容器为单位的内核,从而有效解决了容器进程发生“逃逸”或者夺取整个宿主机的控制权的问题。这个原理,可以用如下所示的示意图来表示清楚。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/95/1d/959c4c40c767acb6a3ffe6e144202e1d.png" alt="">
|
||||
|
||||
而它们的区别在于,Kata Containers 使用的是传统的虚拟化技术,通过虚拟硬件模拟出了一台“小虚拟机”,然后在这个小虚拟机里安装了一个裁剪后的 Linux 内核来实现强隔离。
|
||||
|
||||
而 gVisor 的做法则更加激进,Google 的工程师直接用 Go 语言“模拟”出了一个运行在用户态的操作系统内核,然后通过这个模拟的内核来代替容器进程向宿主机发起有限的、可控的系统调用。
|
||||
|
||||
接下来,我就来为你详细解读一下 KataContainers 和 gVisor 具体的设计原理。
|
||||
|
||||
**首先,我们来看 KataContainers**。它的工作原理可以用如下所示的示意图来描述。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8d/89/8d7bbc8acaf27adff890f0be637df889.png" alt="">
|
||||
|
||||
我们前面说过,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 进程来帮你做到。
|
||||
|
||||
而在具体的架构上,Kata Containers的实现方式同一个正常的虚拟机其实也非常类似。这里的原理,可以用如下所示的一幅示意图来表示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/16/f3/1684d0d89c170c2f8e6d050919c883f3.jpg" alt="">
|
||||
|
||||
可以看到,当 Kata Containers 运行起来之后,虚拟机里的用户进程(容器),实际上只能看到虚拟机里的、被裁减过的 Guest Kernel,以及通过 Hypervisor 虚拟出来的硬件设备。
|
||||
|
||||
而为了能够对这个虚拟机的 I/O 性能进行优化,Kata Containers 也会通过 vhost 技术(比如:vhost-user)来实现 Guest 与 Host 之间的高效的网络通信,并且使用 PCI Passthrough (PCI 穿透)技术来让 Guest 里的进程直接访问到宿主机上的物理设备。这些架构设计与实现,其实跟常规虚拟机的优化手段是基本一致的。
|
||||
|
||||
相比之下,gVisor 的设计其实要更加“激进”一些。它的原理,可以用如下所示的示意图来表示清楚。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2f/7b/2f7903a7c494ddf6989d00c794bd7a7b.png" alt="">
|
||||
|
||||
gVisor工作的核心,在于它为应用进程、也就是用户容器,启动了一个名叫 Sentry 的进程。 而Sentry 进程的主要职责,就是提供一个传统的操作系统内核的能力,即:运行用户程序,执行系统调用。所以说,Sentry 并不是使用 Go 语言重新实现了一个完整的 Linux 内核,而只是一个对应用进程“冒充”内核的系统组件。
|
||||
|
||||
在这种设计思想下,我们就不难理解,Sentry 其实需要自己实现一个完整的 Linux 内核网络栈,以便处理应用进程的通信请求。然后,把封装好的二层帧直接发送给 Kubernetes 设置的 Pod 的Network Namespace 即可。
|
||||
|
||||
此外,Sentry 对于Volume 的操作,则需要通过 9p 协议交给一个叫做 Gofer 的代理进程来完成。Gofer 会代替应用进程直接操作宿主机上的文件,并依靠seccomp机制将自己的能力限制在最小集,从而防止恶意应用进程通过 Gofer 来从容器中“逃逸”出去。
|
||||
|
||||
而在具体的实现上,gVisor 的 Sentry 进程,其实还分为两种不同的实现方式。这里的工作原理,可以用下面的示意图来描述清楚。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5a/b8/5a1d6e0291306417864033b3f40f74b8.png" alt="">
|
||||
|
||||
**第一种实现方式**,是使用Ptrace机制来拦截用户应用的系统调用(System Call),然后把这些系统调用交给 Sentry 来进行处理。
|
||||
|
||||
这个过程,对于应用进程来说,是完全透明的。而 Sentry 接下来,则会扮演操作系统的角色,在用户态执行用户程序,然后仅在需要的时候,才向宿主机发起 Sentry 自己所需要执行的系统调用。这,就是 gVisor 对用户应用进程进行强隔离的主要手段。不过, Ptrace 进行系统调用拦截的性能实在是太差,仅能供 Demo 时使用。
|
||||
|
||||
而**第二种实现方式**,则更加具有普适性。它的工作原理如下图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3f/bf/3faf90550425378be91eb8cd2f0c63bf.png" alt="">
|
||||
|
||||
在这种实现里,Sentry 会使用 KVM 来进行系统调用的拦截,这个性能比 Ptrace 就要好很多了。
|
||||
|
||||
当然,为了能够做到这一点,Sentry 进程就必须扮演一个 Guest Kernel 的角色,负责执行用户程序,发起系统调用。而这些系统调用被 KVM 拦截下来,还是继续交给 Sentry 进行处理。只不过在这时候,Sentry 就切换成了一个普通的宿主机进程的角色,来向宿主机发起它所需要的系统调用。
|
||||
|
||||
可以看到,**在这种实现里,Sentry 并不会真的像虚拟机那样去虚拟出硬件设备、安装 Guest 操作系统。它只是借助 KVM 进行系统调用的拦截,以及处理地址空间切换等细节。**
|
||||
|
||||
值得一提的是,在 Google 内部,他们也是使用的第二种基于 Hypervisor 的gVisor 实现。只不过 Google 内部有自己研发的 Hypervisor,所以要比 KVM 实现的性能还要好。
|
||||
|
||||
通过以上的讲述,相信你对 Kata Containers 和 gVisor 的实现原理,已经有一个感性的认识了。需要指出的是,到目前为止,gVisor 的实现依然不是非常完善,有很多 Linux系统调用它还不支持;有很多应用,在 gVisor 里还没办法运行起来。 此外,gVisor也暂时没有实现一个 Pod 多个容器的支持。当然,在后面的发展中,这些工程问题一定会逐渐解决掉的。
|
||||
|
||||
另外,你可能还听说过 AWS 在2018年末发布的一个叫做 Firecracker 的安全容器项目。这个项目的核心,其实是一个用 Rust 语言重新编写的 VMM(即:虚拟机管理器)。这就意味着, Firecracker 和 Kata Containers 的本质原理,其实是一样的。只不过, Kata Containers 默认使用的 VMM 是 Qemu,而 Firecracker,则使用自己编写的 VMM。所以,理论上,Kata Containers 也可以使用 Firecracker 运行起来。
|
||||
|
||||
## 总结
|
||||
|
||||
在本篇文章中,我为你详细地介绍了拥有独立内核的安全容器项目,对比了 KataContainers 和 gVisor 的设计与实现细节。
|
||||
|
||||
在性能上,KataContainers 和 KVM 实现的 gVisor 基本不分伯仲,在启动速度和占用资源上,基于用户态内核的 gVisor 还略胜一筹。但是,对于系统调用密集的应用,比如重 I/O 或者重网络的应用,gVisor 就会因为需要频繁拦截系统调用而出现性能急剧下降的情况。此外,gVisor 由于要自己使用 Sentry 去模拟一个Linux 内核,所以它能支持的系统调用是有限的,只是 Linux 系统调用的一个子集。
|
||||
|
||||
不过,gVisor 虽然现在没有任何优势,但是这种通过在用户态运行一个操作系统内核,来为应用进程提供强隔离的思路,的确是未来安全容器进一步演化的一个非常有前途的方向。
|
||||
|
||||
值得一提的是,Kata Containers 团队在 gVisor 之前,就已经 Demo 了一个名叫 Linuxd 的项目。这个项目,使用了 User Mode Linux (UML)技术,在用户态运行起了一个真正的 Linux Kernel 来为应用进程提供强隔离,从而避免了重新实现 Linux Kernel 带来的各种麻烦。
|
||||
|
||||
有兴趣的话,你可以[在这里查看](https://lc32018.sched.com/event/ER8x/run-linux-kernel-as-a-daemon-lai-jiangshan-hypersh)这个演讲。我相信,这个方向,应该才是安全容器进化的未来。这比 Unikernels 这种根本不适合实际场景中使用的思路,要靠谱得多。
|
||||
|
||||
>
|
||||
本篇图片出处均引自[ Kata Containers 的官方对比资料](https://www.openstack.org/assets/presentation-media/kata-containers-and-gvisor-a-quantitave-comparison.pdf)。
|
||||
|
||||
|
||||
## 思考题
|
||||
|
||||
安全容器的意义,绝不仅仅止于安全。你可以想象一下这样一个场景:比如,你的宿主机的 Linux 内核版本是3.6,但是应用却必须要求 Linux 内核版本是4.0。这时候,你就可以把这个应用运行在一个 KataContainers 里。那么请问,你觉得使用 gVisor 是否也能提供这种能力呢?原因是什么呢?
|
||||
|
||||
感谢你的收听,欢迎你给我留言,也欢迎分享给更多的朋友一起阅读。
|
||||
Reference in New Issue
Block a user