k8s Node 节点(kubelet)的主要功能就是启动和停止容器的组件,这组件我们称之为 容器运行时(Container Runtime)
,这其中最知名的就是 Docker 了。为了更具扩展性,k8s 从 v1.5 版本开始就加入了容器运行时插件 API,即 Container Runtime Interface,简称 CRI
。
CRI 概述
每个容器运行时都有各自的特点,因此用户希望 k8s 能够支持更多的容器运行时。k8s 从 v1.5 版本开始,引入了 CRI 接口规范,通过插件接口模式,k8s 无需重新编译就可以使用更多的容器运行时
。
可替代的容器运行时支持是 k8s 中的新概念。在 k8s v1.3 发布时,rktnetes 项目同时发布,让 rkt 容器引擎成为除 Docker 外的又一选择。然而,不管是 Docker 还是 rkt ,都用到了 kubelet 的内部接口,同 kubelet 源码纠缠不清。这种程度的集成需要对 kubelet 的内部机制有非常深入的了解,还会给社区带来管理压力,这就给新生代容器运行时造成了难于跨越的集成壁垒。CRI 接口规范试图用定义清晰的抽象层清除这一壁垒,让开发者能够专注于容器运行时(Container Runtime)本身
。在通向插件式容器支持及建设健康生态环境的路上,这是一小步,也是很重要的一步。
什么是 CRI?
CRI(容器运行时接口)是一个插件接口
,它使 kubelet 能够使用各种容器运行时,无需重新编译集群组件。你需要在集群中的每个节点上都有一个可以正常工作的 容器运行时, 这样 kubelet 能启动 Pod 及其容器。CRI 是
kubelet
和容器运行时(Container Runtime)
之间通信的主要协议。k8s容器运行时接口(CRI)
定义了主要gRPC
协议, 用于集群组件kubelet
和 容器运行时。
CRI 包含 Protocol Buffers、gRPC API、运行库支持及开发中的标准规范和工具
。Docker 的 CRI 实现在 k8s v1.6 中被更新为 Beta 版本,并在 kubelet 启动时默认启动。
什么是 Container Runtime?
Container Runtime 提供容器运行环境,是负责运行容器的软件
。
说明:k8s 自 v1.24 版起,Dockershim 已从 Kubernetes 项目中移除。
你需要在 集群内每个 Node 节点上安装一个 容器运行时
以使 Pod 可以运行在上面。
说明:v1.24 之前的 k8s 版本包括与 Docker Engine 的直接集成,使用名为 dockershim(垫片) 的组件。这种特殊的直接整合不再是 k8s 的一部分 (这次删除被作为 v1.20 发行版本的一部分宣布)。你可以阅读检查 Dockershim 弃用是否会影响你 以了解此删除可能会如何影响你。要了解如何使用 dockershim 进行迁移, 请参阅从 dockershim 迁移。如果你正在运行 v1.24 以外的 Kubernetes 版本,检查该版本的文档。
CRI 主要组件
kubelet 使用 gRPC 框架通过 UNIX Socket 与容器运行时(或CRl代理)进行通信
。在这个过程中 kubelet 是客户端,CRI 代理(shim)是服务端
,如下图所示。
Protocol Buffers API
包含两个 gRPC
服务:ImageService 和 RuntimeService
。
ImageService
提供了从仓库拉取镜像、查看和移除镜像的功能。RuntimeService
负责 Pod 和容器的生命周期管理,以及与容器的交互 (exec/attach/port-forward )。
rkt 和 Docker
这样的容器运行时可以使用一个 Socket
同时提供两个服务,在 kubelet
中可以用 --containcr-runtime-endpoint 和 --image-service-endpoint
参数设置这个 Socket
。
Pod 和 Container 生命周期管理
Pod 由一组应用容器(Container)组成,其中包含共有的环境和资源约束。在 CRI 里,这个环境被称为 PodSandbox(Pod 沙箱)
。k8s 有意为容器运行时留一些发挥空间,他们可以根据自己的内部实现来解释PodSandbox。对于 Hypervisor 类的运行时,PodSandbox 会具体化为一个虚拟机。其他例如 Docker,会是一个 Linux 命名空间(namespace)
。在 v1alphal 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(ListPodSandboxReauest) returns(ListPodSandboxResponse) { }
// 容器操作
rpc CreateContainer(CreateContainerRequest) returns(CreateContainerResponse) { }
rpc StartContainer(StartContainerReauest) returns(StartContainerResponse) { }
rpc StopContainer(StopContainerReauest) returns(StopContainerResponse) { }
rpc RemoveContainer(StopContainerReauest) returns(RemoveContainerResponse) { }
rpc ListContainers(ListContainersReauest) returns(ListContainersResponse) { }
rpc ContainerStatus(ContainerStatusReauest) returns(ContainerStatusResponse) { }
...
}
面向容器级别的设计思想
k8s 中最小的调度单元是 Pod,它曾经可能采用的一个 CRI 设计就是复用 Pod 对象,使得容器运行时可以自行实现控制逻辑和状态转换,极大地简化 API,让CRI 能够更广泛地适用于多种容器运行时。但是经过深入讨论之后,k8s 放弃了这一想法。
首先,kubelet 有很多 Pod 级别的功能和机制(例如 crash-loop backoff机制)。如果交给容器运行时去实现,则会造成很重的负担;
其次,更重要的是,Pod 标准还在快速演进中。很多新功能(如初始化容器,Init Conatiner)都是由 kubelet 完成管理的,无须交给容器运行时实现。
CRI 选择了在容器级别进行实现
,使得容器运行时能够共享这些通用特性,以获得更快的开发速度。这并不意味着设计哲学的改变—— kubelet 要负责、保证容器应用的实际状态和声明状态的一致性。
Kubernetes 为用户提供了与 Pod 及其中的容器进行交互的功能(kubectlexec/attach/port-forward
)。kubelet 目前提供了两种方式来支持这些功能。
调用容器的本地方法。
使用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) { }
...
}
目前还存在一个潜在的问题,kubectl 处理所有的请求连接,使其成为 Node 通信瓶颈的可能
。在设计 CRI 时,要让容器运行时能够跳过中间的过程。容器运行时可以启动一个单独的流式服务来处理请求(还能对 Pod 的资源使用情况进行记录),并将服务地址返回给 kubelet 。这样 kubelet 就能反馈信息给 API Server(kube-apiserver
),使之可以直接连接到容器运行时提供商的服务,并连接到客户端。
尝试使用新的 CRI 来创建容器
尝试新的 Kubelt-CRI-Docker 集成,只需为 kubelet 启动参数加上 --enable-cri=true
开关来启动 CRI。该选项从 k8s v1.6 开始已经作为 kubelet 的默认选项。如果不希望使用 CRI ,可以设置 --enable-cri=false
来关闭该功能。
查看 kubelet 的日志,可以看到启用 CRI 和创建 gRPC Server
的日志:
10603 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:<niI>}]}
ExperimentalQ0SReserved:map[]}
...
10603 15:08:29.060283 3442 kuhelet.go:573] Starting the GRPC server for the docker CRI shim.
创建一个 Deployment:
kubectl run nginx --image=nginx:1.8 --replicas=1
...
deployment "nginx" created
查看 Pod 的详细信息,可以看到将会创建 Sandbox(沙箱)
的 Event:
kubectl describe pod nginx
...
Events:
...From Type Reason Message
...------- -------- ----------- ---------------
...default-scheduler Normal Scheduled Successfuller assigned nginx to k8s-node-1
...kubelet,k8s-node-1 Normal SandboxReceived Pod sandbox received, it will be created.
从 Event 日志信息可以看出 kubelet 使用了 CRI 接口来创建 Container 容器。
常用的 CRI
目前已经有多款开源的 CRI 项目可用于 k8s,常见的容器运行时有以下几种:
containerd(k8s v1.24 版本及以后默认的容器运行时)
CRI-O
Docker Engine(k8s v1.24 版本已经剔除,默认使用 containerd)
Mirantis Container Runtime(MCR)
frakit(基于 Hypervisor 的容器运行时)
关于容器运行时的更多信息,请查看:https://kubernetes.io/zh-cn/docs/setup/production-environment/container-runtimes/