容器运行时概览

要把进程运行在容器中,需要有便捷的SDK或命令来调用Linux的系统功能,从而创建出容器。容器的运行时(runtime)就是运行和管理容器进程、镜像的工具。

容器运行时分类

根据容器运行时提供的功能,可以把容器运行时分为低层运行时高层运行时

低层运行时

低层运行时主要负责与宿主机操作系统打交道,根据指定的容器镜像在宿主机上运行容器的进程,并对容器的整个生命周期进行管理。 而这个低层运行时,正是负责执行容器平台配置过的设置容器 Namespace、Cgroups等基础操作的组件。

常见的低层运行时种类有:

  • runc:传统的运行时,基于 Linux Namespace 和 Cgroups 技术实现,代表实现是Docker

  • runv:基于虚拟机管理程序的运行时,通过虚拟化 guest kernel,将容器和主机隔离开来,使得边界更加清晰,代表实现是Kata Container 和 Firecracker

  • runsc:runc + safety,通过拦截应用程序的所有系统调用,提供安全隔离的轻量级容器运行时沙箱,代表实现是谷歌的 gVisor

高层运行时

高层运行时主要负责镜像的管理、转化等工作,为容器的运行做前提准备。主流的高层运行时主要是 Containerd 和 CRI-O。

高层运行时与底层运行时各司其职,容器运行时一般先由高层运行时将容器镜像下载下来,并解压转换为容器运行需要的操作系统文件,再又底层运行时启动和管理容器。

二者关系如下图: file

Kubernetes 容器运行时

Kubernetes 早期是利用 Docker 作为容器运行时管理工具的,在 1.6 版本之前 Kubernetes 将 Docker 默认为自己的运行时工具,通过直接调用 Docker 的API 来创建和管理容器。在 Docker 项目盛行不就,CoreOS 推出了 rkt 运行时工具,Kubernetes 又添加了对 rkt 的支持。但随着容器技术的不断发展,越来越多的运行工具,提供对所有运行时工具的支持,显然是不现实的;而且直接将运行时的集成内置于 Kubernetes,两者紧密结合,对 Kubernetes 代码本身也是一种负担。

为了解决这个局面,Kubernetes 将对容器的操作抽象为一个接口,将接口作为 kubelet 与运行时工具之间的桥梁,kubelet 通过发送接口请求对容器进行启动和管理,各个容器工具通过实现这个接口即可接入 Kubernetes。 这个统一的容器操作接口,就是容器运行时接口(Container Runtime Interface, CRI)

file

上图可以看到,CRI 主要有gRPC client、gRPC Server 和具体的容器运行时工具 三个组件。其中 kubelet 作为gRPC 的客户端来调用 CRI 接口;CRI shim 作为 gRPC 服务端来相应 CRI 请求,负责将 CRI 请求的内容转换为具体的容器运行时 API,在 kubelet 和运行时之间充当翻译的角色。

具体的容器创建逻辑是,Kubernetes 在通过调度一个具体的节点运行 Pod,该节点的 Kubelet 在接到 Pod 创建请求后,调用一个叫作 GenericRuntime 的通用组件来发起创建 Pod 的 CRI 请求给 CRI shim;CRI shim 监听一个端口来响应 Kubelet,在收到 CRI 请求后,将其转化为具体的容器运行时指令,并调用相应的容器运行时来创建 Pod。

因此,任何容器运行时如果想接入 Kubernetes,都需要实现一个自己的 CRI shim,来实现 CRI 接口规范。查看 Kubernetes 代码可以发现,它定义了下图所示两类接口:RuntimeServiceImageServiceRuntimeService 定义了跟容器相关操作,如创建、启动、删除容器等。ImageService 主要定义了容器相关的操作,如拉取镜像、删除镜像等。

file

ImageService 的操作比较简单,就是拉取、删除、查看镜像状态及获取镜像列表这几个操作。

上图可以看出来,RuntimeService 除了有 container 的管理接口外,还包含 PodSandbox 相关的管理接口和 exec、attach 等与容器交互的接口。

顾名思义,PodSandbox 这个概念对应的是 Kubernetes 的 Pod,它描述了 Kubernetes里的Pod 与容器运行相关的属性或者信息,如HostName、CgroupParent等。设计这个的初衷是因为 Pod 里所有容器的资源和环境信息是共享的,但是不同的容器运行时实现共享的机制不同,如Docker 中Pod 会是一个 Linux 命名空间,各容器网络信息的共享通过创建 pause 容器的方法来实现,而Kata Container 则直接将 Pod 具化为一个轻量级的虚拟机。将这个逻辑抽象为PodSandbox 接口,可以让不同的容器运行时在 Pod 实现上自由发挥。

Exec、Attach 和 PortForward 是三个和容器进行数据交互的接口,由于交互数据需要长连接来传输,所以把这些接口称作 Streaming API。CRI shim 依赖一套独立的 Streaming Server 机制来实现客户端与容器的交互需求。长连接比较消耗网络资源,为了避免因长连接给 Kubelet 节点带来网路流量瓶颈,CRI 要求容器运行时启动一个对应请求的单独的流量服务器,让客户端直接与流服务器进行连通交互。

file

kubectl exec 命令实现过程如下:

  1. 客户端发送 kubectl exec 命令给 apiserver

  2. apiserver 调用 kubelet 的 Exec API

  3. kubelet 调用 CRI 的 Exec 接口(具体的执行者为实现该接口的 CRI Shim)

  4. CRI Shim 向 kubelet 返回 Streaming Server 的地址和端口

  5. kubelet 以 redirect 的方式返回给 apiserver

  6. apiserver 通过重定向来向 Stream Server 发起真正 /exec 请求,与它建立长连接,完成Exec 的请求和响应。

