Kubelet CRI 容器运行时 Containerd

本文深入剖析了Docker的组件构成,重点介绍了containerd和runc。containerd作为Docker的子系统,负责容器生命周期管理和镜像管理,而runc是OCI标准的容器运行时,直接创建和运行容器。此外,文章还探讨了CRI(容器运行时接口)在Kubernetes中的作用,强调了不同容器运行时如containerd、Docker和CRI-O的层级和性能比较。
摘要由CSDN通过智能技术生成

Docker 的组件构成


Docker 整体架构采用 C/S(客户端 / 服务器)模式,主要由客户端和服务端两大部分组成。客户端负责发送操作指令,服务端负责接收和处理指令。客户端和服务端通信有多种方式,即可以在同一台机器上通过UNIX套接字通信,也可以通过网络连接远程通信。

从整体架构可知,Docker 组件大体分为 Docker 相关组件,containerd 相关组件和容器运行时相关组件。下面我们深入剖析下各个组件。 

 containerd 相关的组件


(1)containerd

containerd 组件是从 Docker 1.11 版本正式从 dockerd 中剥离出来的,它的诞生完全遵循 OCI 标准,是容器标准化后的产物。containerd 完全遵循了 OCI 标准,并且是完全社区化运营的,因此被容器界广泛采用。

containerd 不仅负责容器生命周期的管理,同时还负责一些其他的功能:

  • 镜像的管理,例如容器运行前从镜像仓库拉取镜像到本地
  • 接收 dockerd 的请求,通过适当的参数调用 runc 启动容器
  • 管理存储相关资源
  • 管理网络相关资源

containerd 包含一个后台常驻进程,默认的 socket 路径为 /run/containerd/containerd.sock,dockerd 通过 UNIX 套接字向 containerd 发送请求,containerd 接收到请求后负责执行相关的动作并把执行结果返回给 dockerd。

[root@jenkins ~]# ls /run/containerd/containerd.sock
/run/containerd/containerd.sock 

如果你不想使用 dockerd,也可以直接使用 containerd 来管理容器,由于 containerd 更加简单和轻量,生产环境中越来越多的人开始直接使用 containerd 来管理容器。

(2)containerd-shim

containerd-shim 的意思是垫片,类似于拧螺丝时夹在螺丝和螺母之间的垫片。containerd-shim 的主要作用是将 containerd 和真正的容器进程解耦,使用 containerd-shim 作为容器进程的父进程,从而实现重启 containerd 不影响已经启动的容器进程。

(3)ctr

ctr 实际上是 containerd-ctr,它是 containerd 的客户端,主要用来开发和调试,在没有 dockerd 的环境中,ctr 可以充当 docker 客户端的部分角色,直接向 containerd 守护进程发送操作容器的请求。

了解完 containerd 相关的组件,我们来了解一下容器的真正运行时 runc。

容器运行时组件runc 


runc 是一个标准的 OCI 容器运行时的实现,它是一个命令行工具,可以直接用来创建和运行容器。

下面我们通过一个实例来演示一下 runc 的神奇之处。

第一步,准备容器运行时文件:进入 /home/centos 目录下,创建 runc 文件夹,并导入 busybox 镜像文件。

$ cd /home/centos
 ## 创建 runc 运行根目录
 $ mkdir runc
 ## 导入 rootfs 镜像文件
 $ mkdir rootfs && docker export $(docker create busybox) | tar -C rootfs -xvf -

第二步,生成 runc config 文件。我们可以使用 runc spec 命令根据文件系统生成对应的 config.json 文件。命令如下:

$ runc spec

此时会在当前目录下生成 config.json 文件,我们可以使用 cat 命令查看一下 config.json 的内容:

