2.9 CRI(容器运行时接口)详解
归根结底,Kubernetes Node(kubelet)的主要功能就是启动和停止容器的组件,我们称之为容器运行时(Container Runtime),其中最知名的就是Docker了。为了更具扩展性,Kubernetes从1.5版本开始就加入了容器运行时插件API,即Container Runtime Interface,简称CRI。
2.9.1 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接口规范试图用定义清晰的抽象层清除这一壁垒,让开发者能够专注于容器运行时本身。在通向插件式容器支持及建设健康生态环境的路上,这是一小步,也是很重要的一步。
2.9.2 CRI的主要组件
kubelet使用gRPC框架通过UNIX Socket与容器运行时(或CRI代理)进行通信。在这个过程中kubelet是客户端,CRI代理(shim)是服务端,如图2.3所示。
图2.3 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。
2.9.3 Pod和容器的生命周期管理
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方法:
service RuntimeService { // 沙箱操作 rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {} rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse) {} rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse) {} rpc PodSandboxStatus(PodSandboxStatusRequest) returns (PodSandboxStatusResponse) {} rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse) {} // 容器操作 rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse) {} rpc StartContainer(StartContainerRequest) returns (StartContainerResponse) {} rpc StopContainer(StopContainerRequest) returns (StopContainerResponse) {} rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse) {} rpc ListContainers(ListContainersRequest) returns (ListContainersResponse) {} rpc ContainerStatus(ContainerStatusRequest) returns (ContainerStatusResponse) {} ...... }
2.9.4 面向容器级别的设计思路
众所周知,Kubernetes的最小调度单元是Pod,它曾经可能采用的一个CRI设计就是复用Pod对象,使得容器运行时可以自行实现控制逻辑和状态转换,这样一来,就能极大地简化API,让CRI能够更广泛地适用于多种容器运行时。但是经过深入讨论之后,Kubernetes放弃了这一想法。
首先,kubelet有很多Pod级别的功能和机制(例如crash-loop backoff机制),如果交给容器运行时去实现,则会造成很重的负担;其次且更重要的是,Pod标准还在快速演进中。很多新功能(如初始化容器)都是由kubelet完成管理的,无须交给容器运行时实现。
CRI选择了在容器级别进行实现,使得容器运行时能够共享这些通用特性,以获得更快的开发速度。这并不意味着设计哲学的改变——kubelet要负责、保证容器应用的实际状态和声明状态的一致性。
Kubernetes为用户提供了与Pod及其中的容器进行交互的功能(kubectl exec/attach/port- forward)。kubelet目前提供了两种方式来支持这些功能。
(1)调用容器的本地方法。
(2)使用Node上的工具(例如nsenter及socat)。
因为多数工具都假设Pod用Linux namespace做了隔离,因此使用Node上的工具并不是一种容易移植的方案。在CRI中显式定义了这些调用方法,让容器运行时进行具体实现。下面的伪代码显示了Exec、Attach、PortForward这几个调用需要实现的RuntimeService方法:
service RuntimeService { ...... // ExecSync在容器中同步执行一个命令。 rpc ExecSync(ExecSyncRequest) returns (ExecSyncResponse) {} // Exec在容器中执行命令 rpc Exec(ExecRequest) returns (ExecResponse) {} // Attach附着在容器上 rpc Attach(AttachRequest) returns (AttachResponse) {} // PortForward从Pod沙箱中进行端口转发 rpc PortForward(PortForwardRequest) returns (PortForwardResponse) {} ...... }
目前还有一个潜在问题是,kubelet处理所有的请求连接,使其有成为Node通信瓶颈的可能。在设计CRI时,要让容器运行时能够跳过中间过程。容器运行时可以启动一个单独的流式服务来处理请求(还能对Pod的资源使用情况进行记录),并将服务地址返回给kubelet。这样kubelet就能反馈信息给API Server,使之可以直接连接到容器运行时提供的服务,并连接到客户端。
2.9.5 尝试使用新的Docker-CRI来创建容器
要尝试新的Kubelet-CRI-Docker集成,只需为kubelet启动参数加上--enable-cri=true开关来启动CRI。这个选项从Kubernetes 1.6开始已经作为kubelet的默认选项了。如果不希望使用CRI,则可以设置--enable-cri=false来关闭这个功能。
查看kubelet的日志,可以看到启用CRI和创建gRPC Server的日志:
I0603 15:08:28.953332 3442 container_manager_linux.go:250] Creating Container Manager object based on Node Config: {RuntimeCgroupsName: SystemCgroupsName: KubeletCgroupsName: ContainerRuntime:docker CgroupsPerQOS:true CgroupRoot:/ CgroupDriver:cgroupfs ProtectKernelDefaults:false EnableCRI:true NodeAllocatableConfig:{KubeReservedCgroupName: SystemReservedCgroupName: EnforceNodeAllocatable:map[pods:{}] KubeReserved:map[] SystemReserved:map[] HardEvictionThresholds:[{Signal:memory.available Operator:LessThan Value:{Quantity:100Mi Percentage:0} GracePeriod:0s MinReclaim:<nil>}]} ExperimentalQOSReserved:map[]} ...... I0603 15:08:29.060283 3442 kubelet.go:573] Starting the GRPC server for the docker CRI shim.
创建一个Deployment:
$ kubectl run nginx --image=nginx deployment "nginx " created
查看Pod的详细信息,可以看到将会创建沙箱(Sandbox)的Event:
$ kubectl describe pod nginx ...... Events: ...From Type Reason Message ...----------------- ----- --------------- ----------------------------- ...default-scheduler Normal Scheduled Successfully assigned nginx to k8s-node-1 ...kubelet, k8s-node-1 Normal SandboxReceived Pod sandbox received, it will be created. ......
这表明kubelet使用了CRI接口来创建容器。
2.9.6 CRI的进展
目前已经有多款开源CRI项目可用于Kubernetes:Docker、CRI-O、Containerd、frakti(基于Hypervisor的容器运行时),各CRI运行时的安装手册可参考官网https://kubernetes.io/docs/setup/cri/的说明。