Container Runtime
众所周知,在 k8s 集群中 Node 上负责启动和管理容器的最底层软件是 Container Runtime,其中使用最为广泛的容器运行时是 Docker。但是,k8s 支持的容器运行时不止于 Docker,还支持像 rkt、containerd、kata 等容器运行时。为了提高系统设计的可扩展性和代码的可维护性,k8s 把 kubelet 对容器的操作统一地抽象成一个接口,在 1.5 版本引入了 CRI1。CRI 作为在 Kubelet 和 容器运行时之间的一个中间抽象层,Kubelet 只需要和这个接口打交道,而容器只需要提供一个该接口的实现,给 Kubelet 暴露出 gRPC 服务即可2。
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 也会被移出3。
备注:在 k8s v1.24.0 release 中,dockershim 也终于被完全移除4 。
After its deprecation in v1.20, the dockershim component has been removed from the kubelet. From v1.24 onwards, you will need to either use one of the other supported runtimes (such as containerd or CRI-O) or use cri-dockerd if you are relying on Docker Engine as your container runtime. For more information about ensuring your cluster is ready for this removal, please see this guide.
以 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接口可以分为两组,下面将分别介绍这两部分的接口,这两个服务可以在一个 gRPC server 里面实现,当然也可以分开成两个独立服务。目前社区的很多运行时都是将其在一个 gRPC server 里面实现。
- ImageService:主要是容器镜像相关的操作,比如拉取镜像、删除镜像等。
- RuntimeService:主要是跟容器相关的操作,比如创建、启动、删除Container、Exec等。
RuntimeService
CRI设计的一个重要原则,就是确保这个接口只注容器不关注 Pod,原因是:
- Pod 是 kubernetes 的编排概念,而不是容器运行时的概念,所以不能假设所有下层容器项目,都能够暴露出可以直接映射为 Pod 的API。
- 如果 CRI 中引入了关于 Pod 的概念,那么接下来只要 Pod API 对象的字段发生编号,那么 CRI 就可能需要跟着变更。早期 k8s 开发中,Pod对象的变化比较频繁,对于CRI这样的标准接口来说,这样的变更率有点麻烦。
|
|
容器生命周期
在CRI中有 RunPodSandbox 的接口,其中的 PodSandbox 它并不是 k8s 里 Pod 的API对象,只是抽取了Pod中一部分与容器运行时有关的字段,如 HostName、DnsConfig、CgroupParent等。
|
|
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。
|
|
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
|
|
容器运行时
| 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 对象5,用来支持多容器运行时,比如
- Kata Containers/gVisor + runc
- Windows Process isolation + Hyper-V isolation containers
RuntimeClass 表示一个运行时对象,在使用前需要开启特性开关 RuntimeClass,并创建 RuntimeClass CRD。
然后就可以定义 RuntimeClass 对象
|
|
而在 Pod 中定义使用哪个 RuntimeClass:
|
|
-
No backlinks found.