$ cat config.json
{
	"ociVersion": "1.0.1-dev",
	"process": {
		"terminal": true,
		"user": {
			"uid": 0,
			"gid": 0
		},
		"args": [
			"sh"
		],
		"env": [
			"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
			"TERM=xterm"
		],
		"cwd": "/",
		"capabilities": {
			"bounding": [
				"CAP_AUDIT_WRITE",
				"CAP_KILL",
				"CAP_NET_BIND_SERVICE"
			],
			"effective": [
				"CAP_AUDIT_WRITE",
				"CAP_KILL",
				"CAP_NET_BIND_SERVICE"
			],
			"inheritable": [
				"CAP_AUDIT_WRITE",
				"CAP_KILL",
				"CAP_NET_BIND_SERVICE"
			],
			"permitted": [
				"CAP_AUDIT_WRITE",
				"CAP_KILL",
				"CAP_NET_BIND_SERVICE"
			],
			"ambient": [
				"CAP_AUDIT_WRITE",
				"CAP_KILL",
				"CAP_NET_BIND_SERVICE"
			]
		},
		"rlimits": [
			{
				"type": "RLIMIT_NOFILE",
				"hard": 1024,
				"soft": 1024
			}
		],
		"noNewPrivileges": true
	},
	"root": {
		"path": "rootfs",
		"readonly": true
	},
	"hostname": "runc",
	"mounts": [
		{
			"destination": "/proc",
			"type": "proc",
			"source": "proc"
		},
		{
			"destination": "/dev",
			"type": "tmpfs",
			"source": "tmpfs",
			"options": [
				"nosuid",
				"strictatime",
				"mode=755",
				"size=65536k"
			]
		},
		{
			"destination": "/dev/pts",
			"type": "devpts",
			"source": "devpts",
			"options": [
				"nosuid",
				"noexec",
				"newinstance",
				"ptmxmode=0666",
				"mode=0620",
				"gid=5"
			]
		},
		{
			"destination": "/dev/shm",
			"type": "tmpfs",
			"source": "shm",
			"options": [
				"nosuid",
				"noexec",
				"nodev",
				"mode=1777",
				"size=65536k"
			]
		},
		{
			"destination": "/dev/mqueue",
			"type": "mqueue",
			"source": "mqueue",
			"options": [
				"nosuid",
				"noexec",
				"nodev"
			]
		},
		{
			"destination": "/sys",
			"type": "sysfs",
			"source": "sysfs",
			"options": [
				"nosuid",
				"noexec",
				"nodev",
				"ro"
			]
		},
		{
			"destination": "/sys/fs/cgroup",
			"type": "cgroup",
			"source": "cgroup",
			"options": [
				"nosuid",
				"noexec",
				"nodev",
				"relatime",
				"ro"
			]
		}
	],
	"linux": {
		"resources": {
			"devices": [
				{
					"allow": false,
					"access": "rwm"
				}
			]
		},
		"namespaces": [
			{
				"type": "pid"
			},
			{
				"type": "network"
			},
			{
				"type": "ipc"
			},
			{
				"type": "uts"
			},
			{
				"type": "mount"
			}
		],
		"maskedPaths": [
			"/proc/acpi",
			"/proc/asound",
			"/proc/kcore",
			"/proc/keys",
			"/proc/latency_stats",
			"/proc/timer_list",
			"/proc/timer_stats",
			"/proc/sched_debug",
			"/sys/firmware",
			"/proc/scsi"
		],
		"readonlyPaths": [
			"/proc/bus",
			"/proc/fs",
			"/proc/irq",
			"/proc/sys",
			"/proc/sysrq-trigger"
		]
	}
}

config.json 文件定义了 runc 启动容器时的一些配置,如根目录的路径,文件挂载路径等配置。
第三步,使用 runc 启动容器。我们可以使用 runc run 命令直接启动 busybox 容器。

此时,我们已经创建并启动了一个 busybox 容器。

我们新打开一个命令行窗口,可以使用 run list 命令看到刚才启动的容器。

$ cd /home/centos/runc/
$ runc list
D          PID         STATUS      BUNDLE              CREATED                          OWNER
busybox     9778        running     /home/centos/runc   2020-09-06T09:25:32.441957273Z   root

通过上面的输出,我们可以看到,当前已经有一个 busybox 容器处于运行状态。

总体来说,Docker 的组件虽然很多,但每个组件都有自己清晰的工作职责,Docker 相关的组件负责发送和接受 Docker 请求,contianerd 相关的组件负责管理容器的生命周期,而 runc 负责真正意义上创建和启动容器。这些组件相互配合,才使得 Docker 顺利完成了容器的管理工作。

总结


到此,相信你已经完全掌握了 Docker 的组件构成,各个组件的作用和工作原理。本节课时的重点我帮你总结如下。

CRI


kubelet在启动容器进程的时候,要真正的启动这些容器进程,随着版本的迭代,它逐渐将启动过程当中这些标准的行为,抽象为一个一个的接口。

它的好处是kubelet就不需要和具体的runtime产生绑定关系了,kubelet自己是一些代码框架,它定义好这些接口,它只需要调用这些接口,由不同的容器运行时提供商来实现这些接口。(让运行时可以适配我的标准,这样可以使用你也可以不使用你)

这样我们就可以选择是使用docker还是containerd呢还是kata其他类型的容器?

这样就使得kubernetes就不和某一个运行时有具体的强绑定关系,防止被某个厂商锁死。

容器运行时(Container Runtime),运行于Kubernetes(K8s)集群的每个节点中,负责容器的整个生命周期。其中Docker是目前应用最广的。随着容器云的发展,越来越多的容器运行时涌现。为了解决这些容器运行时和Kubernetes的集成问题,在Kubernetes1.5版本中,社区推出了CRI(Container Runtime Interface,容器运行时接口)以支持更多的容器运行时。

CRI就是,它将容器运行时的这些实现标准的接口都抽象出来了,kubelet要去操作任何的容器进程都是通过CRI的接口,然后不同的容器运行时会实现这些CRI接口,最终去操作这些容器进程。

