Kubernetes已经成为容器编排调度领域的事实标准。其优秀的架构不仅保证了丰富的容器编排和调度功能,还提供了多层次的扩展接口,满足用户的定制化需求。其中,容器运行时是Kubernetes管理和运行容器的关键组件。当然,它也提供了一个简单易用的扩展接口,即CRI(ContainerRuntimeInterface)。本文将介绍CRI的起源、演变和未来展望。主要内容分为四个部分:Kubernetes架构介绍、ContainerRuntime接口的基本原理、ContainerRuntime的演进和未来展望。Kubernetes简介我们知道Kubernetes是一个开源的容器集群管理系统,发展非常迅速,已经成为最新最活跃的容器编排系统。从架构的角度来看,Kubernetes的组件可以分为Master和Node两部分。Master是整个集群的大脑,Master负责所有的编排、调度和API访问。具体来说,Master包括以下组件:etcd保存了整个集群的状态。kube-apiserver提供资源操作的唯一入口,提供认证、授权、访问控制、API注册和发现机制。kube-controller-manager负责维护集群的状态,包括很多资源的控制器,是保证Kubernetes声明式API工作的大脑。kube-scheduler负责资源调度,根据预定的调度策略将Pod调度到相应的Nodes上;而Node负责运行具体的容器,并为容器提供存储、网络等必要的功能:kubelet负责维护容器的生命周期,同时负责Volume(CSI)和网络(CNI)的管理;容器运行时负责图像管理以及Pod和容器的实际运行。Kubelet默认的容器运行时是Docker;kube-proxy负责为Service提供集群内的服务发现和负载均衡。Network插件还负责基于CNI(ContainerNetworkInterface)为容器配置网络。除了这些核心组件,Kubernetes当然还包括很多丰富的功能,这些功能都是通过扩展(Addon)来部署的。例如,kube-dns和metrics-server作为容器部署在集群中,并提供API供其他组件调用。提示:在Kubernetes中,通常可以听到两种不同类型的扩展Kubernetes功能的方式:1)第一种是扩展(Addon),比如dashboard、EFK、Prometheus、各种Operator等,这些扩展不需要Kubernetes提供标准接口,但它们都为Kubernetes添加了新功能;2)另一种方式是Plugin,如CNI、CRI、CSI、DevicePlugin等,由Kubernetes的各个核心组件提供。标准内置接口和外部插件实现这些接口,将Kubernetes扩展到更多用例场景。Kubelet架构刚刚提到,Kubelet负责维护容器的生命周期。此外,它还配合kube-controller-manager管理容器的存储卷,配合CNI管理容器的网络。下面是Kubelet的简单架构示意图:可以发现Kubelet也是由很多组件组成的,包括kubeletServer对外提供的用于服务调用的API,如kube-apiserver、metrics-server等。例如,kubectlexec需要通过KubeletAPI/exec/{token}与容器进行交互。ContainerManager管理容器的各种资源,如cgroups、QoS、cpuset、device等。VolumeManager管理容器的存储卷,如格式化磁盘,挂载到Node本地,最后将挂载路径传递给容器。Eviction负责容器的驱逐,比如在资源不足时驱逐低优先级的容器,保证高优先级容器的运行。cAdvisor负责为容器提供指标数据源。指标和统计信息提供容器和节点指标数据。比如metrics-server通过/stats/summary提取的metric数据,是HPA自动扩容的依据。再往下是GenericRuntimeManager,它是容器运行时的管理者,负责与CRI交互,完成对容器和镜像的管理。在CRI下,有两种容器运行时实现。一种是内置的dockershim,实现了对docker容器引擎的支持和对CNI网络插件(包括kubenet)的支持。另一种是外部容器运行时,用于支持runc、containerd、gvisor等外部容器运行时。Kubelet通过CRI接口与外部容器运行时进行交互,上图右侧是其架构CRI容器运行时。它通常包括以下组件:CRIServer,即CRIgRPC服务器,监听unixsocket。在讨论容器运行时时,这个Server通常也被称为CRIshim(夹在容器引擎和Kubelet之间的层)。StreamingServer,提供流媒体API,用于Exec、Attach、PortForward等流媒体接口。容器和镜像管理,例如拉取镜像、创建和启动容器等。CNI网络插件支持为容器配置网络。***是容器引擎(ContainerEngine)的管理,比如支持runc、containerd或者支持多个容器引擎。这样,Kubernetes中的容器运行时可以根据功能的不同分为三个部分:首先是GenericRuntimeManager,Kubelet中容器运行时的管理模块,通过CRI对容器和镜像进行管理。二是容器运行时接口,是Kubelet与外部容器运行时的通信接口。三是具体的容器运行时实现,包括Kubelet内置的dockershim和外部容器运行时(如cri-o、cri-containerd等)。ContainerRuntimeInterface(CRI)ContainerRuntimeInterface(CRI),顾名思义,就是用来扩展Kubernetes中的容器运行时,让用户可以选择自己喜欢的容器引擎。CRI基于gPRC。用户无需关心内部通信逻辑,只需要实现定义的接口,包括RuntimeService和ImageService:RuntimeService负责管理Pod和容器的生命周期,ImageService负责镜像的生命周期管理.除了gRPCAPI之外,CRI还包括用于实现流式服务器(用于Exec、Attach、PortForward等)和CRI工具的库。后面会详细介绍这两个。基于CRI接口的容器运行时通常称为CRIshim,它是一个监听本地unixsocket的gRPCServer;而kubelet作为gRPC客户端,调用CRI接口。另外,外部容器在运行时,需要负责管理容器的网络。推荐使用CNI,与Kubernetes网络模型一致。CRI的引入为容器社区带来了新的繁荣。针对不同场景诞生了cri-o、frakti、cri-containerd等一系列容器运行时:cri-containerd——基于containerd的容器运行时cri-o——基于OCI的容器运行时frakti——基于虚拟化的容器runtime,并基于这些容器运行时,可以很方便的对接新的容器引擎,比如可以使用clearcontainer、gVisor等新的容器引擎,配合cri-o或者cri-containerd等,很容易对接Kubernetes,扩展Kubernetes的应用场景到传统IaaS才能实现的强隔离、多租户场景。使用CRI运行时,需要将kubelet的--container-runtime参数配置为remote,并将--container-runtime-endpoint设置为要监控的unixsocket的位置(Windows上为tcp或npipe)。CRI界面那么,CRI界面是什么样子的呢?CRI接口包括两个服务,RuntimeService和ImageService。这两个服务可以在一个gRPC服务器上实现,也可以拆分成两个独立的服务。目前社区中很多运行时都是在gRPC服务器中实现的。镜像管理ImageService提供了五个接口,分别是查询镜像列表、拉取镜像到本地、查询镜像状态、删除本地镜像、查询镜像占用空间。这些很容易映射到dockerAPI或CLI。RuntimeService提供了更多的接口,根据功能可以分为四组:PodSandbox管理接口:PodSandbox是KubernetesPod的抽象,用于为容器提供隔离的环境(比如挂载在同一个cgroup下),并提供共享命名空间,例如网络。PodSandbox通常对应一个Pause容器或者虚拟机。Container的管理接口:在指定的PodSandbox中创建、启动、停止和删除容器。StreamingAPI接口:包括Exec、Attach、PortForward这三个与容器进行数据交互的接口。这三个接口在运行时返回StreamingServer的URL,而不是直接与容器交互。状态接口,包括查询API版本和查询运行时状态。StreamingAPIStreamingAPI用于客户端和容器需要交互的场景,所以使用了流接口,包括Exec、PortForward、Attach。Kubelet内置的docker通过nsenter、socat等支持这些特性,但它们不一定适用于其他运行时。因此,CRI也明确定义了这些API,并要求容器运行时返回一个流媒体服务器URL,以便Kubelet可以重定向APIServer发送的流媒体请求。这样一个完整的Exec流程就是客户端kubectlexec-i-t...kube-apiserver向Kubelet发送流式请求/exec/kubelet通过CRI接口向CRIShim请求Exec的URL,CRIShim返回ExecURL到Kubelet。kube-apiserver返回一个重定向的响应kube-apiserver将流请求重定向到ExecURL,然后CRIShim内部的StreamingServer与kube-apiserver交互完成Exec请求和响应在v1.10及更早版本中,容器运行时必须返回一个APIServer可以直接访问的URL(通常使用与Kubelet相同的监听地址);并且从v1.11开始,Kubelet增加了--redirect-container-streaming(默认为false),默认不再转发而是代理Streaming请求,这样在运行时可以返回一个localhostURL。通过Kubelet代理的好处是Kubelet处理与API服务器通信之间的请求认证。事实上,每个运行时流服务器的处理框架都是相似的,所以Kubelet也提供了一个流服务器库,方便容器运行时开发者参考。容器运行时的演化过程了解了容器运行时接口的基本原理之后,接下来,我们来看一下容器运行时的演化过程。容器运行时的演进可以分为三个阶段:首先,在Kubernetesv1.5之前,Kubelet内置了对Docker和rkt的支持,并通过CNI网络插件为其配置容器网络。这个阶段的用户如果需要自定义runtime函数,会比较痛苦。他们需要修改Kubelet的代码,而这些修改很可能无法推送到上游社区。这样还需要维护自己的fork仓库,维护和升级都非常麻烦。不同用户实现的容器运行时各有千秋,很多用户希望Kubernetes支持自定义运行时。因此,从v1.5开始,增加了CRI接口,通过容器运行时的抽象层去除了这些障碍,使得Kubelet可以在不修改Kubelet的情况下支持运行多个容器运行时。CRI接口包括一套ProtocolBuffers、gRPCAPI、streaming接口的库,以及一系列调试和验证的工具等。现阶段,内置的Docker实现也逐步迁移到CRI接口。但是此时rkt还没有完全迁移,因为从rkt迁移CRI的过程会在一个独立的repository中完成,方便其维护和管理。第三阶段,从v1.11开始,删除Kubelet内置的rkt代码,将CNI的实现迁移到dockershim。这样除了docker之外,其他容器运行时都是通过CRI访问的。除了实现CRI接口外,外部容器运行时还负责为容器配置网络。一般推荐使用CNI,因为它可以支持社区很多网络插件,但这不是必须的。网络插件只需要满足Kubernetes网络的基本假设,即IP-per-Pod,所有Pod和Node可以直接通过IP相互访问。CRIContainerRuntimeCRI的引入给容器社区带来了新的繁荣,适用于各种场景的运行时应运而生。例如:这里还要注意CRI容器运行时和容器引擎的区别:CRI容器运行时是指实现了KubeletCRI接口的运行时,从而可以无缝集成到Kubernetes中。容器引擎只是一个负责管理容器镜像和运行容器的服务。它还有一个称为OCI(OpenContainerInitiative)的标准。例如,CNCF的ContainerRuntimeLandscape包括一系列“ContainerRuntime”,其中一些实现了CRI,例如cri-o;而more只是一个容器引擎,需要通过cri-o,cri-只有containerd才能应用于Kubernetes。CRI工具CRI工具是一个非常有用的工具,用于对Kubernetes容器运行时和容器应用程序进行故障排除。它是SIGNode的子项目,可以应用于所有实现CRI接口的容器运行时。CRITools包括两个组件,用于故障排除和调试的crictl和用于一致性验证的容器运行时实现的critest。crictl让我们先看看crictl。crictl提供了一个类似于docker命令的命令行工具。在排查或调试容器应用时,有时系统管理员需要登录节点查看容器或镜像的状态,以收集系统和容器应用的信息。这时候推荐使用crictl来完成这些任务,因为crictl为所有不同的容器引擎提供了一致的体验。在使用上,crictl的使用与docker命令行工具非常相似。例如,可以使用crictlpods查询PodSandboxes列表,使用crictlps查询容器列表,使用crictlimages查询镜像列表。需要注意的是,crictl的设计目标是故障排除,而不是替代docker或kubectl。例如,由于CRI没有定义镜像构建的接口,所以crictl没有像dockerbuild那样提供构建镜像的功能。但由于crictl提供了一个面向Kubernetes的接口,相比于docker,crictl可以提供更清晰的容器和Pod视图。critestcritest是一个容器运行时一致性验证工具,用于验证容器运行时是否符合KubeletCRI要求。它是CRITOOLS工具的一部分。除了验证测试,critest还提供了CRI接口的性能测试,比如critest-benchmark。建议将critest集成到CRI容器运行时的Devops进程中,保证每次改动都不会破坏CRI的基本功能。另外,你也可以选择将critest和KubernetesNodeE2E的测试结果提交给Sig-node的TestGrid,展示给社区和用户。未来展望DockerRuntimeSplit当前,Docker是内置于Kubelet中的运行时,是默认的容器运行时。这样一来,Kubelet实际上会依赖于Docker,这会给Kubelet本身带来一定的维护负担。例如,Kubelet内部的某些功能可能仅适用于Docker运行时。当Docker或Docker依赖的其他组件(如containerd、runc)发现严重缺陷时,修复这些缺陷需要重新编译发布Kubelet。此外,当用户想要向Docker运行时添加新功能时,这些新功能可能不会轻易引入到Kubelet中,尤其是在三个月的发布周期中,新功能通常不会引入到现有的稳定分支中。从Dockerruntime的角度来看,新特性的引入也普遍较慢。因此,将Docker容器引擎拆分出来,独立成一个cri-docker,就可以解决上述所有问题。由于Docker作为默认的容器引擎已经广泛应用于生产环境,拆分迁移将适用于大部分用户,具体的迁移方式需要社区进行详细讨论。强隔离容器引擎虽然Kubernetes提供了基本的多租户功能,可以将不同的应用隔离在不同的namespace中,也可以使用RBAC来控制不同用户对各种资源的访问,但是由于Docker共享内核的特性,在Kubernetes中运行不受信任的应用程序仍然存在很大的安全风险。为了消除这个问题,强隔离容器引擎应运而生。最早的强隔离容器引擎是hyperd和clearcontainer,是Kata容器的前身,将KubernetesPod作为虚拟机运行,通过虚拟化实现容器应用的强隔离。虚拟化是整个云计算中IaaS的基础,其安全性已经得到广泛验证,因此其安全性是有保障的。这两个项目现已合并到Kata容器中。除了Kata容器,Google和AWS也在推广强隔离的容器引擎,即gVisor和Firecraker。与Kata容器不同的是,gVisor并没有创建一个完整的VM,而是实现了一个自己的沙箱(文档变成了用户态内核),对容器的syscalls进行拦截和过滤,从而达到安全隔离的目的。gVisor虽然比VM轻量级,但是拦截和过滤也会带来很高的成本,对最终容器应用的性能造成一定的损失。同样,Firecraker实现了一个基于KVM的轻量级VM,称为microVM。与Kata不同的是,它没有使用QEMU,而是使用Rust构建了一个精简的设备模型,使得每个microVM只占用大约5MB的内存。多容器运行时有了强隔离的容器引擎之后,不可避免地会出现一些新的问题。例如,许多Kubernetes自己的服务或扩展无法在强隔离环境中运行,因为它们需要HostNetwork或特权模式。因此,多容器运行时应运而生。这样就可以使用runc/docker运行特权应用,使用强隔离容器引擎运行普通应用。例如,一个典型的组合是:runc+katarunc+gVisorWindowsservercontainers+Hyper-Vcontainers过去,很多容器运行时在CRIShim中支持多种容器引擎,并以注解的形式进行选择。借助新的RuntimeClass资源,可以直接通过PodSpec选择不同的运行时。apiVersion:node.k8s.io/v1beta1kind:RuntimeClassmetadata:name:myclas#RuntimeClassisnon-namespacedhandler:myconfiguration---apiVersion:v1kind:Podmetadata:name:mypodspec:runtimeClassName:myclass#...RuntimeClass本身还处于比较早期的阶段,未来还将在调度等方面继续得到进一步加强。
