docker、containerd、runc、shim... 容器技术名词全解析

云原生的概念在最近两年得到了广泛的关注,各大云厂商和技术团队都纷纷推出了各种“云原生”的技术和产品。虽然云原生到底包括哪些概念和技术并没有一个公认的答案,但容器技术是云原生的基础和核心应该是现阶段的一个共识。

但是容器技术经过多年的发展和演变,各种实现方案和早期版本相比已经有了巨大的差异,而且仍然在不断演进的过程中。各种组件的产生、随之而来的接口规范、基于接口规范实现的新可选组件让相关的名称和概念更加复杂。新的开发使用者很难全面理解容器相关的各类名词和概念,以及这些名词和概念在真实的容器部署环境中的作用和关系。

笔者作为容器运行时开发人员,对于容器相关的概念仍然经常犯迷糊。docker、libcontainer、containerd、shim、runc分别起什么作用?CRI、OCI是什么层次的规范、有什么区别?各个组件的功能和协作方式是怎样的?使用k8s管理容器后相关组件和结构又有怎样的变化?这些问题经常让笔者困扰。这篇文章将尝试将这些问题做一个全面的回答,尽量完整的解释相关的名词,从而对容器的具体实现和部署有一个全面的认识。为了更清晰的说明容器概念和技术的当前状态,避免混淆,本文将尽量避免对容器技术历史版本的具体概念和架构做详细介绍。

一、docker相关概念和组件

容器技术并不是从docker才开始出现的,docker最主要的三项特性:镜像化、空间隔离和资源隔离,其对应的技术unionfs、namespace和cgroup在docker技术出现前就已经在linux内核中工作了很多年。lxc(linux container)是最早组合使用这些技术来实现容器的,docker的早期版本干脆就是基于lxc实现的namespace和cgroup管理。但docker确实是让容器化部署变得真正可用、易用的关键技术,在docker出现后,容器技术才真正成为一种高效的业务部署模式而被广泛使用。到目前为止,docker(系列技术和组件)仍然是使用最广泛的容器技术和容器领域的事实标准。

经过长期演变,docker从最初的单个整体模块变成了多个通过标准接口协同工作的多个模块。在不同的使用场景下(例如k8s部署场景),使用到的模块会有所不同。这一节首先分析一下使用docker创建运行容器时涉及到的组件和组件间的关系。

