CRIshim?实现CRI接口的容器运行时通常称为CRIshim。这是一个监听本地unix套接字的gRPC服务器;而kubelet作为gRPC客户端调用CRI接口来管理Pod、容器和镜像的生命周期。另外,容器在运行的时候,需要负责管理容器的网络。推荐使用CNI。kubelet在调用底层容器运行时的执行过程时,并不是直接调用DockerAPI,而是通过一组叫做CRI(ContainerRuntimeInterface,容器运行时接口)的gRPC接口间接执行,这意味着你需要使用新的连接方法与docker通信。k8s为了兼容之前的版本,提供了docker的CRI实现,即kubelet包下的dockershim包。dockershim是一个grpc服务,它侦听kubelet连接到的端口。dockershim收到来自kubelet的请求后,将其转换成RESTAPI请求,发送给dockerdaemon。Kubernetes项目之所以在kubelet中引入这样一个单独的抽象层,当然是为了屏蔽Kubernetes不受底层容器运行时差异的影响。解决思路再次体现了《代码大全2》中提到的经典名言:计算机科学中的任何问题都可以被另一层间接所爱。计算机科学中的任何问题都可以通过添加一个中间层来解决,而我们的CRIshim就添加了这样一个层。CRIshimserver接口示意图**CRI接口包括两个服务,RuntimeService和ImageService,可以在一个gRPC服务器上实现,也可以分离成两个独立的服务。**目前社区很多runtime都是在gRPCserver中实现的。ImageServiceServer提供了5个接口来管理容器镜像。图片管理的ImageService提供了五个接口:查询图片列表;拉取镜像到本地;查询图像状态;删除本地镜像;查询镜像占用的空间等。容器镜像的操作比较简单,暂且略过。接下来主要给大家讲解一下RuntimeService部分。RuntimeService提供了更多的接口,根据功能可以分为四组:PodSandbox管理接口:CRI设计的一个重要原则是保证接口本身只关注容器,不关注Pod。PodSandbox是KubernetesPod的抽象,用于为容器提供隔离环境(比如挂载在同一个CGroup下),提供网络等共享命名空间。PodSandbox通常对应一个Pause容器或者虚拟机;容器管理接口:创建、启动、停止、删除指定PodSandbox中的容器;StreamingAPI接口:包括Exec、Attach、PortForward用于与容器进行数据交互的接口,这三个接口在运行时返回的是StreamingServer的URL,而不是直接与容器进行交互。Kubelet需要与容器项目保持持久连接来传输数据。这种API,我们称之为StreamingAPI。状态接口:包括查询API版本和查询运行时状态。我们通过kubectl命令运行一个Pod,然后Kubelet会通过CRI进行如下操作:首先调用RunPodSandbox接口创建一个Pod容器。Pod容器用于存放容器的相关资源,如网络空间、PID空间、进程空间等资源;然后调用CreateContainer接口在Pod容器的空间创建一个业务容器;然后调用StartContainer接口启动运行容器,最后调用停止,销毁容器的接口是StopContainer和RemoveContainer。整个Container的生命周期就完成了。StreamingAPICRIshim对StreamingAPI的实现依赖于一套独立的StreamingServer机制。StreamingAPI用于客户端与容器进行交互,包括三个接口:Exec、PortForward、Attach。kubelet内置的Docker通过nsenter、socat等支持这些特性,但不一定适用于其他运行时,也不支持Linux以外的其他平台。因此,CRI也明确定义了这些API,并要求容器运行时返回一个StreamingServerURL,以便kubelet可以重定向APIServer发送的流请求。由于容器所有的流请求都会经过kubelet,这可能会对节点的网络流量造成瓶颈,因此CRI需要容器runtime单独启动一个请求对应的流服务器,并将地址返回给kubelet。kubelet将此信息返回给KubernetesAPIServer,它直接打开一个到运行时提供的服务器的流式连接,并通过它与客户端通信。这样一个完整的Exec流程如上图所示,分为多个阶段:Clientkubectlexec-i-t...;kube-apiserver向kubelet发送流请求/exec/;kubelet通过CRI接口RequestExecURL发送CRIShim;CRIShim返回ExecURL给ku??belet;kubelet将重定向的响应返回给kube-apiserver;kube-apiserver将流请求重定向到ExecURL,然后流数据在CRIShim内部的StreamingServer与kube-apiserver交互,完成Exec的请求和响应。也就是说,apiserver实际上是和streamingserver进行交互,获取我们的streaming数据。这使得我们整个CRIServer的界面更轻巧、更可靠。注意:当然,StreamingServer本身需要使用SIG-Node为您维护的StreamingAPI库来实现。并且,流式服务器将在CRIshim启动时一起启动。另外,StreamServer部分如何实现,可以由CRIshim的维护者决定。比如Docker项目,dockershim就是通过直接调用Docker的ExecAPI来实现的。CRI-containerd架构分析及主界面分析整个架构看起来非常直观。这里的Metaservices、Runtimeservice和Storageservice是containerd提供的接口。它们是常见的容器相关接口,包括镜像管理、容器运行时管理等。CRI在此之上包装了一个gRPC服务。右边是具体容器的实现。例如,在创建容器时,您需要创建特定的运行时及其containerd-shim。Container和PodSandbox组成一个Pod。CRI-containerd的好处之一就是containerd额外实现了更丰富的容器接口,因此可以使用containerd提供的ctr工具来调用这些丰富的容器运行时接口,而不仅仅是CRI接口。CRI实现了两个GRPC协议API,提供了两个服务ImageService和RuntimeService。//grpcServicesareallthegrpcservicesprovidedbycricontainerd.typegrpcServicesinterface{runtime.RuntimeServiceServerruntime.ImageServiceServer}//CRIServiceistheinterfaceimplementmentCRIremoteserviceserver.typeCRIServiceinterface{Run()error//io.Closerisusedbycontainerdtogracefullystopcriservice.io.Closerplugin.ServicegrpcServices里面包含很多最重要的实现是cni中的组件RI。CNI,用于配置容器网络。还有containerd.Client,用于连接containerd创建容器。//criServiceimplementsCRIService.typecriServicestruct{//configcontainsallconfigurations.configcriconfig.Config//imageFSPathisthepathtoimagefilesystem.imageFSPathstring//osisaninterfaceforallrequiredosoperations.ososinterface.OS//sandboxStorestoresallresourcesassociatedwithsandboxes.sandboxStore*sandboxstore.Store//sandboxNameIndexstoresallsandboxnamesandmakesureeachname//isunique.sandboxNameIndex*registrar.Registrar//containerStorestoresallresourcesassociatedwithcontainers.containerStore*containerstore.Store//containerNameIndexstoresallcontainernamesandmakesureeach//nameisunique.containerNameIndex*registrar.Registrar//imageStorestoresallresourcesassociatedwithimages.imageStore*imagestore.Store//snapshotStorestoresinformationofallsnapshots.snapshotStore*snapshotstore.Store//netPluginisusedtosetupandteardownnetworkwhenrun/stoppodsandbox.netPlugincni.CNI//clientisaninstanceofthecontainerdclientclient*containerd.Client//streamServer是streamingserverservescontainerstreamingrequest.streamServerstreaming.Server//eventMonitoristhemonitormonitorscontainerdevents.eventMonitor*eventMonitor//initializedindicateswhethertheserverisinitialized.AllGRPCservices//shouldreturnerrorbeforetheserverisinitialized.initializedatomic.Bool//cniNetConfMonitorisusedtoreloadcninetworkconfifthereis//anyvalidfschangeeventsfromcninetworkconfdir.cniNetConfMonitor*cniNetConfSyncer//baseOCISpecscontainscachedOCIspecsloadedvia`Runtime.BaseRuntimeSpec`baseOCISpecsmap[string]*oci.Spec}我们知道Kubernetes的一个运行机制是面向终态的。在每个coordinationcycle中,Kubelet都会从apiserver获取调度到Node的Pod的数据,然后执行一个面向最终状态的过程,实现我们的期望状态循环的第一步,就是获取容器的状态通过列表界面。确保有一面镜子。如果没有镜像,拉取镜像,通过Sandbox和Container接口创建容器。需要注意的是,我们的CNI(ContainerNetworkInterface)也是在CRI上运行的,因为我们在创建Pod的时候,需要同时创建网络资源,然后注入到Pod中(PS:CNI包含在创建Pod的操作)。接下来是我们的容器和图像。我们通过特定的容器创建引擎来创建特定的容器。执行过程为:Kubelet通过CRIruntimeserviceAPI调用CRIplugin创建podCRI通过CNI创建pod网络配置和命名空间CRI使用containerd创建并启动一个暂停容器(sandboxcontainer)并将容器放置在pod的cgroups/namespaceKubelet然后通过CRI镜像服务API调用CRI插件获取容器镜像CRI通过containerd获取容器镜像Kubelet通过CRI运行时服务API调用CRI,使用拉取的镜像启动pod空间中的容器CRI通过containerd创建/启动应用容器,并将容器放置在pod的cgroups/namespace中。Pod完成启动。综上,发现CRI只服务于Kubernetes,呈现上报状态。它适用于Kubernetes,不适用于OCI。所以当你做这个集成的时候,你会发现,尤其是对于VMgVisor\KataContainer,它并不对应CRI的很多假设或者API的写法。所以你的融合工作会比较困难,这是一种不匹配的状态。最后一个就是我们维护起来非常困难,因为有了CRI之后,比如RedHat有自己的CRI实现叫cri-o,本质上和containerd是一样的。最后都是依赖runC来启动容器。需要像cri-o这样的东西吗?我们不知道,如果我想使用Katacontainer和containerd来运行更多,我需要为它们编写两部分集成,将Kata集成到其中。这样很麻烦,也就是说我有100个这样的CRI,我要写100个shim来集成,而且它们的功能都是重复的。所以这创建了一个类似ContainerdShimV2的shim来解决这个问题。我们下次再分解。参考https://time.geekbang.org/column/article/71499?utm_campaign=guanwang&utm_source=baidu-ad&utm_medium=ppzq-pc&utm_content=title&utm_term=baidu-ad-ppzq-titlehttps://blog.frognew.com/2021/04/relearning-container-02.htmlhttps://github.com/kubernetes-sigs/cri-tools/blob/master/docs/crictl.mdhttps://developer.aliyun.com/article/679993本文转载自微信公众号《运维开发故事》
