上篇文章,我们讲到容器引擎Docker与Podman,关于K8S弃用Docker的根本原因在于容器运行时接口CRI,Kubelet 之前使用的是一个命名为 dockershim 的模块,用以实现对 Docker 的 CRI 支持。具体,可参考上篇文章:容器引擎Docker与Podman解析。本文主要针对CRI进行简要解析,以使得大家能够更深入了解K8S底层运行机制,以便能够更好地掌握容器生态技能。
Kubernetes 是一款基础设施平台,可实现对多种不同计算资源(例如虚拟、物理机)进行分组,使其呈现为统一的巨量计算资源,从而为应用程序使用或与其他人共享提供支撑。在这样的架构中,Docker(或者容器运行时)仅用于通过 Kubernetes 控制平面进行调度,从而在实际主机内运行应用程序。
基于上图,我们可以看出:每个 Kubernetes 节点都与控制平面彼此通信。各个节点上的 Kubelet 获取元数据,并执行 CRI 以在该节点上运行容器的Create与Destory。
在Kubernetes v1.5 之前,Docker 作为第一个并且为独一无二的容器运行时,Kubelet 通过内嵌的 dockershim 操作 Docker API 来操作容器,进而达到一个面向终态的效果。后来,随着Rocket、Hyber以及其他“新品种”的加入,使得Kubernetes变得复杂、更加难以维护,于是Container Runtime Interface (CRI)此等接口标准应运而生。
CRI 接口的通信协议是 gRPC,gRPC性能也是优于 http/REST 模式的。gRPC 不需要手写客户端代码和服务端代码,能够自动生成通信协议代码。CRI 接口引入后的Kubelet 的架构如下图所示:
基于上图解析:Generic Runtime Manager,一个通用的运行时管理器。我们可以看到目前 dockershim 还是存在于 Kubelet 的代码中的,它是当前性能最稳定的一个容器运行时的实现。Remote 指的就是 CRI 接口。CRI 接口主要包含两个部分:
- CRI Server,即通用的比如说创建、删除容器这样的接口;
- Streaming Server,一个是流式数据的接口。比如 exec、port-forward 这些流式数据
的接口。
除此,我们所说的 CNI(容器网络接口)也是在 CRI 进行操作的,因为我们在创建 Pod 的时候需要同时创建网络资源然后注入到 Pod 中。目前,CRI 运行时的实现方案主要基于以下两种:
1、containerd
containerd 是一个全开源软件,如果只是想从 Docker 迁移出来,那么 containerd 就是最好的选择。因为它实际上就是在 Docker 之内起效,可以完成所有“运行时”工作,如上图所示。更重要的是,它提供的 CRI 其实 100% 就是由 Docker 所提供。
2、CRI-O
CRI-O 是主要由 Red Hat 员工开发的 CRI 运行时。它的最大区别在于并不依赖于 Docker,而且目前已经在 Red Hat OpenShift 中得到使用。CRI-O 的优势在于其采用极简风格,或者说它的设计本身就是作为“CRI”运行时存在。
不同于作为 Docker 组成部分的 containerd,CRI-O 在本质上属于纯 CRI 运行时、因此不包含除 CRI 之外的任何其他内容。从 Docker 迁移至 CRI-O 往往更为困难,但无论如何,CRI-O 至少可以支持 Docker 容器在 Kubernetes 上的正常运行。
接下来,我们看一下CRI简要的接口设计原理,我们借助具体的容器引擎创建容器为例,描述其行为,具体如下:
我们知道 Kubernetes 的一个运作的机制是面向终态的,在每一次调协的循环中,Kubelet 会向 Apiserver 获取调度到本 Node 的 Pod 的数据,再做一个面向终态的处理,以达到我们预期的状态。第一步,首先通过 List 接口拿到容器的状态,再通过 Sandbox 和 Container 接口来创建容器,另外还有镜像接口用来拉取容器镜像。CRI 描述了 Kubelet 期望的容器运行时行为。
通常,我们通过 Kubectl 命令来运行一个 Pod,那么 Kubelet 就会通过 CRI 执行以下相关操作,具体如下:
1、首先调用 RunPodSandbox 接口来创建一个 Pod 容器,Pod 容器是用来持有容器的相关资源的,比如说网络空间、PID空间、进程空间等资源;
2、然后调用 CreatContainer 接口在 Pod 容器的空间创建业务容器;
3、其次再调用 StartContainer 接口启动容器,相对应的销毁容器的接口为 StopContainer 与 RemoveContainer。
现在,跟大家分享一下 CRI 的流式接口 exec。它可以用来在容器内部执行一个命令,又或者说可以 attach 到容器的 IO 流中做各种交互式的命令。它的特别之处在于,一个是节省资源,另一个是连接的可靠性。其简要工作流图如下所示:
简要流程如下所示:
1、首先 exec 操作会发送到 Apiserver,经过鉴权,Apiserver 将对 Kubelet Server 发起 exec 的请求;
2、然后 Kubelet 会调用 CRI 的 exec 接口将具体的请求发至容器的运行时。这个时候,容器运行时不是直接地在 exec 接口上来服务这次请求,而是通过我们的 Streaming Server 来异步地返回每一次执行的结果。也就是说 Apiserver 其实实际上是跟 Streaming Server 交互来获取我们的流式数据的。这样一来让我们的整个 CRI Server 接口更轻量、更可靠。
接下来,我们来了解下有关涉及CRI的工作流,具体如下所示:
从上面的工作流,我们可以看到,CRI 运行时将执行以下操作:从 kubelet 获取 gRPC 请求。然后根据规范创建 OCI json 配置。
OCI 运行时负责使用 Linux 内核系统调用(例如 cgroups 与命名空间)并生成容器。
最后,我们来了解一下目前 CRI 的一些具体实现技术,主要涉及以下:
- CRI-containerd
- CRI-O
- PouchContainer
- ...
基于实际的场景应用,目前,CRI-containerd 是目前社区中比较主流的新一代 CRI 的实现,CRI-O 来自于红帽公司,PouchContainer 是由 alibaba 实现的 CRI,其它的 CRI 实现暂不赘述,大家有兴趣可以去研究。