3.4 选择容器运行时
鉴于CRI的可用性,平台构建团队在涉及容器运行时的时候获得了选择的灵活性。然而,现实情况是,在过去几年中,容器运行时已经成为一个实施细节。如果你使用Kubernetes发行版或被托管的Kubernetes服务,容器运行时很可能已经为你选择好了。即使是像Cluster API这样的社区项目也是如此,它提供了包含容器运行时的预制节点镜像。
尽管如此,如果你的确有机会选择一个运行时,或者有专门的运行时用例(例如,基于虚拟机的运行时),你应该根据信息来做出这个决定。在本节中,我们将讨论在选择容器运行时需要考虑的问题。
当我们在现场帮助一些组织团队时,我们喜欢问的第一个问题是他们对哪种容器运行时有经验。在大多数情况下,对容器有长期经验的组织正在使用Docker,并熟悉Docker的工具链和用户体验。虽然Kubernetes支持Docker,但我们并不鼓励使用它,因为它有一套Kubernetes不需要的扩展功能,如构建镜像、创建容器网络等。换句话说,对于Kuber-netes来说,成熟的Docker守护进程过于沉重或臃肿。好消息是,Docker使用的是conta-inerd,这是社区中最普遍的容器运行时之一。缺点是,平台操作者必须学习containerd CLI。
另一个要考虑的问题是可用性。你可能会在获得Kubernetes的地方得到对容器运行时的支持。Kubernetes发行版(如VMware的Tanzu Kubernetes Grid,Red Hat的OpenShift,以及其他发行版)通常会搭载一个特定的容器运行时。你应该使用官方的推荐,除非你有一个非常令人信服的理由不这样做。在这种情况下,请确保你了解使用不同容器运行时的意义。
与其密切相关的是容器运行时的一致性测试。Kubernetes项目,特别是节点特别兴趣小组(sig-node),定义了一套CRI验证测试和节点一致性测试,以确保容器运行时是兼容的,并且行为符合预期。这些测试是每个Kubernetes版本的一部分,一些运行时可能比其他运行时覆盖面更大。正如你所想象的,测试覆盖面越大越好,因为运行时的任何问题都会在Kubernetes发布过程中被发现。社区通过Kubernetes测试网格(https://k8s-testgrid.appspot.com)提供所有测试和结果。在选择运行时的时候,你应该考虑容器运行时的一致性测试,更广泛地考虑该运行时与整个Kubernetes项目的关系。
最后,你应该确定你的工作负载是否需要比Linux容器所提供的更强的隔离保证。虽然不太常见,但也有一些用例需要工作负载的虚拟机级隔离,例如执行不受信任的代码或运行需要强大的多租户保证的应用程序。在这些情况下,你可以利用专门的容器运行时,如Kata Containers。
现在我们已经讨论了选择容器运行时应该考虑的问题,让我们来回顾一下最普遍的容器运行时:Docker、containerd和CRI-O。我们还将探索Kata Containers来了解如何在虚拟机中运行Pod。最后,我们将学习Virtual Kubelet,因为它提供了另一种在Kubernetes上运行工作负载的方法。
3.4.1 Docker
Kubernetes通过称为dockershim的CRI shim支持Docker引擎作为容器运行时。shim是一个内置于kubelet的组件。本质上讲,它是一个gRPC服务器,实现了我们在前面描述的CRI服务。由于Docker引擎没有实现CRI,所以需要shim。dockershim作为一个中间件,让kubelet可以通过CRI与Docker进行通信(而不是将所有kubelet的代码特殊化),以便与CRI和Docker引擎一起工作。dockershim将处理CRI调用和Docker引擎API调用之间的转换。图3-2描述了kubelet如何通过shim与Docker进行交互。
图3-2:kubelet和Docker引擎之间通过dockershim的交互
正如我们在前面提到的,Docker在底层利用了containerd。因此,从kubelet传入的API调用最终会被转发到containerd,后者会启动容器。最终,生成的容器在containerd下结束,而不是在Docker守护进程下结束。
从故障排除的角度来看,你可以使用Docker CLI来列出和检查在特定节点上运行的容器。虽然Docker没有Pod的概念,但dockershim将Kubernetes命名空间、Pod名称和Pod ID编码到容器的名称中。例如,下面的代码显示了属于default命名空间中名为nginx的Pod容器。Pod基础设施容器(又称暂停容器)的名称中带有k8s_POD_前缀。
你也可以使用containerd CLI ctr来检查容器,尽管其输出不像Docker CLI的输出那样用户友好。Docker引擎使用了一个名为moby的容器命名空间:
最后,如果节点上有crictl,你可以使用它。crictl工具是一个由Kubernetes社区开发的命令行工具。它是一个CLI客户端,用于通过CRI与容器运行时进行交互。即使Docker没有实现CRI,你也可以通过dockershim UNIX套接字使用crictl:
3.4.2 containerd
containerd可能是我们在构建基于Kubernetes平台时遇到的最常见的容器运行时。在撰写本书时,containerd是基于Cluster API的节点镜像中的默认容器运行时,并且在各种被托管的Kubernetes产品(例如AKS、EKS和GKE)中都可用。
containerd容器运行时通过containerd CRI插件来实现CRI。CRI插件是一个原生的containerd插件,从containerd v1.1开始就可以使用,并且默认是启用的。在运行Pod时,kubelet使用这个套接字与containerd交互,如图3-3所示。
生成容器的进程树看起来与使用Docker引擎时的进程树完全一样。这是意料之中的,因为Docker引擎就是使用containerd来管理容器的:
图3-3:kubelet和containerd之间通过containerd CRI插件进行的交互
如果想要检查节点上的容器,你可以使用ctr。与Docker相反,Kubernetes管理的容器在一个叫作k8s.io而不是moby的containerd命名空间中:
你也可以使用crictl CLI通过containerd UNIX套接字与containerd进行交互:
3.4.3 CRI-O
CRI-O是一个专门为Kubernetes设计的容器运行时,它是CRI的一个实现。因此,与Docker和containerd相比,它不能在Kubernetes之外使用。在撰写本书时,CRI-O容器运行时的主要使用者之一是Red Hat的OpenShift平台。
与containerd类似,CRI-O通过UNIX套接字暴露CRI。kubelet通常使用位于/var/run/crio/crio.sock的套接字与CRI-O进行交互。图3-4描述了kubelet通过CRI直接与CRI-O交互的情况。
图3-4:kubelet和CRI-O之间使用CRI API的交互
在创建容器时,CRI-O实例化了一个名为conmon的进程。conmon是一个容器监视器。它是容器进程的父进程,并处理多种问题,例如暴露附加到容器的方法、处理容器的终止、将容器的STDOUT和STDERR流存储到日志文件中等:
由于CRI-O被设计为Kubernetes的一个低级组件,CRI-O项目不提供CLI。因此,你可以在CRI-O中使用crictl,就像在任何其他实现CRI的容器运行时中一样:
3.4.4 Kata Containers
Kata Containers是一个开源的、专门的运行时,使用轻量级的虚拟机而不是容器来运行工作负载。该项目有丰富的历史,是由之前两个基于虚拟机的运行时(Intel的Clear Containers和Hyper.sh的RunV)合并而成的。
由于使用了虚拟机,Kata提供了比Linux容器更强的隔离保证。如果你有防止工作负载共享Linux内核的安全要求,或者有cgroup隔离无法满足的资源保证要求,那么Kata Containers可能是一个很好的选择。例如,Kata Containers的一个常见用例是运行多租户的Kubernetes集群,这些集群将运行不受信任的代码。百度云(https://oreil.ly/btDL9)和华为云(https:// oreil.ly/Mzarh)等云供应商在其云基础设施中就使用了Kata Containers。
要在Kubernetes中使用Kata Containers,仍然需要一个可插拔的容器运行时放在kubelet和Kata运行时之间,如图3-5所示,原因是Kata Containers没有实现CRI。相反,它利用现有的容器运行时(如containerd)来处理与Kubernetes的交互。为了与containerd集成,Kata Containers项目实现了containerd运行时API,特别是v2 containerd-shim API(https://oreil.ly/DxGyZ)。
图3-5:kubelet和Kata Containers之间通过containerd进行的交互
因为containerd是必需的,并且在节点上可用,所以可以在同一个节点上运行Linux容器Pod和基于VM的Pod。Kubernetes提供了一个配置和运行多个容器运行时的机制,称为RuntimeClass。使用RuntimeClass API,你可以在同一个Kubernetes平台上提供不同的运行时,使开发者能够使用选择更适合他们需求的运行时。下面的代码片段是一个Kata Containers运行时的RuntimeClass例子:
要在kata-containers运行时下运行一个Pod,开发者必须在Pod的规范中指定运行时类的名称:
Kata Containers支持不同的管理程序来运行工作负载,包括QEMU(https://www.qemu.org)、NEMU(https://github.com/intel/nemu)和AWS Firecracker(https://firecracker-mic-rovm.github.io)。例如,当使用QEMU时,我们可以在启动一个使用kata-containers的运行时类Pod后看到一个QEMU进程:
Kata Containers提供了一些有趣的功能。虽然至今我们还没有看到它在生产中的使用,但是如果你在Kubernetes集群中需要虚拟机级别的隔离保障,Kata Containers是值得关注的。
3.4.5 Virtual Kubelet
Virtual Kubelet(https://github.com/virtual-kubelet/virtual-kubelet)是一个开源项目,其行为类似于kubelet,并且提供了一个可插拔的API。虽然本身不是一个容器运行时,但它的主要目的是为运行Kubernetes Pod提供替代运行时。由于Virtual Kubelet的可扩展架构,这些替代运行时基本上可以是任何可以运行应用程序的系统,如无服务器框架、边缘框架等。例如,如图3-6所示,Virtual Kubelet可以在云服务上启动Pod,如Azure Container Instances或AWS Fargate。
Virtual Kubelet社区提供了各种服务提供程序,如果它们符合你的需要,你可以利用它们,包括AWS Fargate、Azure Container Instances、HashiCorp Nomad等。如果你有一个更具体的用例,你也可以实现你自己的服务提供程序。实现一个服务提供程序需要使用Virtual Kubelet库编写一个Go程序来处理与Kubernetes的集成,包括节点注册、运行Pod以及导出Kubernetes所期望的API。
图3-6:Virtual Kubelet在云服务上运行Pod,如Azure Container Instances、AWS Fargate等
尽管Virtual Kubelet能够实现有趣的场景,但我们还没有遇到一个需要它的实际使用案例。尽管如此,知道它的存在还是很有用的,你应该把它放在你的Kubernetes工具箱里。