一、k8s、docker与containerd
Docker兴起的时候,推动容器化标准,发起了开放容器倡议(Open Container Initiaiv)即OCI;OCI主要包含两个规范,一个是容器运行时规范(runtime-spec),一个是容器镜像规范(image-spec)。后来,docker在各大公司的忽悠以及自身的权衡下,将runc捐赠给OCI、以及将containerd捐赠给了CNCF。
runc是一个轻量级的命令行工具,参照OCI Runtime标准实现,可用于运行容器;containerd是一个开放、可靠的容器运行时;使用垫片containerd-shim支持多种OCI Runtime实现;例如在containerd中使用runc,就需要使用containerd-shim-runc。
Docker容器启动过程如下,它的架构为 C/S/containerd/runc。
graph LR
docker-cli--> dockerd --gRPC--> containerd --exec--> containerd-shim-runc--exec--> runc
早期k8s是通过自定义客户端访问docker暴露的API:
graph LR
k8s--> dockerd-manager--> docker-->containerd-->container
k8s为了兼容其它容器引擎,定义了容器运行时接口(CRI);通过CRI接口调用 docker-sim后再调用Docker API。
graph LR
k8s--CRI--> docker-sim--> docker-->containerd-->container
docker-sim--CRI-->k8s
此时containerd已经被捐赠了,而k8s多加了一个CRI接口用于兼容其它容器引擎,看似麻烦其实另有深意。在时机成熟后,k8s基于CRI开发了cri-containerd;cri-containerd是守护进程可以绕过docker,用来实现k8s和containerd之间的交互。
graph LR
k8s--CRI-->CRI-Containerd-->containerd-->container
CRI-Containerd--CRI-->k8s
这时候containerd还作为一个独立的组件被k8s间接调用,但是已经不需要依赖docker了。后来为了更高效的调用,变成了Containerd的CRI组件。
graph LR
k8s--CRI-->containerd-->component-CRI-Containerd-->container
二、Containerd
Containerd 已经变成一个工业级的容器运行时,能作为Linux与Window系统的守护进程,可用于:
- 管理主机系统的完整生命周期
- 镜像的传输与存储
- 容器的执行(containerd-shim)
- 监控底层存储、网络组件及其它更底层的组件
主机系统(host system):是为其他系统或用户提供服务的任何联网计算机。这些服务可能包括但不限于打印机、网络或数据库访问。
特点如下:
- 支持OCI镜像规范;
- 支持OCI运行时规范(又名
runC) - 支持镜像的推送与拉取
- 支持容器运行时与生命周期
- 提供用于创建、修改、删除接口的
网络服务原语 - 管理容器网络的
命名空间,用于加入已存在的命名空间 - 通过 CAS存储支撑全局镜像的多租户模式
网络服务原语(Network primitives),包含请求、指示、响应、证实。
请求(request)指一个实体希望得到完成某些操作的服务;指示(indication)指通知一个实体,有某个事件发生;响应(response)指一个实体希望响应一个事件;证实(confirm)指返回对先前请求的响应。而服务是各层向它上层提供的一组原语,定义了两层之间的接口;协议是同层对等实体之间交换数据帧、分组和报文的格式及意义等信息的一组规则。
名字空间(英语:Namespace),也称命名空间、名称空间等,它表示着一个标识符(identifier)的可见范围。一个标识符可在多个名字空间中定义,它在不同名字空间中的含义是互不相干的。这样,在一个新的名字空间中可定义任何标识符,它们不会与任何已有的标识符发生冲突,因为已有的定义都处于其他名字空间中。
CAS存储:内容寻址存储,也称为内容寻址存储或缩写CAS,是一种存储信息的方式,因此可以根据其内容而不是位置来检索信息。
三、架构体系
containerd的架构如下:
三层架构
containerd采用经典的 C/S架构,整个分为三层:
Backend在整个架构的最底层,负责与系统做交互;Core层是containerd的核心层,主要包含Service与Metadata两块;API层通过GRPC协议为客户端提供API。
组件、子系统与模块
为了确保业务相互独立,将containerd功能单元划分为独立的组件(component),而组件被粗略地组织成子系统(subsystem),而桥接子系统的组件可以称为模块(module)。模块通常作为不同子系统的横切面提供统一服务,例如持久存储或事件分发等;理解这些组件及其关系是修改和扩展系统的关键。
一般每个子系统都有一个或多个控制器相关的组件用于实现子系统的功能,并作为服务暴露给外部调用。除了子系统外,还存在一些跨子系统边界的组件,例如:
- Executor(执行器): ;实现实际的容器运行时
- Supervisor(监控器): 监视并报告容器状态
- Metadata(元数据): 在图数据库中存储任何与images和bundles相关的持久引用
- Content:提供对镜像中可寻址内容的访问,存储不可变内容
- Snapshot: 管理文件系统上容器镜像的快照,对标Docker中的 graphdriver。镜像的层被分解成快照
- Events: 支持事件的收集和使用,提供一致的事件驱动的行为和审计,事件可以重置到各个模块中
- Metrics: 暴露组件监控指标,并通过Metrics API访问
此外,有些组件是在客户端实现的,以提高灵活性。
Bundle与Runtime
【下图来自 containerd/Architecture,用于理解设计,但不代表实际的架构】
该架构体系的主要目标是协调Bundle子系统的创建与执行,主要包括以下两部分:
- Bundle包含了配置、元数据以及文件系统根目录的数据,包含了
运行容器所需要的所有信息;在文件系统中只是一个目录而已,允许用户从磁盘镜像中提取和打包bundles。 - Runtimes:表示所有支持Runtime规范的程序;可以通过Bundle创建运行时容器。
综上,bundle是containerd的核心概念,下图中镜像的数据流表示了bundle的创建流程:
大致流程如下
-
使用Distribution控制器拉取镜像,然后将镜像的数据存储到Content组件中,将镜像名以及root manifest pointers存储到Metadata组件中;
-
镜像拉取成功后,用户可以指示bundle控制器将镜像解压到bundle组件中;
-
从 Content组件 中消费后,镜像中的层被解压缩到Snapshot组件中。
-
当容器的rootfs快照准备好时,Bundle控制器可以使用镜像的manifest文件和配置来准备执行配置。其中一部分是将挂载从snapshot模块输入到执行配置中。
-
将准备好后的Bundle传递给runtime组件,它会读取Bundle的配置来创建一个运行时容器
manifest是一个文件,这个文件包含了有关于镜像信息,如层、大小和摘要