上图展示了docker运行容器时的主要组件和组件间的关系。(图片来自https://www.cnblogs.com/sparkdev/p/9129334.html,本文参考了博客作者sparkdev的多篇相关文章)

组件包括:

docker:docker既是整套技术和产品的名称,又是其中一个组件的名称。到今天,docker组件只是一个最外围的入口,为使用者提供一种命令行形式的客户端(CLI)来执行容器的各种操作,使用golang实现。docker客户端将用户输入的命令和参数转换为后端服务的调用参数,通过调用后端服务来实现各类容器操作。

这个组件其实是可替代性最强的组件,有很多的替代性实现,例如各种其他语言的docker客户端和库。这个客户端组件使用docker这个名称是为了和最早期的docker使用习惯保持一致。在docker的早期实现中,所有功能都实现在一个二进制程序docker中,docker既能作为客户端,又能作为服务端,所有操作都是基于docker程序完成的。

dockerd:dockerd是运行于服务器上的后台守护进程(daemon),负责实现容器镜像的拉取和管理以及容器创建、运行等各类操作。dockerd向外提供RESTful API,其他程序(例如docker客户端)可以通过API来调用dockerd的各种功能,实现对容器的操作。但时至今日,在dockerd中实现的容器管理功能也已经不多,主要是镜像下载和管理相关的功能,其他的容器操作能力已经分离到containerd组件中,通过grpc接口来调用。又被称为docker enginedocker daemon

containerd:containerd是另一个后台守护进程,是真正实现容器创建、运行、销毁等各类操作的组件,它也包含了独立于dockerd的镜像下载、上传和管理功能。containerd向外暴露grpc形式的接口来提供容器操作能力。dockerd在启动时会自动启动containerd作为其容器管理工具,当然containerd也可以独立运行。containerd是从docker中分离出来的容器管理相关的核心能力组件,https://www.docker.com/blog/docker-containerd-integration/https://www.docker.com/blog/what-is-containerd-runtime/介绍了将其从docker中独立出来的原因(虽然基于这两篇文章笔者还是没太看懂这个操作在当时的必要性...)。但是为了支持容器功能实现的灵活性和开放性,更底层的容器操作实现(例如cgroup的创建和管理、namespace的创建和使用等)并不是由containerd提供的,而是通过调用另一个组件runc来实现。

runc:runc实现了容器的底层功能,例如创建、运行等。runc通过调用内核接口为容器创建和管理cgroup、namespace等Linux内核功能,来实现容器的核心特性。runc是一个可以直接运行的二进制程序,对外提供的接口就是程序运行时提供的子命令和命令参数。runc内通过调用内置的libcontainer库功能来操作cgroup、namespace等内核特性。

可以看到从docker cli、dockerd、containerd到runc,这些组件都号称提供了容器操作能力,但上层组件提供的可能只是接口封装、状态展现相关的能力,而下层组件则负责更基础、更核心的内核功能调用、底层功能实现。虽然不太容易清楚的区分各层组件提供的具体能力,但将这些层次想象成软件设计中的功能抽象、接口封装和底层实现就能大体理解这些组件间的关系和拆分成多个组件的原因。

containerd-shim:除了这些主要组件外,图中还有containerd-shim这个组件。containerd-shim位于containerd和runc之间,当containerd需要创建运行容器时,它没有直接运行runc,而是运行了shim,再由shim间接的运行runc。据开发者介绍(https://groups.google.com/g/docker-dev/c/zaZFlvIx1_k?pli=1),shim主要有3个用途:

1. 让runc进程可以退出,不需要一直运行。这里有个疑问,为了让runc可以退出所以再启动一个shim,听起来似乎没什么意义。我理解这样设计的原因还是想让runc的功能集中在容器核心功能本身,同时也便于runc的后续升级。shim作为一个简单的中间进程,不太需要升级,其他组件升级时它可以保持运行,从而不影响已运行的容器。

2. 作为容器中进程的父进程,为容器进程维护stdin等管道fd。如果containerd直接作为容器进程的父进程,那么一旦containerd需要升级重启,就会导致管道和tty master fd被关闭,容器进程也会执行异常而退出。

3. 运行容器的退出状态被上报到docker等上层组件,又避免上层组件进程作为容器进程的直接父进程来执行wait4等待。这一条没太理解,可能与shim实现相关,或许是shim有什么别的方式可以上报容器的退出状态从而不需要直接等待它?需要阅读shim的实现代码来确认。

除了上述组件,还有一些相关名称和概念值得一提。

lxc:上文中提到,lxc是最早的linux容器技术,早期版本的docker直接使用lxc来实现容器的底层功能。虽然使用者相对较少,但lxc项目仍在持续开发演进中。

libcontainer:docker从0.9版本开始自行开发了libcontainer模块来作为lxc的替代品实现容器底层特性,并在1.10版本彻底去除了lxc。在1.11版本拆分出runc后,libcontainer也随之成为了runc的核心功能模块。

moby:moby是docker公司发起的开源项目,其中最主要的部分就是同名组件moby,事实上这个moby就是dockerd目前使用的开源项目名称,docker项目中的engine(dockerd)仓库现在就是从moby仓库fork而来的。

docker-ce:docker的开源版本,CE指Community Edition。docker-ce中的组件来自于moby、containerd等其他项目。

docker-ee:docker的收费版本,EE指Enterprise Edition。其基础组件来源和docker-ce是一样的,但附加了一些其他的组件和功能。docker-ee只能在一些企业版操作系统或云计算平台中使用,相关的资料也很少见,应该没有多少使用者。https://medium.com/devops-dudes/2020-differences-between-docker-ce-and-ee-abd10b646597中对其做了一些介绍,并将其称作“docker公司垂死挣扎的挣钱手段”。。。

二、kubernetes引入的相关概念和组件

kubernetes:kubernetes(简写为k8s)是google开源的容器编排、部署、运维系统。k8s的目标是更清晰便捷的使用和管理容器,因此其功能是构建在docker等容器技术之上的。这里我们只讨论k8s与容器技术相关的部分概念,而不对k8s本身的概念和组件做过多探讨。

上图是k8s在单个节点上的容器运行架构。可以看到,上图中除了已经介绍过的docker相关组件dockerd、containerd、containerd-shim、runc之外,还多了kubelet和dockershim两个组件。

kubelet:kubelet是k8s在单机节点上的服务进程,负责为k8s系统管理这台节点上的容器。k8s系统对容器的创建、删除等调度行为都需要通过节点上的kubelet来完成。

dockershim:kubelet并没有直接和dockerd交互,而是通过了一个dockershim的组件间接操作dockerd。dockershim提供了一个标准的接口,让kubelet能够专注于容器调度逻辑本身,而不用去适配dockerd的接口变动。而其他实现了相同标准接口的容器技术也可以被kubelet集成使用,这个接口称作CRI。dockershim和CRI的出现也是容器生态系统演化的历史产物。在k8s最早期的版本中是不存在dockershim的,kubelet直接和dockerd交互。但为了支持更多不同的容器技术(避免完全被docker控制容器技术市场),kubelet在之后的版本开始支持另一种容器技术rkt。这给kubelet的维护工作造成了巨大的挑战,因为两种容器技术没有统一的接口和使用逻辑,kubelet同时支持两种技术的使用还要保证一致的容器功能表现,对代码逻辑和功能可靠性都有很大的影响。为了解决这个问题,k8s提出了一个统一接口CRI,kubelet统一通过这个接口来调用容器功能。但是dockerd并不支持CRI,k8s就自己实现了配套的dockershim将CRI接口调用转换成dockerd接口调用来支持CRI。因此,dockershim并不是docker技术的一部分,而是k8s系统的一部分。

在2020年12月,k8s宣布从其1.20版本开始将默认不再使用dockershim,并将在后续版本中删除dockershim。这也意味着kubelet不再通过dockerd操作容器,docker这个名词不再直接出现在k8s官方生态中。但kubelet仍然基于来自于docker的containerd、runc等组件,因此其底层容器管理逻辑并没有很大的变化。其组件架构变化如下图:

可以看到,在新的架构中,kubelet直接与containerd交互,跳过了dockershim和dockerd这两个步骤。containerd通过其内置的CRI插件提供了CRI兼容接口。

cri-containerd:在k8s和containerd的适配过程中,还曾经出现过cri-containerd这个组件。在containerd1.0版本中,containerd提供了cri-containerd作为独立进程来实现CRI接口,其定位和dockershim类似。但在containerd1.1版本中,就将这个功能改写成了插件形式直接集成到了containerd进程内部,使containerd可以直接支持CRI接口,cri-containerd也就成了历史名词,其repo(https://github.com/containerd/cri)也被合入了containerd,作为其一个内置插件包存在(github.com/containerd/containerd/pkg/cri)。

三、容器运行时相关标准和概念

在上文中已经出现了两种接口标准:CRI和OCI。

这两种接口标准的出现使容器技术的组件化和标准化成为了可能,也随之产生了大量符合接口规范的不同容器实现技术,很大程度上促进了容器技术的发展和市场的繁荣。

CRI:CRI是Container Runtime Interface(容器运行时接口)的缩写。如上文所述,它是k8s团队提出的容器操作接口标准,符合CRI标准的容器模块才能集成到k8s体系中与kubelet交互。符合CRI的容器技术模块包括dockershim(用于兼容dockerd)、rktlet(用于兼容rkt)、containerd(with CRI plugin)、CRI-O等。

rkt与rktlet:rkt是CoreOS公司主导的容器技术,在早期得到了k8s的支持成为k8s集成的两种容器技术之一。随着CRI接口的提出,k8s团队也为rkt提供了rktlet模块用于与rkt交互,rktlet和dockersim的意义基本相同。随着CoreOS被Redhat收购,rkt已经停止了研发,rktlet已停止维护了。

CRI-O:CRI-O是Redhat公司推出的容器技术。从名字就能看出CRI-O的出发点就是一种原生支持CRI接口规范的容器技术。CRI-O同时兼容OCI接口和docker镜像格式。CRI-O的设计目标和特点在于它是一项轻量级的技术,k8s可以通过使用CRI-O来调用不同的底层容器运行时模块,例如runc。

OCI:OCI是Open Container Initiative(开放容器倡议)的缩写。OCI是以docker为首的容器技术公司创建的组织,也是这个组织制定的容器相关标准的统称。OCI标准主要包括两部分:镜像标准和运行时标准。符合OCI运行时标准的容器底层实现模块能够被containerd、CRI-O等容器操作模块集成调用。runc就是从docker中拆分出来捐献给OCI组织的底层实现模块,也是第一个支持OCI标准的模块。除了runc外,还有gVisor(runsc)、kata等其他符合OCI标准的实现。

gVisor:google开源的一种容器底层实现技术,对应的模块名称是runsc。其特点是安全性,runsc中实现了对linux系统调用的模拟实现,从而在用户态实现应用程序需要的内核功能,减小了恶意程序通过内核漏洞逃逸或攻击主机的可能性。

kata:Hyper和Intel合作开源的一种容器底层实现技术。kata通过轻量级虚拟机的方式运行容器,容器内的进程都运行在一个kvm虚拟机中。通过这种方式,kata实现了容器和物理主机间的安全隔离。

容器运行时:最后讨论一下容器运行时的概念。在前文中,笔者尽量避免使用“容器运行时”这个名词,因为这个名词从容器技术出现开始就被用来描述各种容器技术,已经无法作为一个精确的技术名称使用了。docker是容器运行时,dockershim是容器运行时,containerd是容器运行时,runc还是容器运行时。大体上,容器运行时这个名词相当于是“容器技术的某种具体实现”这么一个模糊概念。在CRI和OCI标准的名称中,也都说明自己是容器运行时的标准,但很显然两者约束的对象不是同一层次的。大体上,我们可以把符合CRI接口的这类容器运行时称作高层容器运行时,这类运行时技术提供容器的创建、运行、删除等高层功能接口。而符合OCI接口的运行时则称作底层容器运行时,这类运行时技术真正调用各类内核特性、实现容器在操作系统中的运行和管理。笔者个人认为后者更符合运行时的概念,毕竟真正让容器运行起来的正是这些底层实现技术。

小结

本文总结了笔者所了解的主要容器相关技术概念。可以看到容器技术是一项快速发展中的技术,不同的技术不断出现、演化和消亡。很多技术形态和标准都是历史演化和商业斗争的产物,小型技术创业公司如docker、CoreOS和技术巨头如Google、Redhat在容器生态的建设中起到了不同维度的作用。这篇文章介绍的概念可能很快会过时或不完整,需要持续的跟进容器和云原生技术的发展来保持相关知识的时效性。

  • 14
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值