众所周知,在 k8s 集群中 Node 上负责启动和管理容器的最底层软件是 Container Runtime
,其中使用最为广泛的容器运行时是 Docker。但是,k8s 支持的容器运行时不止于 Docker,还支持像 rkt、containerd、kata 等容器运行时。为了提高系统设计的可扩展性和代码的可维护性,k8s 把 kubelet 对容器的操作统一地抽象成一个接口,在 1.5 版本 引入了 CRI 。CRI 作为在 Kubelet 和 容器运行时之间的一个中间抽象层,Kubelet 只需要和这个接口打交道,而容器只需要提供一个该接口的实现,给 Kubelet 暴露出 gRPC 服务即可。
CRI 架构
CRI Shim
CRI 机制要求 Container Runtime
实现一个 CRI gRPC Server
, 该 gRPC Server
需要监听本地的 Unix socket
,而 Kubelet
则作为 gRPC Client
运行。这即是 CRI Shim
,扮演的是kubelet和容器项目之间的垫片,它的作用是实现CRI规定的每个接口,然后把具体的CRI请求翻译成对后端容器项目的请求或操作 。在当前 k8s 1.20
版本,除了 dockershim 还包含在 kubelet 代码中之外,其他容器运行时的 CRI shim
都需要自己实现并部署在宿主机上。在不久的将来,dockershim
也会被移出。
以 Containerd 为例,在 1.0 及以前版本将 dockershim 和 docker daemon 替换为 cri-containerd + containerd,而在 1.1 版本直接将 cri-containerd 内置在 Containerd 中,简化为一个 CRI 插件。
Containerd 内置的 CRI 插件实现了 Kubelet CRI 接口中的 Image Service
和 Runtime Service
,通过内部接口管理容器和镜像,并通过 CNI 插件给 Pod 配置网络。
根据下面这张图我们再来回顾下 Pod 创建的过程,在调度器将 Pod 调度到某个具体的 Node 之后:
- kubelet 通过
SyncLoop
来判断需要执行的具体操作,如创建一个Pod,那么kubelet调用GenericRuntime
的通用组件来发起创建 Pod 的CRI请求 - 如果使用的是Docker项目,负责响应这个请求的是
dockershim
组件,它把CRI请求里的内容拿出来,组装成Dcoker API
请求发送给Docker Daemon
基于以上设计,SyncLoop
本身就要求这个控制循环是绝对不可以被阻塞的,所以凡是在 kubelet 里有可能会消耗大量时间的操作,如准备 Pod 的Volume,拉取镜像等,SyncLoop
都会开启单独的 Goroutine
来进行操作。
如前所述,CRI接口可以分为两组,下面将分别介绍这两部分的接口:
- ImageService:主要是容器镜像相关的操作,比如拉取镜像、删除镜像等。
- RuntimeService:主要是跟容器相关的操作,比如创建、启动、删除Container、Exec等。
RuntimeService
CRI设计的一个重要原则,就是确保这个接口只注容器不关注 Pod,原因是:
- Pod 是 kubernetes 的编排概念,而不是容器运行时的概念,所以不能假设所有下层容器项目,都能够暴露出可以直接映射为 Pod 的API。
- 如果 CRI 中引入了关于 Pod 的概念,那么接下来只要 Pod API 对象的字段发生编号,那么 CRI 就可能需要跟着变更。早期 k8s 开发中,Pod对象的变化比较频繁,对于CRI这样的标准接口来说,这样的变更率有点麻烦。
1 | type RuntimeService interface { |
容器生命周期
在CRI中有 RunPodSandbox 的接口,其中的 PodSandbox 它并不是 k8s 里 Pod 的API对象,只是抽取了Pod中一部分与容器运行时有关的字段,如HostName
、DnsConfig
、CgroupParent
等。
1 | type PodSandboxManager interface { |
PodSandbox 这个接口其实是kubernetes将Pod这个概念映射到容器运行时层面所需要的字段,是一个Pod对象的子集。作为容器项目,需要自己决定如何使用这些字段来实现 k8s 期望的Pod模型,它的原理如下图所示:
比如执行
kubectl run
创建一个包含 A 和 B 两个容器的叫作 foo 的Pod之后,这个Pod的信息最后来到 kubelet,kubelet会按照图中所示的顺序调用CRI接口。在具体的CRI shim中,这些接口的实现是完全不同的:- 如Docker项目的Dockershim就会创建一个叫作foo的Infra容器(pause容器),用来hold住整个Pod的Network Namespace
- 如基于虚拟化技术的容器 Kata Containers 项目的CRI实现会直接创建出一个轻量级虚拟机来充当Pod
在RunPodSandbox接口的实现中,还需要调用
networkPlugin.SetUpPod
来为整个Sandbox设置网络。这个SetUpPod
方法,实际上就在执行CNI插件里的add
方法,即 CNI 插件为Pod创建网络,并且把Infra容器加入到网络中的操作。- kubelet继续调用
CreateContainer
和StartContainer
接口来创建和启动容器A、B- 对应到dockershim里,就直接启动A、B两个Docker容器。最后宿主机上出现三个Docker容器组成的这个Pod
- 如果是 Kata Container,
CreateContainer
和StartContainer
接口的实现就之后在创建的轻量级虚拟机中创建A、B容器对应的Mount Namespace,最后在宿主机上,只会用一个叫作foo的轻量级虚拟机在运行。
Streaming API
除了上述对容器生命周期的实现之外,CRI shim的另一个重要工作就是实现 exec
和 logs
等接口,这些接口与前面的操作有一个很大的不同,这些gRPC接口调用期间,kubelet 需要跟容器项目维护一个长连接来传输数据,这种API称为Streaming API。
1 | type ContainerManager interface { |
CRI shim
中对 Streaming API
的实现,依赖于一套独立的 Streaming Server
机制,如下图所示:
对一个容器执行kubectl exec 命令的时候:
- 这个请求首先被交给 APIServer
- APIServer 会调用 kubelet 的Exec API
- kubelet 调用 CRI 的Exec接口
- CRI shim 负责响应 kubelet 的这个调用请求,它不会直接去调用后端的容器项目来进行处理,只会返回一个URL(这个URL是该CRI shim对应的Streaming Server的地址和端口)给 kubelet
- kubelet拿到这个URL后,以 Redirect 的方式返回给 APIServer
- APIServer通过重定向来向
Streaming Server
发起真正的/exec
请求,与它建立长连接
此处的
Streaming Server
只需要通过使用 SIG-Node 维护的Streaming API
库来实现,Streaming Server会在 CRI shim 启动时一起启动,一起启动的这一部分如何实现,由CRI shim
自行决定,如 Docker 的dockershim就直接调用Docker的Exec API来作为实现。
ImageService
1 | type ImageManagerService interface { |
容器运行时
CRI 容器运行时 | 维护者 | 主要特性 | 容器引擎 |
---|---|---|---|
Dockershim | Kubernetes | 内置实现、特性最新 | docker |
cri-o | Kubernetes | OCI标准不需要Docker | OCI(runc、kata、gVisor…) |
cri-containerd | Containerd | 基于 containerd 不需要Docker | OCI(runc、kata、gVisor…) |
Frakti | Kubernetes | 虚拟化容器 | hyperd、docker |
rktlet | Kubernetes | 支持rkt | rkt |
PouchContainer | Alibaba | 富容器 | OCI(runc、kata…) |
Virtlet | Mirantis | 虚拟机和QCOW2镜像 | Libvirt(KVM) |
目前基于 CRI 容器引擎已经比较丰富了,包括
- Docker: 核心代码依然保留在 kubelet 内部(pkg/kubelet/dockershim),是最稳定和特性支持最好的运行时
- OCI 容器运行时:
- 社区有两个实现
- Containerd,支持 kubernetes v1.7+
- CRI-O,支持 Kubernetes v1.6+
- 支持的 OCI 容器引擎包括
- runc:OCI 标准容器引擎
- gVisor:谷歌开源的基于用户空间内核的沙箱容器引擎
- Clear Containers:Intel 开源的基于虚拟化的容器引擎
- Kata Containers:基于虚拟化的容器引擎,由 Clear Containers 和 runV 合并而来
- 社区有两个实现
- PouchContainer:阿里巴巴开源的胖容器引擎
- Frakti:支持 Kubernetes v1.6+,提供基于 hypervisor 和 docker 的混合运行时,适用于运行非可信应用,如多租户和 NFV 等场景
- Rktlet:支持 rkt 容器引擎(rknetes 代码已在 v1.10 中弃用)
- Virtlet:Mirantis 开源的虚拟机容器引擎,直接管理 libvirt 虚拟机,镜像须是 qcow2 格式
- Infranetes:直接管理 IaaS 平台虚拟机,如 GCE、AWS 等
RuntimeClass
RuntimeClass 是 v1.12 引入的新 API 对象,用来支持多容器运行时,比如
- Kata Containers/gVisor + runc
- Windows Process isolation + Hyper-V isolation containers
RuntimeClass 表示一个运行时对象,在使用前需要开启特性开关 RuntimeClass
,并创建 RuntimeClass CRD:
1 | kubectl apply -f https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/runtimeclass/runtimeclass_crd.yaml |
然后就可以定义 RuntimeClass 对象
1 | apiVersion: node.k8s.io/v1alpha1 # RuntimeClass is defined in the node.k8s.io API group |
而在 Pod 中定义使用哪个 RuntimeClass:
1 | apiVersion: v1 |