咱们都知道,容器技术其实在好久之前就已经出现,但只是在最近十年因为云计算的发展才逐渐进入大众的视野。对于容器运行时,传统意义上来讲就是表明容器从拉取镜像到启动运行再到停止的整个生命周期,较相似于 Java 中的 Java hotspot 运行时。在本文中我会介绍容器运行时相关概念及组件原理,梳理下咱们常听到的 OCI、runc、containerd 等名词之间的关系。html
容器运行时顾名思义就是要掌控容器运行的整个生命周期,以 docker 为例,其做为一个总体的系统,主要提供的功能以下:linux
docker build
docker images
docker ps
docker run
docker pull/push
然而这些功能都可由小的组件单独实现,且没有相互依赖。然后 Docker 公司与 CoreOS 和 Google 共同建立了 OCI (Open Container Initial),并提供了两种规范:git
filesystem bundle
filesystem bundle(文件系统束): 定义了一种将容器编码为文件系统束的格式,即以某种方式组织的一组文件,并包含全部符合要求的运行时对其执行全部标准操做的必要数据和元数据,即config.json 与 根文件系统。github
然后,Docker、Google等开源了用于运行容器的工具和库 runc,做为 OCI 的一种实现参考。在此以后,各类运行时工具和库也慢慢出现,例如 rkt、containerd、cri-o 等,然而这些工具所拥有的功能却不尽相同,有的只有运行容器(runc、lxc),而有的除此以外也能够对镜像进行管理(containerd、cri-o)。目前较为流行的说法是将容器运行时分红了 low-level 和 high-level 两类。docker
low-level: 指的是仅关注运行容器的容器运行时,调用操做系统,使用 namespace 和 cgroup 实现资源隔离和限制。high-level: 指包含了更多上层功能,例如 grpc调用,镜像存储管理等。json
不一样工具的关系以下图:安全
low-level runtime 关注如何与操做系统交互,建立并运行容器。目前常见的 low-level runtime有:架构
容器在 linux 中使用 namesapce 实现资源隔离,使用 cgroup 实现资源限制,这部分在k8s基础--容器篇中对原理详细介绍,此处不作赘述。这里咱们详细介绍下如何建立一个简单的 runtime。函数
咱们以 busybox 镜像做为运行时的一个根文件系统,首先建立一个临时目录并将 busybox 中的全部文件解压缩到目录中工具
CID=$(docker create busybox)
ROOTFS=$(mktemp -d)
docker export $CID | tar -xf - -C $ROOTFS复制代码
限制咱们须要建立 cgroup 对内存和cpu进行限制
UUID=$(uuidgen)
cgcreate -g cpu,memory:$UUID
# 内存限制设置为 100MB
cgset -r memory.limit_in_bytes=100000000 $UUID
# cpu 限制设置为 512m
cgset -r cpu.shares=512 $UUID复制代码
上面 cpu.shares
是相对于同时运行的其余进程的CPU。单独运行的容器可使用整个CPU,可是若是其余容器正在运行,它们会按照比例分配cpu资源。除此之外,还能够对cpu内核数量的使用进行限制:
# 设置检查CPU使用状况的频率,单位是微秒
cgset -r cpu.cfs_period_us=1000000 $UUID
# 设置任务在一个时间段内在一个核心上运行的时间量,单位是微秒
cgset -r cpu.cfs_quota_us=2000000 $UUID复制代码
而后咱们使用 unshare
命令在 cgroug 中执行命令,它能够实现 namespace 的隔离。
cgexec -g cpu,memory:$UUID \
> unshare -uinpUrf --mount-proc \
> sh -c "/bin/hostname $UUID && chroot $ROOTFS /bin/sh"
/ echo "Hello from in a container"
Hello from in a container
复制代码
最后,在执行结束后,经过下面的指令清理环境
cgdelete -r -g cpu,memory:$UUID
rm -r $ROOTFS复制代码
这一部分的实践,主要参考自文章 https://www.ianlewis.org/en/container-runtimes-part-2-anatomy-low-level-contai
High-level runtimes相较于low-level runtimes位于堆栈的上层。low-level runtimes负责实际运行容器,而High-level runtimes负责传输和管理容器镜像,解压镜像,并传递给low-level runtimes来运行容器。目前主流的 high-level runtime 有:
这里咱们以 containerd 为例具体解析整个架构以及工做原理。
containerd 的架构图如图
其中,grpc 模块向上层提供服务接口,metrics 则提供监控数据(cgroup 相关数据),二者均向上层提供服务。containerd 包含一个守护进程,该进程经过本地 UNIX 套接字暴露 grpc 接口。
storage 部分负责镜像的存储、管理、拉取等metadata 管理容器及镜像的元数据,经过bootio存储在磁盘上task -- 管理容器的逻辑结构,与 low-level 交互event -- 对容器操做的事件,上层经过订阅能够知道发生了什么事情Runtimes -- low-level runtime(对接 runc)
containerd 主要流程以下:(图片来源于阿里云的公开课)
图中的 containerEngine 在 docker 中就是 docker-containerd 组件,建立容器记录的metadata,并请求 containerd 的 task 模块,task 模块会在 runtime 中建立 task 实例,分别会加入 task list, 监控 cgroup 等操做,每一个 task 实例则调用 shim 去建立container。
containerd-shim 是 containerd 的一个组件,主要是用于剥离 containerd 守护进程与容器进程。containerd 经过 shim 调用 runc 的包函数来启动容器。当咱们执行 pstree
命令时,能够看到以下的进程关系:引入shim,容许runc 在建立和运行容器以后退出,并将 shim 做为容器的父进程,而不是 containerd 做为父进程,这样作的目的是当 containerd 进程挂掉,因为 shim 还正常运行,所以能够保证容器不受影响。此外,shim 也能够收集和报告容器的退出状态,不须要 containerd 来 wait 容器进程。
当咱们有需求去替换 runc 运行时工具库时,例如替换为安全容器 kata container 或 Google 研发的 gViser,则须要增长对应的shim(kata-shim等),以上二者均有本身实现的 shim。
目前已有 shim v1 和 shim v2 两个版本,至于 K8s 如何使用 CRI 与 containerd 和 shim 交互,这部分将在下一篇博文中介绍。若是喜欢,请关注个人公众号,或者查看个人博客 http://packyzbq.coding.me. 我会不定时的发送我本身的学习记录,你们互相学习交流哈~