写点什么

麒麟云容器运行时优化之容器创建优化

作者:麒麟云
  • 2023-07-19
    湖北
  • 本文字数:2585 字

    阅读完需:约 8 分钟

麒麟云容器运行时优化之容器创建优化

CRI-O 是在 kubernetes CRI 标准提出后,RedHat 开发的一个轻量高效、仅为 Kubernetes 所用的容器运行时,cri-o 已成为 kubernetes 广泛采用的容器运行时组件。RedHat 在其容器云产品 Openshift 的最新版中已默认采用 CRI-O 做为容器运行时。在底层 OCI 运行时方面,RedHat 推出了 crun,crun 采用 c 编写。在容器创建需要用到的 fork,exec 模型支持,以及跟 linux 系统底层交互方面,c 比 golang 更有优势,此外,c 也更轻量高效。根据社区的测试结果,不管是用时还是内存开销,crun 都比 golang 编写的 runC 好。RedHat 的容器管理产品 podman 默认的 oci 运行时已采用 crun。麒麟云团队对 CRI-O 和 crun 创建容器的过程进行了深入的分析,提出了一些优化的方法。测试结果表明,优化后会有 25%的性能提升。这篇文章介绍相关的代码和所做的工作,基于 CRI-O v1.24 版本,crun v1.8 版本。


1 容器创建分析

与 docker 不同,kubernetes 中想要起一个业务容器,通常需要先创建 pod(通常使用 pause 镜像),然后在 pod 中起业务容器。多个容器可以使用同一个 pod,共享部分命名空间。而 pod 对于底层 OCI 侧来说,实际上也是一个容器。对于 CRI-O 来说,pod 对应 sandbox 的概念。一个标准的创建流程是先使用 pause 镜像(由配置文件决定)创建一个 sandbox,然后再创建容器 Container。我们主要关注创建容器的部分。

在 CRI-O 中创建完 sandbox 后,从耗时上看,创建容器大致需要以下三步:

1. 准备好容器的 rootfs,layers 等信息并记录,对应图中 createSandboxContainer 函数;

2. 调用底层 OCI 运行时,配置 namespace 和 cgroup,对应图中 createContainerPlatform 函数;

3. 持久化容器状态,对应图中 ContainerStateToDisk 函数。


图 1 CRI-O创建容器序列图


 其中函数 createContainerPlatform 会调用底层 OCI 运行时,创建容器的 namespace 和 cgroup,并配置 apparmor 和 seccomp 等安全配置。

以 crun 作为底层运行时为例,容器创建的状态流转,涉及到多次 fork 操作(部分操作已省略)。

从 libcrun_container_create 函数开始,在调用 detach_process 后将进程 daemonize 化。

然后进入最关键的容器初始化函数 libcrun_container_run_internal,它会 fork 出一个子进程,并通过 libcrun_run_linux_container 和 container_init 函数进行同步协作,完成 namespace 和 cgroup 的配置。


图2  crun创建容器活动图


 经过测试,CRI-O 创建单个容器火焰图如下:


图3 CRI-O创建单个容器火焰图


 耗时占比大致如下:


图4 CRI-O创建容器耗时占比饼图


 在创建容器的过程中,我们可以简单地将其按照函数的不同分成这三个部分。后续的介绍也会围绕这三个部分展开。

2 conmon 合并

在第二部分的“createContainerPlatform”函数中,其内部的操作可以简单的描述为:cgroup 创建,调用 conmon,conmon 调用 crun 实际创建容器,之后 CRI-O 等待 crun 返回容器 pid,确保容器创建完成后结束。


图5 createContainerPlatform序列图


 其中,conmon 起到类似 Docker 中垫片的作用,垫片的作用是让容器运行时和容器相互独立,使得容器管理程序(CRI-O)的启停不会影响容器运行。conmon 负责监控 crun 并获取容器的输入输出流,将容器的退出持久化到文件,CRI-O 通过 fsnotify 感知到容器退出。conmon 形态为一个独立的二进制,被 CRI-O 以命令行的方法调用。所以理论上,我们可以将 conmon 和 crun 合并,让 conmon 与 crun 绑定,减少一次 fork/exec 的开销,缩短 CRI-O 调用 conmon,再由 conmon 到 crun 的调用链。同时对 conmon 进行精简,减少二进制大小,缩短程序加载时间,进而缩短第二部分中 CRI-O 的等待时间。

