容器运行时container详解
容器运行时container详解
1 容器运行时介绍
容器运行时主要负责管理容器的整个生命周期,其提供的主要功能如下:
- 1 制定容器镜像格式
- 2 构建容器镜像
- 3 管理容器镜像
- 4 管理容器实例
- 5 运行容器
- 6 容器镜像共享
2 容器与镜像规范
Docker 公司与 CoreOS 和 Google 共同创建了 OCI (Open Container Initial),并提供了两种规范分别是:
3 基于OCI标准实现的容器运行时
根据容器运行时是否提供对镜像的管理将容器运行时分为 low-level 和 high-level 两类。
- low-level : runc、lxc、lmctfy、kata-runtime、runhcs、crun
- high-level : containerd、cri-o、rkt、isulad、Frakti、PouchContainer
其中 low-level 主要功能为创建运行容器,并与操作系统进行交互。相较于 High-level , low-level 缺少对容器的编排和镜像的管理等功能。
4 low-level 容器运行时详细介绍
4.1 RunC
RunC是一个依据OCI标准用于生成和运行容器的轻量级Linux工具。RunC是目前最广泛的容器运行时,它最初是作为Docker的一部分开发,后来被提取出来作为一个单独的工具和库,作为OCI实现的容器运行时参考。
其中RunC包含libcontainer(用于创建容器)和命令管理工具两个部分。libcontainer是具体通过cgroup、namespace等Linux内核特性创建容器运行时。
5 High-level 容器运行时详解介绍
5.1 containerd
containerd 是一个工业标准的轻量容器运行时。作为守护程序运行时,containerd 管理完成的容器运行时生命周期、镜像拉取和存储、容器运行、监控、low-level 存储和网络等。目前 containerd 已经加入到 CNCF 基金会中。
整个 containerd 的组件架构图如下:
由图可以看出containerd的组件之间通过RPC进行通信,这样的好处是可以将各个模块解耦开。但是也牺牲了组件之间通信的性能。以下列出一些组件的主要作用:
- Content 以 image layer 哈希值(一般使用 sha256 算法生成)为索引,支持快速 - - layer 快速查找和读取,并支持对 layer 添加 label。索引和 label 信息存储在 boltDB。
Images,在 boltDB 中存储了 reference 到 manifest layer 的映射,结合 Content 可以组织完整的 image 信息。 - Snapshot 存储、处理解压后的 fs layers 和容器 work layer,索引信息同样存储在 boltDB。Snapshot 内置支持多种U nionFS(如 overlay,aufs,btrfs)。
- Containers,以 container ID 为索引,在 boltDB 中存储了低级运行时描述、 snapshot 文件系统类型、 snapshotKey(work layer id)、image reference 等信息。
- Diff 可用于比对 image layer tar 和 fs layers 差异输出 diffID,可以校验 image config 中的 diffID,同样也能比对 fs layers 之间的差异。
- Task 是组件结构中最重要的模块,它负责与容器进程管理和 low-Level 容器运行时组件进行交互。当创建容器时,会传输给 Task 组件的是一个包含 low-Level 容器运行时、OCI spec、镜像等信息在请求任务。 Task 模块根据请求任务中包含的信息构建出传递给 containerd-shim 进程的运行命令,通过系统调用启动 shim 进程,并与 shim 进程建立 TTRPC通信,shim进程最后会根据构建命令组成容器运行所需要的 rootfs,并结合 OCI spec 调用low-Level 容器运行时运行容器进程,并返回给 containerd 进程。
5.2 CRI-O
CRI-O是一个基于OCI规范和k8s容器运行时接口的 High-Level 容器运行时。与 containerd 不同,CRI-O不同模块之间为纯粹的Go语言依赖,而非基于RPC的通信协议。相较于 containerd CRI-O 的内部组件扩展性较差,但取消了基于通信协议的代价。以下列出一些组件的主要作用:
- containers/image 负责 Image 的下载,镜像规范的通用抽象,镜像内容的进一步处理等。
- containers/storage 负责CRI-O 大部分业务逻辑包括镜像层次的接口处理、镜像的接口处理、容器存储的接口处理。
CRI-O 运行容器进程时,先确保对应 image 存在(不存在则尝试下载),随之基于 image top layer 创建 UnionFS,同时生成 OCI spec config.json,之后,根据请求方提供的低级运行时信息(RuntimeHandler),使用不同包装实现操作容器进程。
- RuntimeHandler 为非 VM 类型,创建并委托监视进程 conmon 操作低级运行时创建容器。之后,conmon 在特定路径提供一个可与容器进程通信的 socket 文件,并负责持续监视容器进程并负责将其 stream 写入指定日志文件。容器进程创建成功之后,CRI-O 直接与低级运行时交互执行 start、delete、update 等操作,或者通过 socket 文件直接与容器进程交互
- RuntimeHandler 为 VM,则创建并委托 containerd-shim 进程处理间接容器进程(请求包含完整 rootfs,Mounts 为 空)。与非 VM 类型不同,此后所有容器进程相关操作均通过 shim 完成
整个CRI-O的架构图如下:
6 模拟容器运行时Demo
笔者认为容器运行时本质上是使用Linux特性中chroot进行文件系统隔离的基础上做进一步资源隔离。通过cgroup指定容器启动中的cpu、内存、网络等资源的规模,从而区分容器环境和宿主机环境。一个 low-Level 容器一般指一个按照 OCI 规范的根文件系统 ( rootfs ) 和配置文件 ( config.json ) 并运行隔离进程的实现。因此完全可以通过现有命令chroot、cgset等通过cli模拟一个简单的容器运行时。
这里以busybox镜像作为运行时的一个根文件系统,模拟创建一个容器运行时。
6.1 准备根文件系统
CID=$(docker create busybox)
# mktemp会在/tmp目录下创建一个临时的文件夹
ROOTFS=$(mktemp -d)
# 将镜像解压到临时的文件目录中
# 镜像本质就是一个压缩的根文件系统,包含预选准备好的二进制文件和相关库,以一定规范组织好的压缩文件
docker export $CID | tar -xf - -C $ROOTFS
6.2 创建 cgroup 对内存和cpu进行限制
UUID=$(uuidgen)
cgcreate -g cpu,memory:$UUID
# 内存限制设置为 100MB
cgset -r memory.limit_in_bytes=100000000 $UUID
# cpu 限制设置为 512m
# cpu.shares 是相对于同时运行的其他进程的CPU。单独运行的容器可以使用整个CPU,但是如果其他容器正在运行,它们会按照比例分配cpu资源。除此以外,还可以
cgset -r cpu.shares=512 $UUID
6.3 对cpu内核数量的使用进行限制
# 设置检查CPU使用情况的频率,单位是微秒
cgset -r cpu.cfs_period_us=1000000 $UUID
# 设置任务在一个时间段内在一个核心上运行的时间量,单位是微秒
cgset -r cpu.cfs_quota_us=2000000 $UUID
6.4 实现 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
6.5 执行结束后,通过下面的指令清理环境
cgdelete -r -g cpu,memory:$UUID
rm -r $ROOTFS
Member discussion