归根结底,Kubernetes Node(kubelet)的主要功能就是启动和停止容器的组件,我们称之为容器运行时(Container Runtime),其中最知名的就是Docker了。为了更具扩展性,Kubernetes从1.5版本开始就加入了容器运行时插件API,即Container Runtime Interface,简称CRI。
每个容器运行时都有特点,因此不少用户希望Kubernetes能够支持更多的容器运行时。Kubernetes从1.5版本开始引入了CRI接口规范,通过插件接口模式,Kubernetes无须重新编译就可以使用更多的容器运行时。CRI包含Protocol Buffers、gRPC API、运行库支持及开发中的标准规范和工具。Docker的CRI实现在Kubernetes 1.6中被更新为Beta版本,并在kubelet启动时默认启动。
可替代的容器运行时支持是Kubernetes中的新概念。在Kubernetes 1.3发布时,rktnetes项目同时发布,让rkt容器引擎成为除Docker外的又一选择。然而,不管是Docker还是rkt,都用到了kubelet的内部接口,同kubelet源码纠缠不清。这种程度的集成需要对kubelet的内部机制有非常深入的了解,还会给社区带来管理压力,这就给新生代容器运行时造成了难以跨越的集成壁垒。CRI接口规范尝试用定义清晰的抽象层清除这一壁垒,让开发者能够专注于容器运行时本身。
kubelet使用gRPC框架通过UNIX Socket与容器运行时(或CRI代理)进行通信。在这个过程中kubelet是客户端,CRI代理(shim)是服务端,如图2.7所示。

图2.7 CRI的主要组件
Protocol Buffers API包含两个gRPC服务:ImageService和RuntimeService。
◎ ImageService提供了从仓库中拉取镜像、查看和移除镜像的功能。
◎ RuntimeService负责Pod和容器的生命周期管理,以及与容器的交互(exec/attach/port-forward)。rkt和Docker这样的容器运行时可以使用一个Socket同时提供两个服务,在kubelet中可以用--container-runtime-endpoint和--image-service-endpoint参数设置这个Socket。
Pod由一组应用容器组成,其中包含共有的环境和资源约束。在CRI里,这个环境被称为PodSandbox。Kubernetes有意为容器运行时留下一些发挥空间,它们可以根据自己的内部实现来解释PodSandbox。对于Hypervisor类的运行时,PodSandbox会具体化为一个虚拟机。其他例如Docker,会是一个Linux命名空间。在v1alpha1 API中,kubelet会创建Pod级别的cgroup传递给容器运行时,并以此运行所有进程来满足PodSandbox对Pod的资源保障。
在启动Pod之前,kubelet调用RuntimeService.RunPodSandbox来创建环境。这一过程包括为Pod设置网络资源(分配IP等操作)。PodSandbox被激活之后,就可以独立地创建、启动、停止和删除不同的容器了。kubelet会在停止和删除PodSandbox之前首先停止和删除其中的容器。
kubelet的职责在于通过RPC管理容器的生命周期,实现容器生命周期的钩子、存活和健康监测,以及执行Pod的重启策略等。
RuntimeService服务包括对Sandbox和Container操作的方法,下面的伪代码展示了主要的RPC方法:


众所周知,Kubernetes的最小调度单元是Pod,它曾经可能采用的一个CRI设计就是复用Pod对象,使得容器运行时可以自行实现控制逻辑和状态转换,这样一来,就能极大地简化API,让CRI能够更广泛地适用于多种容器运行时。但是经过深入讨论之后,Kubernetes放弃了这一想法。
首先,kubelet有很多Pod级别的功能和机制(例如crash-loop backoff机制),如果交给容器运行时去实现,则会造成很重的负担;然后,Pod标准还在快速演进。很多新功能(如初始化容器)都是由kubelet完成管理的,无须交给容器运行时实现。
CRI选择了在容器级别进行实现,使得容器运行时能够共享这些通用特性,以获得更快的开发速度。这并不意味着设计哲学的改变—kubelet要负责、保证容器应用的实际状态和声明状态的一致性。
Kubernetes为用户提供了与Pod及其中的容器进行交互的功能(kubectl exec/attach/port-forward)。kubelet目前提供了两种方式来支持这些功能:①调用容器的本地方法;②使用Node上的工具(例如nsenter及socat)。
因为多数工具都假设Pod用Linux namespace做了隔离,因此使用Node上的工具并不是一种容易移植的方案。在CRI中显式定义了这些调用方法,让容器运行时进行具体实现。下面的伪代码显示了Exec、Attach、PortForward这几个调用需要实现的RuntimeService方法:

目前还有一个潜在的问题是,kubelet处理所有的请求连接,使其有成为Node通信瓶颈的可能。在设计CRI时,要让容器运行时能够跳过中间过程。容器运行时可以启动一个单独的流式服务来处理请求(还能对Pod的资源使用情况进行记录),并将服务地址返回给kubelet。这样kubelet就能反馈信息给API Server,使之可以直接连接到容器运行时提供的服务,并连接到客户端。
要尝试新的kubelet-CRI-Docker集成,只需为kubelet启动参数加上--enable-cri=true开关来启动CRI。这个选项从Kubernetes 1.6开始已经作为kubelet的默认选项了。如果不希望使用CRI,则可以设置--enable-cri=false来关闭这个功能。
查看kubelet的日志,可以看到启用CRI和创建gRPC Server的日志:


创建一个Deployment:

查看Pod的详细信息,可以看到将会创建沙箱(Sandbox)的Event:

这表明kubelet使用了CRI接口来创建容器。
目前已经有多款开源CRI项目可用于Kubernetes:Docker、CRI-O、Containerd、frakti(基于Hypervisor的容器运行时),各CRI运行时的安装手册可参考官网的说明。