3 seccomp 计算优化

在创建流程中,crun 作为底层 OCI 运行时负责实际启动容器进程,CRI-O 则会一直等待 crun 完成创建返回 pid。因此 crun 的创建速度直接影响整体的创建速度。我们调试中发现,在一些国产 arm64 平台上 crun 对于 seccomp BPF 每次都重新计算,这会增加约 10ms 的开销,占单次容器创建时间的 17%左右。理论上,X86 平台上也存在这一问题。seccomp 的主要作用是通过限制系统调用,保证容器安全。大多数容器需要受限制的系统调用都是通过 seccomp profile 模板生成的,容器之间的 seccomp BPF 文件通常是相同的。因此,可以通过将前一个容器创建过程中计算的 seccomp BPF 缓存到磁盘,并利用 checksum 验证缓存是否命中来避免重复计算 seccomp BPF,从而减少了容器创建的时间开销,提高了容器的创建效率。测试较不使用缓存时 crun 耗时缩短 15%。

crun 的相关代码如下图,Libcrun_open_seccomp_bpf 会去查找是否有 seccomp bpf 缓存,如果存在将 from_cache 置为 true。Libcrun_generate_seccomp 会根据 from_cache 跳过计算,否则会每次都会重新计算 seccomp bpf。



由于 find_in_cache 需要用到 gcrypt-devel 库计算 checksum,但是 crun 的 rpm spec 打包没有指定对其的依赖关系,导致使用 rpm 安装的 crun 二进制,不会使用 seccomp 缓存。

4 容器 layer 记录异步写

在 kubernetes 的大部分应用场景下,容器提供的为无状态的服务,容器异常可以通过重新创建容器恢复。在 CRIO 的原有代码中,对容器的 layer 信息记录采用的为同步写文件方式,引入了较大的延迟。我们在 CRI-O 的配置文件提供可选配置,用户可对容器是否有状态进行配置。我们修改了 storage 写文件逻辑,对容器的 layer 记录引入 volatileLayerLocation 与原来的 stableLayerLocation 做区分,对于镜像 layer 采用同步写的方式确保不丢失,对于指定了 Volatile=True 特性的容器的 layer 使用异步写的方式存放在 volatile-layers.json 中。通过分别存储镜像 layer 与启用了 Volatile 特性的容器 layer,确保对 volatile 和非 volatile 容器都能出错重新创建。这能够让来自第一部分“createSandboxContainer” 函数的耗时缩短约 50%,根据实际环境读写性能的不同,实际缩短的时间也会发生变化。

5 性能对比

在搭载了 KylinV10 系统的物理环境中使用 bucketbench 测试不同线程数创建总计 100 个容器的结果如下,整体而言与优化前相比提高了约 25%的创建速度。


图6 CRI-O容器创建速度对比图


银河麒麟云原生操作系统是麒麟软件面向云原生场景,为支撑云原生应用而全新打造的一款操作系统产品。云原生应用对操作系统有更多的功能需求和特殊的性能需求,如容器编排,集群调度,服务发现,负载均衡,弹性伸缩,资源隔离,快速容器创建,低延迟容器网络等。同时,在云原生应用编排领域,kubernetes 已成为业界的事实标准。银河麒麟云原生操作系统采用“内核+操作系统+ kubernetes”的联合设计思想,在传统操作系统接口外,为云原生应用提供 kubernetes 扩展 API,支持操作系统集群粒度统一纳管,支持一致性发布和原子回滚。在容器调度,容器网络,容器运行时等关键组件进行深入优化,致力打造国产平台容器性能标杆。银河麒麟云原生操作系统已运用于银行和证券领域。


用户头像

麒麟云

关注

打造国产平台容器性能标杆 2023-06-13 加入

银河麒麟云原生操作系统面向云原生场景,致力打造国产平台容器性能标杆。采用“Kernel+OS+ Kubernetes”的联合设计思想,在传统操作系统接口外,为云原生应用提供kubernetes 扩展API,现已运用于银行和证券等领域。

评论

发布
暂无评论
麒麟云容器运行时优化之容器创建优化_云原生_麒麟云_InfoQ写作社区