CRI是Kubernetes定义的一组gRPC服务。kubelet作为客户端,基于gRPC框架,通过Socket和容器运行时通信。

它包括两类服务∶镜像服务(Image Service)和运行时服务(Runtime Service)。

  • 镜像服务提供下载、检查和删除镜像的远程程序调用。
  • 运行时服务包含用于管理容器生命周期,以及与容器交互的调用(exec/attach/port-forward)的远程程序调用。

kubelet是客户端,可以看到在kubelet里面有grpc client,它会去基于grpc框架去调用运行时服务,类似docker containerd这类的服务。它是运行在kubelet之外的,在安装kubernetes的时候,会先去安装docker / containerd,安装完之后再去安装kubelet。

这样的话kubelet作为客户端来调用runtime的接口。这些runtime就要按照CRI的规范去实现那些接口。

容器运行时最终的目的是要去启动一个一个的容器进程,所以你可以理解为容器运行时它本身是一个中间层,它向上面对的是kubelet,向下面对的是这些容器进程。

所以它分为high-level-runtime和low-level-runtime,high-level-runtime是对外提供的这些grpc服务。由客户端grpc client来调用这些服务,它接受到这些请求需要通过low-level的api去启动和操作这些容器。

它有点像一层代理,它只是做命令的转发,接受来自客户端的请求,然后再去操作下面的容器进程。它所谓的low-level-runtime,容器的话就是runc。

运行时的层级


Dockershim, containerd和CRI-O都是遵循CRI的容器运行时, 我们称他们为高层级运行时(High-level Runtime)。

容器运行时最终遵循了oci的一个标准,oci定义了容器相关的行业标准。这个行业标准主要分为了三大类,定义了镜像如何打包,如何解压的这样一个规范。通过镜像如何去运行容器进程这样的一个规范。

OCI(Open Container Initiative,开放容器计划)定义了创建容器的格式和运行时的开源行业标准,包括镜像规范(Image Specification)和运行时规范(Runtime Specification)。

镜像规范定义了OCI镜像的标准。高层级运行时将会下载一个OCI镜像,并把它解压成 OCI运行时文件系统包(filesystem bundle)(还有存储镜像文件到指定的目录下)。

运行时规范则描述了如何从OCI运行时文件系统包运行容器程序,并且定义它的配置、运行环境和生命周期。如何为新容器设置命名空间(namepsaces)和控制组(cgroups),以及挂载根文件系统等等操作,都是在这里定义的。它的一个参考实现是runC。我们称其为低层级运行时(Low-level R untime)。除runC以外,也有很多其他的运行时遵循OCI标准,例如kata-runtime。

CRI


containerd是能够取代docker的,因为docker本身内嵌了containerd的,docker是一个独立的产品,我们可以通过docker的命令去管理自己的网络,管理自己的存储,所以docker大部分臃肿的逻辑都在docker daemon它自身的逻辑上面,这些逻辑处理完之后也是将请求发给自带内嵌的containerd里面去, 这样其实真正执行的逻辑是在containerd里面。

存储,网络是kubernetes和docker之间的竞争,在kubernetes里面用的都是kubernetes管理的这套体系,那么docker的那部分其实是冗余的。不再使用docker自身的daemon了,但是可以用你docker自身所带的containerd的。

对于CRI来说主要的就是runtimeservice和imageservice。imageservice提供了很多接口,主要是对image相关的操作。

runtimeservice同样通过了很多接口,可以看到对sandbox有很多操作,以及和对用户容器相关的。

开源运行时比较


上面是三个常用的运行时,最上面的路径其实是最长的。

如果是docker运行时,那么是kubelet调用dockeshim,调用docker再去调用containerd,containerd再通过runc的接口去调用底层的容器进程。

如果是containerd那么就没有docker-shim和docker,直接由kubelet发起命令到containerd,然后containerd到runc。

如果是crio那么更加轻量级,kubelet调用crio,然后直接调用runc。

越来越简洁。

Docker和Containerd的细节差异


如果是通过kubelet去调用docker的话,这个请求是在kubelet的cri接口调用docker-shim,docker-shim再连接到docker-daemon,docker-daemon里面红圈的这部分才是有用的,也就是和image相关的操作,以及正真去去调用containerd的这部分操作是有用的,上面存储网络,已经docker提供的cli这些都是不需要的,那么这些都是冗余的。

如果将docker换掉的话,那么红线外的部分就没有了,那么复杂性就大大降低的,直接使用cri的接口调用containerd,移除了这些不必要的调用转发环节,这样整个系统架构更加简单。

上面就是理解去掉docker的意义,这是一个必要的动作。

多种运行时性能比较


在选型的时候一个是架构的简单程度,稳定性,还有它的性能。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值