我们之前说了kubelet的工作原理和CRI的来龙去脉,我们更加深入了解一下CRI设计和工作原理
我们说一下Kubernetes的架构图
在整个管理过程中可以看出,CRI机制发挥的作用,基于所有运行时容器都会实现一个自己CRI shim,自行对CRI请求进行处理,这样,Kubernetes就可以通过这个同一个抽象层CRI,使得下层容器运行时可以自由的对接进入Kubernetes当中
所以说,CRI shim是各个运行时容器厂商自由发挥的场地,除了Docker shim之外,其他的所有的运行时容器,都是需要额外部署在宿主上的
举个例子,CNCF里的container项目,就可以提供一个典型的CRI shim能力,将Kubernetes发出的CRI请求,转换为对Containerd的调用,然后创建出runC容器,而runC项目,就是类似docker容器,可以为其组合的去设置Namespace cgroups chroot等基操的组件
那么,我们就看一下contain下CRI的接口的定义,基本如下:
我们将CRI分为两组
第一组RuntimeService,提供的接口主要是和容器相关的操作,创建容器,启动容器,删除容器,exec命令
第二组,则是imageService,提供的接口,主要是容器镜像相关的操作,比如拉取镜像,删除镜像
我们主要关心的CRI设计的第一个部分,而且我们看出来,第一部分,是关于容器,而不是Pod,这是因为Pod是Kubernetes的编排概念,而不是容器运行时的概念,所以,我们就不能假设所有下层容器项目,能够暴露出可以直接映射为Pod的API
而且,如果引入了Pod的概念,那么接下来只要Pod API对象字段发生变化,CRI就可能需要改变,而在Kubernetes开发的前期,Pod对象的变化还是比较频繁的,但对于CRI这样的标准接口,变更频率就有点麻烦了
所以在CRI的设计中,并没有直接创建Pod或者启动Pod的接口
CRI中其实还是有一组名为RunPodSandbox的接口的
这个PodSandbox,对应的并不是Kubernetes的Pod Api对象,而是一部分和容器运行时的字段,比如HostName,DnsConfig,CgroupParent等,PodSandbox接口描述的,就是Kubernetes对整个Pod配置时的必要字段,就是一个Pod对象子集
所以我们利用shim决定如何使用这些字段来实现一个Kubernetes期望的Pod模型,
kubectl run创建一个名为foo的,包括A B两个容器的Pod之后,这个Pod的信息最后来到了kubelet,kubelet就会按照上面的1-5的流程进行调用CRI接口
具体的CRI shim 中,这些接口的实现是完全不同的,比如,如果Docker项目,dockershim就会创建出一个名为foo的Infra容器,用来hold住整体Pod的Network Namesapce
基于虚拟化技术的容器,比如Kate Container项目,会直接创建一个轻量级的虚拟机来hold所有的Pod
需要注意的是,在RunPodSandbox这个接口的实现,需要调用networkPlugin.SetUpPod(…)来为这个Sandbox设置网络,这个SetUpPod(…)方法执行了CNI插件的add方法,创建并加入了网络中
接下来,分别调用了CreateContainer和StartContainer接口来进行创建和启动容器A B,对应到了dockershim中,就是启动了A B 两个的容器,所以如果是docker shim来执行上述的流程,那么在执行结束后一定会得到的是三个容器,来组成这个Pod
如果是Kate Containers的话,CreateContainer和StartContainer接口的而实现,就是在之前创建的轻量级虚拟机的里面,创建两个Mount Namespae,所以在宿主机上,最后只会有一个foo名称的轻量级虚拟机在运行.
最后在上面的第一组的API中,还有一个比较重要的接口,就是exec,如何通过CRI API完成这类接口,这类接口的典型特点,就是交互双方要建立一个长连接来进行传输数据.这类API统称为Streaming API
这类Streaming API的实现,依赖于一组独立的Streaming Server机制,基本如下所示
我们对一个容器进行执行kubectl exec命令的时候,这个请求会首先交给API Server,然后API Server就会调用kubelet的Exec API
这样,kubelet就会调用CRI的exec接口,这个exec的接口,在CRI shim上,这个shim不会真的调用后端的容器API,而是返回一个URL给kubelet.这个URL就是CRI shim中的对应的Streaming Server的地址和端口
kubelet会直接将这个URL以重定向的方式返回给API Server,这样API Server就会通过重定向和Streaming Server发起真正/exec请求,从而建立连接
这个Streaming Server本身,是通过SIG-Node维护的Streaming API来实现的,并且不同的容器CRI shim实现方式不一样,对于Docker,就是直接调用Docker的Exec API实现
在本篇中,我们详细讲解了CRI的设计和具体工作原理,梳理CRI接口的实现核心流程
上面的CRI的接口讲解,说明了CRI的实现并不具体,而是一种抽象的概念,让开发者有很高的自由度
这个自由度包含了容器的生命周期管理,将Pod映射为自己的实现,如何调用CNI插件为Pod设置网络
而且因为足够的自由,导致一些非典型的容器,例如Kate Container,gVisor都能无缝接入Kubernetes项目的原因
那么我们问一下Device Plugin为容器分配GPU信息,是通过CRI哪个接口传递给dockershim的呢?
Device plugin中的allocate函数是通过container creating调用的,从而让device plugin可以获取到目录信息,必然在Createcontainer()接口