容器运行时探讨--从dockershim正式从K8s移除说起
背景
2022年05月, Kubernetes 1.24正式发布 ,比较引人注目的就是在这个版本中正式将dockershim 组件从 kubelet 中删除。从这个版本开始,用户使用Kubernetes时需要优先选择containerd 或 CRI-O作为容器运行时。如果希望继续依赖 Docker Engine 作为容器运行时,需要cri-dockerd组件。
其实,早在在一年多前,Kubernetes 1.20版本就宣布对Docker的支持置为废弃(Deprecated)状态,不再演进。 Kubernetes 1.20 变更日志描述如下: "Docker support in the kubelet is now deprecated and will be removed in a future release. " 。Kubernetes弃用Docker,其实也不用过分紧张,在文章 别慌: Kubernetes 和 Docker 中, 作者简介清晰的表达了观点:弃用 Docker 这个底层运行时,转而支持符合为 Kubernetes 创建的容器运行接口 Container Runtime Interface (CRI) 的运行时。 Docker 构建的镜像,将在你的集群的所有运行时中继续工作,一如既往。
那么Kubernetes为什么要移除Docker?主要原因还是Docker长期以来不支持Kubernetes主推的CRI容器运行时标准,Kubernetes需要长期维护着dockershim组件来专门适配Docker。dockershim是出现在Kubernetes发展初期、Docker如日中天阶段的产物,但是随着containerd等容器运行时的发展,Kubernetes也有了足够的理由不再维护dockershim了。
Kubernetes移除dockershim后,是不是就完全不支持Docker了?答案是否定的。目前社区里已经有一个独立于Kubernetes并且支持CRI的shim cri-dockerd (由Mirantis提供,于2019年收购Docker Enterprise部门),用户可以使用该shim实现Kubernetes对Docker对支持。
此外,由于Docker Image已经成为了各类容器运行时使用的标准镜像格式,未来很长一段时间,开发阶段使用Docker,生产环境中使用的Containerd等其他容器运行时,可能会成为一种普遍的现象。
这里我们简要介绍了一下Kubernetes移除dockershim的整个事件。同时,也提到了很多概念(CRI、dockershim、containerd等),为了更好的加深理解,本文将对容器运行时进行深入的探讨。
容器运行时发展史
为了对容器运行时有全面的认识,我们首先回顾下容器运行时的发展史。
2013年前,以Google为主导的容器虚拟化技术,例如Cgroups技术、LXC(Linux Container)、LMCTFY项目。
2013 年,Docker 项目正式发布,凭借着“Build,Ship and Run Any App, Anywhere”的理念迅速席卷天下。
2014 年,Google基于内部的 Borg 系统开源了 Kubernetes项目,用于解决大规模集群的容器部署、运行、管理等问题。随后几年,Kubernetes逐渐成为容器编排领域的标准。
2014 年,CoreOS 基于App Container Specification,实现了容器引擎 Rocket (简称 rkt)试图与 Docker 分庭抗礼。
2015年,Docker发布容器集群管理系统Docker swarm,以及配套的Docker machine、Docker Compose等工具,力图构建完善的容器编排系统,和Kubernetes展开正面竞争。
2015年,为了避免容器技术领域的分裂,Docker联合Linux基金会(Linux Foundation)推动成立了OCI(Open Container Initiative)组织,旨在制定一个容器镜像格式与运行时标准。同时,Docker公司将libcontainer模块捐给CNCF社区,作为OCI标准的实现,这就是我们常说的runc。
2015年,Cloud Native Computing Foundation(CNCF)成立,旨在“构建云原生计算 —— 一种围绕着微服务、容器和应用动态调度的、以基础设施为中心的架构,并促进其广泛使用”。
2016年,为了适应OCI标准,Docker将containerd独立拆分,并捐赠给了社区。这次拆分使得 Docker 将容器的管理功能移出 Docker 的核心引擎,并移入一个单独的守护进程(即 containerd)。
2016年,微软在 Windows Server 上为基于 Windows 的应用添加了容器支持,称之为 Windows Containers。它与 Windows Server 2016 一同发布,可以原生地在 Windows 上运行Docker容器。
2016年,自 Kubernetes 1.5开始,Container Runtime Interface(CRI)发布,通过 CRI 可以支持 kubelet 使用不同的容器运行时,而不需要重新编译。
2017 年,容器生态开始模块化、规范化。CoreOS 和 Docker 联合提议将 rkt 和 containerd 作为新项目纳入 CNCF。OCI 发布 1.0,CRI/CNI 得到广泛支持。
2017 年 - 2019 年,容器引擎技术飞速发展,新技术不断涌现。Kata Containers 社区成立,Google 开源 gVisor 代码,AWS 开源 firecracker,阿里云发布安全沙箱 1.0。
2020年 12月8日 Kubernetes 决定在 v1.20 版本之后将废弃 Docker 作为容器运行时,转而使用为 Kubernetes 创建的 Container Runtime Interface(CRI)运行时,标志着 Docker 的巅峰已经到了。
2020 年 - 202x 年,容器引擎技术升级,Kata Containers 开始 2.0 架构,阿里云发布沙箱容器 2.0....
2022年,Kubernetes 1.24 正式将dockershim 组件从 kubelet 中删除。
回顾过去10年的历史,基本上就是一段围绕着Kubernetes跟Docker的爱恨情仇,中间也催生了OCI跟CRI两个标准。接下来我们将重点介绍OCI跟CRI。
OCI与Docker的演进
提到容器运行时,不得不提到Open Container Initiative(OCI)。
Open Container Initiative(OCI)
OCI是在防止容器技术分裂的背景下提出的,制定容器镜像格式和容器运行时的正式规范(OCI Specifications),其内容主要包括 OCI Runtime Spec (容器运行时规范)、 OCI Image Spec (镜像格式规范)、 OCI Distribution Spec (镜像分发规范)。
其中,我们熟知的 runc 就是Runtime Spec的一种参考实现。最早是从 Docker的 libcontainer中迁移而来的,后由Docker捐献给OCI。Docker容器也是基于runc创建的。
这样容器运行时可以从层级上进行如下划分:
- low-level runtime(即OCI 运行时):指的是仅关注运行容器的容器运行时,调用操作系统,使用 namespace 和 cgroup 实现资源隔离和限制。例如,runc、rkt等。
- high-level runtime:相较于low-level runtimes位于堆栈的上层,负责传输和管理容器镜像,解压镜像,并传递给low-level runtimes来运行容器。例如 containerd、cri-o等。
Docker
从 Docker 1.11 版本(2016年)开始,Docker Daemon拆分成了多个模块以适应 OCI 标准。其中,containerd 独立负责容器运行时和生命周期(如创建、启动、停止、中止、信号处理、删除等),其他一些如镜像构建、卷管理、日志等由 Docker Daemon 的其他模块处理。
上图操作容器流程可以简化为:Docker Daemon-> containerd -> containerd-shim -> runc。其中,containerd-shim是容器直接操作者。
这里之所以引入containerd-shim,是因为在创建和运行容器之后runc会退出,此时将 containerd-shim 作为容器的父进程,而不是 containerd 作为父进程,从而避免containerd挂掉时,整个宿主机上所有的容器都得退出的问题。
Container Runtime Interface(CRI)
什么是CRI
说到Kubernetes剔除dockershim事件,不得不提到CRI。首先我们先了解下CRI出现的背景:在 Kubernetes 早期(v1.5 之前),Docker 作为K8s支持唯一的容器运行时,Kubelet 是通过硬编码的方式直接调用 Docker API的;后来出现了新的容器运行时rkt,也希望整合进了Kubelet代码中。但是后来随着越来越多的容器运行时的出现,继续内嵌的方式显然已经不适合了。在这个背景下,就提出了Container Runtime Interface(CRI )标准,用于将 Kubelet 代码与具体的容器运行时的实现代码解耦。
但是 Kubernetes 推出 CRI 的时候还没有现在的统治地位,各种容器运行时并不会主动去实现 CRI 接口,所以就需要通过CRI shim的方式对各种容器运行时进行适配。例如,Docker就没有打算支持CRI,原因有:Docker出现比Kubernetes早,Docker的地位比较稳固,处于强势的一方;Docker也有意推广Swarm,Swarm被视为K8s的竞品。最终,Kubelet选择了内置dockershim的方式,提供对Docker的支持。
在引入了 CRI 接口之后,Kubelet 的架构。
CRI 定义的 API 主要包括两个 gRPC 服务,ImageService 和 RuntimeService。
- ImageService:负责拉取镜像、查看和删除镜像等。
- RuntimeService:负责管理 Pod 和容器的生命周期,以及与容器交互的调用(exec/attach/port-forward)等。
有了CRI,kubelet可以实现对于容器运行时(例如Docker、containerd、CRI-O等)的统一管理。
Docker支持形态:
- CRI推出初期,Docker江湖地位很高,Kubernetes为了支持Docker,在 kubelet 中内置了 dockershim。
- 流程:kubelet(CRI Client)通过 CRI 接口调用 dockershim(CRI Server);dockershim请求 Docker Daemon去调用containerd,然后通过containerd-shim、runc去真正创建容器。
很显然,Docker的调用链路有点过长,且冗余操作较多。也正是因为这个原因,随着 CRI 的生态越来越完善,最终Kubernetes 社区在2020年7月决定开始着手移除 dockershim 了, 1.24 版本已正式删除dockershim 。
Containerd支持形态:
- containerd 1.0:对 CRI 的适配是通过一个单独的 CRI-Containerd 进程来完成的。独立CRI-Containerd显然是有些多余的,主要原因是,CRI初期还没有绝对的统治力,需要kubelet去适配各容器运行时。
- containerd1.1:去掉了 CRI-Containerd,直接把适配逻辑作为插件集成到了 containerd 主进程中。
综上可见,CRI可以实现kubelet对Docker、containerd、CRI-O的统一管理。同时,Kubernetes 1.24将dockershim 组件从 kubelet 中删除后,也建议用户使用containerd 或 CRI-O。那么接下来我们将重点介绍下containerd跟CRI-O。
containerd
containerd 是一个工业级标准的容器运行时,它强调简单性、健壮性和可移植性。
- 管理容器的生命周期(从创建容器到销毁容器)
- 拉取/推送容器镜像
- 存储管理(管理镜像及容器数据的存储)
- 调用 runc 运行容器(与 runc 等容器运行时交互)
- 管理容器网络接口及网络
containerd 最早是从 Docker 里分离出来,作为一个独立的开源项目,目标是提供一个更加开放、稳定的容器运行基础设施。(详见Docker部分的架构图)
从containerd提供的官方架构图可以看出 containerd 采用的也是 C/S 架构。containerd管理着容器生命周期,从镜像传输和存储到容器执行和监测,再到底层存储到网络附件等等。具体运行容器由 runc 负责,实际上只要是符合 OCI 规范的容器都可以支持。
此外,containerd 还实现了更丰富的容器接口,可以使用 ctr 工具来调用这些丰富的容器运行时接口,而不只是 CRI 接口。例如:
- crictl:一个类似 docker 的命令行工具,用来操作 CRI 接口。
- critest:验证 CRI 接口是否是符合预期。
- 性能工具:测试接口性能。
CRI-O
Kubernetes 社区也做了一个专门用于 Kubernetes 的 CRI 运行时 CRI-O,可以直接兼容 CRI 和 OCI 规范。CRI-O是使用Docker作为Kubernetes运行时的轻量级替代方案,支持任何符合 OCI 标准的容器运行时。kubelet通过CRI直接与CRI-O对话,以提取一个镜像并启动较低级别的运行时(例如runc)。
CRI-O和containerd之间的一个重要区别是删除了一些不必要的Linux功能,以减少外部攻击的可能。
CRI-O是通过直接在 OCI 上包装容器接口来实现的一个 CRI 服务。对外提供的只有具体的 CRI 接口,没有类似containerd一样的ctr工具能力。
整体架构如下:
多容器运行时
随着越来越多的容器运行时的出现,不同的容器运行时也有不同的需求场景,于是就有了多容器运行时的需求,为此Kubernetes社区推出了 RuntimeClass。
目前阿里云 ACK 安全沙箱容器已经支持了多容器运行时。如下图所示,左侧是 runc 的 Pod,对应的 RuntimeClass 是 runc;右侧是runv 的Pod,对应的 RuntimeClass 是 runv。在containerd可以配置多个容器运行时。
- runc:kube-apiserver->kubelet->cri-plugin(cri-plugin 在 containerd 的配置文件中查询 runc 对应的 Handler)-> Shim API runtime v1 -> containerd-shim(它是一个实现了 CRI 的插件)-> 创建runc容器
- runv:kube-apiserver->kubelet->cri-plugin(cri-plugin 在 containerd 的配置文件中查询 runv 对应的 Handler)-> Shim API runtime v2 -> containerd-shim-kata-v2(它是一个实现了 CRI 的插件)-> 创建kata容器
Kubernetes日志采集支持
我们知道日志作为可观测性建设中的重要一环,可以记录详细的访问请求以及错误信息,非常利于问题的定位。Kubernetes上的应用、Kubernetes组件本身、宿主机等都会产生各类日志数据,而且伴随着Kubernetes的发展过程,中间经历了从Docker到CRI的阶段,也提高了日志采集场景的复杂度。
iLogtail作为阿里云 SLS 推出的可观测数据采集的基础设施,承载了阿里巴巴集团、蚂蚁、公有云上的日志、监控、Trace、事件等多种可观测数据的采集工作。在Kubernetes场景下,对Docker、containerd都有很好的支持。iLogtail一般采用Sidecar和DaemonSet两种方式部署模式,
Logtail采集业务Pod/容器的前提是需要能够访问到宿主机上的容器运行时并且可以访问到业务容器的数据。
- 访问容器运行时:Logtail容器会将宿主机上容器运行时(Docker Engine/ContainerD)的sock挂载到容器目录中,因此Logtail容器就可以访问到这台节点的Docker Engine/ContainerD
- 访问业务容器的数据:Logtail容器会将宿主机的根目录('/'目录)挂载到容器的/logtail_host挂载点上,因此可以通过/logtail_host目录访问其他容器的数据(前提是容器运行时中的文件系统以普通文件系统的形式存储在宿主机上,一般overlayfs即可,或者容器的日志目录挂载到宿主机上,例如以hostPath或emptyDir方式挂载)
具体的工作步骤如上图,主要分为两部分:
- 容器发现:
- 从容器运行时(Docker Engine/ContainerD)中获取到所有的容器及其配置信息,例如容器名、ID、挂载点、环境变量、Label等
- 根据采集配置中的IncludeEnv、ExcludeEnv、IncludeLabel、ExcludeLabel定位需要采集的容器。
- 容器数据采集:
- 确定容器被采集的数据地址,包括标准输出和文件的采集地址。
- 标准输出:容器的标准输出最终必须保存成文件才能被采集到,对于DockerEngine和ContainerD都需要配置LogDriver,配置为json-file、local即可(一般默认配置都会保存到文件,所以大部分情况不需要关心)。
- 容器文件:容器文件为的系统为overlay时,会自动根据UpperDir查找到所有容器中的文件;但是ContainerD默认的配置为devicemapper,这种情况下必须把日志挂载到HostPath或者EmptyDir才可以查找到对应的路径。
- 根据对应的地址采集数据,其中对于标准输出比较特殊,需要去解析标准输出文件,从而得到用户实际的标准输出。
- 最后,根据配置的解析规则解析原始的日志,并上传到SLS。
更多详细信息,请参见文章《 Kubernetes日志采集原理全方位剖析 》。目前 iLogtail已开源 ,欢迎加星关注。
总结
本文的主要内容就到此为止了,这里为大家简单总结一下:
- 介绍 OCI及Docker的一些发展演进。
- 介绍了CRI以及符合CRI标准的containerd跟CRI-O。
- Kubernetes日志采集原理。