$ kubectl get  pod -A --v=9
I1108 17:04:48.527247  919808 loader.go:372] Config loaded from file:  /home/qiqios/.kube/config
I1108 17:04:48.550542  919808 round_trippers.go:466] curl -v -XGET  -H "Accept: application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json" -H "User-Agent: kubectl/v1.24.0 (linux/amd64) kubernetes/4ce5a89" 'https://192.168.3.20:6443/api/v1/pods?limit=500'
I1108 17:04:48.551882  919808 round_trippers.go:510] HTTP Trace: Dial to tcp:192.168.3.20:6443 succeed
I1108 17:04:48.579624  919808 round_trippers.go:553] GET https://192.168.3.20:6443/api/v1/pods?limit=500 200 OK in 28 milliseconds
I1108 17:04:48.579747  919808 round_trippers.go:570] HTTP Statistics: DNSLookup 0 ms Dial 0 ms TLSHandshake 13 ms ServerProcessing 9 ms Duration 28 ms
I1108 17:04:48.579768  919808 round_trippers.go:577] Response Headers:
I1108 17:04:48.579825  919808 round_trippers.go:580]     X-Kubernetes-Pf-Flowschema-Uid: 9c0fe9f2-70cc-42f2-bb65-24cb8f13900b
I1108 17:04:48.579962  919808 round_trippers.go:580]     X-Kubernetes-Pf-Prioritylevel-Uid: 7ad95437-5fc2-447d-943d-47fc9110056c
I1108 17:04:48.580586  919808 round_trippers.go:580]     Date: Tue, 08 Nov 2022 09:04:48 GMT
I1108 17:04:48.580704  919808 round_trippers.go:580]     Audit-Id: 247acdbd-22f1-4985-a097-5b4a3c8ce4c9
I1108 17:04:48.580733  919808 round_trippers.go:580]     Cache-Control: no-cache, private
I1108 17:04:48.580784  919808 round_trippers.go:580]     Content-Type: application/json

kuelet 在引入 CRI 之后,主要架构如下图所示。其中 Generic Runtime Manager 负责发送容器创建、删除等 CRI 请求,Container Runtime Interface(CRI) 负责定义 CRI 接口规范,具体的 CRI 实现可分为两种:kubelet 内置的 dockershim 和远端的 CRI shim。

其中dockershim 是 Kubernetes 自己实现的适配 Docker 接口的 CRI 接口实现,主要用来将 CRI 请求里的内容组装成 Docker API 请求发给 Docker Daemon;

远端的 CRI shim 主要是用来匹配其他的容器运行时工具到 kubelet。CRI shim 主要负责响应 kubelet 发送的 CRI 请求,并将请求转化为具体的运行时命令发送给具体的运行时(如runc、kata等);Stream Server 用来响应客户端与容器的交互,除此之外,CRI 还提供接入CNI 的能力以实现 Pod 网络的共享。常用的远端 CRI 的实现有 CRI-Containerd、CRI-O等。

file

从上图可以看出,Kubernetes 把 docker shim 内置了官方的代码库中,将 Docker 设计为 Kubernetes 默认的容器运行时工具。在 Kubernetes 1.24 版本中,dockershim 已经废弃,替换为 containerd 作为其默认运行时。

Kubernetes 抛弃 Docker 转而使用 containerd 的缘由

Docker 最初是一个单体引擎,主要负责容器镜像的制作、上传、拉取及容器的运行及管理。随着容器技术的发展,为了促进容器技术相关的规范生成和 Docker 自身项目的发展,Docker 将单体引擎拆分为三部分,分别为 runC、containerd 和 dockerd,其中 runC 主要负责容器的运行和生命周期的管理(即前述的低层运行时)、containerd 主要负责容器镜像的下载和解压等镜像管理功能 (即前述的高层运行时)、dockerd 主要负责提供镜像制作、上传等功能同时提供容器存储和网络的映射功能,同时也是Docker 服务器端的守护进程,用来响应 Docker 客户端(命令行 CLI 工具)发来的各种容器、镜像管理的任务。

Docker 公司将 runC 捐献给了 OCI,将 containerd 捐献给了 CNCF,剩下的dockerd 作为 Docker 运行时由 Docker 公司自己维护。 file

如前所述,Kubernetes 在引入 CRI 之后,kubelet 需要通过 CRI shim 去调用具体的容器运行时工具,由于早期 Kubernetes 对 Docker 的支持是内置的,因此官方自己实现了 dockershim,通过 dockershim 去访问 dockerd。 file

官方废弃对Docker 支持后,使用 containerd 为默认运行时,kubelet 需要一个 CRI shim作为中间件去调用运行时,那 kubelet 在抛弃了 dockershim 之后又是怎么访问 containerd 呢,答案是 containerd 自己集成了 CRI shim,提供了一个 CRI 插件来实现 shim 的功能,这样 kubelet 就可以直接访问 containerd。 file

由上图可以看到,废弃 dockershim 之前的 kubelet 其实也是使用 containerd 作为高层运行时,只是中间通过了 dockershim 和 dockerd 两步转发。

在讲 dockershim 移除之后,kubelet 越过 docker 门户直接访问了 containerd ,这明显的轻量化了调用过程 ,大大加快了kubelet调用运行时的速度。

参考文献:

https://mp.weixin.qq.com/s/QNdWjb5roamFTsdIMsuepQ


容器运行时概览
http://www.qiqios.cn/2022/11/08/容器运行时概览/
作者
一亩三分地
发布于
2022年11月8日
许可协议