RunC深度分析

从源码分析RunC的实现

容器标准化宗旨

随着容器的发展,Linux基金会于2015年6月成立OCI(Open Container Initiative)组织,旨在围绕容器格式和运行时制定一个开放的工业化标准。而RunC时docker按照开放容器格式标准(OCF,Open Container Format)制定的一种具体实现。 容器标准化宗旨具体分为以下五条:

  • 操作标准化: 容器的标准化操作包括使用标准容器创建、启动、停止容器,使用标准文件系统工具复制和创建容器快照,使用标准化网络工具进行下载和上传。
  • 内容无关: 内容无关指不管容器内容时什么,容器标准操作后都能产生同样的效果。如容器可以用同样的方式上传、启动,不管是php应用还是mysql数据库服务。
  • 基础设施无关: 无论时个人的笔记本还是AWS S3,还是openstack,或者其他基础设施,都应该支持对容器的各项操作。
  • 为自动量身定制: 制定容器统一标准,是操作内容无关化、平台无关化的根本目的之一,为了可以让容器操作全平台自动化。
  • 工业级交付: 制定容器标准的一大目标,就是使软件分发可以达到工业级交付成为现实。

容器标准包(bundle)配置

一个标准的容器包至少包含三部分:

  • config.json: 基本配置文件,包括与宿主机独立的、和应用相关的特定信息,如安全权限、环境变量和参数等。
  • runtime.json: 运行时配置文件,包含运行时与主机相关的信息,如内存限制、本地设备访问权限、挂载点等。除此之外,运行时配置文件还提供了hooks特性,这样可以在容器的运行前和停止后执行一些自定义的脚本。
  • rootfs: 根文件系统目录,包含 了容器执行所需的必要环境依赖,如/bin、/var、/lib、/dev、/usr等目录及相应文件。rootfs目录必须与包含配置信息等config.json文件同时存在容器目录最顶层。

标准的容器生命周期

包括三个基本过程:

  • 容器创建: 创建包括文件系统、namespaces、cgroups、用户权限在内的各项内容。
  • 容器进程的启动: 运行容器进程,进程的可执行文件定义在config.json中的arg项。
  • 容器暂停: 容器作为进程可以被外部程序关停,然后容器标准规范应该包含对容器暂停信号的捕获,并做相应资源回收的处理,避免孤儿进程的出现。

runC的实现原理:

runC的前身就是docker的libcontainer项目,在libcontainer的基础上加上了一层用cli.go实现的命令行的封装,程序结构如下图,执行逻辑基本上都是:
command →️ runC →️ libcontainer

_config.yml

libcontainer特性:

1. 建立文件系统

容器运行需要rootfs,所有容器中要执行的命令,都需要包含在rootfs中,所有挂载在容器销毁都会被卸载,因为mount namespace会在容器销毁时一同销毁。当容器的文件系统刚挂载完毕时,/dev文件系统会被一系列设备节点所填充,所以/dev文件系统下的设备节点不应该由rootfs管理,libcontainer会负责处理并正确启动这些设备。当文件系统创建完毕后,umask权限会被重新设置回0022。

2. 资源管理

所有cgroups支持的子系统都被加入到libcontainer的管理中,libcontainer都可以直接操控,具体的可以看另一篇关于cgroups的博客。

3. 运行时和初始化进程

容器创建过程中,父进程要与容器的init进程进行同步通信,通信的方式是通过向容器中传入管道来实现。当init启动时,它会等待父进程完成初始化,建立uid/gid映射,把新进程放进新建的cgroup等等,直到收到父进程的EOF信息才会关闭管道。

4. 在运行着的容器中执行新进程

用户可以在运行着的容器中执行一条新指令,也就是docker exec的功能。通过这种方式运行的进程会随着容器的状态变化,如果容器杯暂停,进程也会随之暂停,恢复也会恢复,容器不存在时进程就会被销毁。

5. 容器热迁移

Libcontainer用CRIU(Checkpoint/Restore In Userspace)作为容器检查点保存与恢复(即热迁移)的解决方案,通信方式为rpc。libcontainer可以把一个正在运行的进程状态保存到磁盘上,然后在本地或其他机器上恢复当前的运行状态。
CRIU的工作方式分为两部分,一部分时检查点的保存,分为三步:
(1)收集进程与其子进程构成的树,并冻结所有进程。
(2)收集task(包括进程和线程)使用的所有资源,并保存。
(3)清理收集资源的相关寄生代码,并于进程分离。

另一部分是恢复,分为四步:
(1)读取快照文件并解析出共享的资源,对多个进程共享的资源优先恢复,其他资源则随后需要时恢复。
(2)使用fork恢复整个进程树,但此时并不恢复线程。
(3)恢复所有基础task(包括进程和线程)资源,除了内存映射、计数器、证书和线程,主要是打开文件、准备namespace、创建socket连接等。
(4)恢复进程运行的上下文环境,恢复剩下的其他资源,继续运行进程。

libcontainer实现原理:

下面主要从libcontainer中Factory、Container、Process三个最核心的接口入手介绍libcontainer的实现。

1. Factory

Factory对象为容器创建和初始化工作提供了一组抽象接口,具体实现是LinuxFactory结构体。Factory接口中包含如下四个方法,通过LinuxFactory来讲解具体实现。

  • Create(): 通过id和配置参数创建容器,并返回一个Container对象,这个对象包含id、root目录、配置参数、初始化路径和参数、criu路径、uid/gid路径和cgroup管理方式。
  • Load(): 通过id来获取Container对象,通过读取root目录下的state.json来获取容器信息。
  • Type(): 返回容器管理的类型,默认“libcontainer”。
  • StartInitialization(): 容器初始化函数,是在容器内部执行的,当容器创建时,默认执行的第一条init命令就是这个函数。先是获取环境变量,生成一个init配置参数,真正执行init的是linuxStandardInit结构体的Init()方法,初始化网络、路由、namespace、hostname等等配置,最后执行需要执行的命令。值得注意的是,如果容器成功启动,StartInitialization()这个函数是不会返回的,里面defer函数也就都不会执行。

2. Container

Container对象包含了容器配置、控制、状态显示等功能,是对不同平台容器的抽象。runC中实现的是linux平台下的容器对象linuxContainer,每一个Container进程内部都是线程安全的。还有一点,由于Container有可能被外部的进程销毁,所以每个方法都会检测容器是否存在。

  • ID(): 显示Container的ID,ID唯一。
  • Status(): 返回容器内进程是运行还是停止状态,通过刷新来更新最新状态。
  • State(): 返回容器的状态,包括容器ID、配置信息、init进程ID、init启动时间、创建时间、cgroup路径、intelrdt路径、namespace路径。
  • Config(): 返回容器配置信息。
  • Processes(): 返回容器内所有进程的ID。
  • Stats(): 返回容器的统计信息,主要是网卡、cgroups和intelrdt的统计信息。
  • Set(): 设置容器cgroup各子系统的路径,正在运行的容器也可以通过该方法进行实时修改从而改变资源分配。
  • Start(): 用于启动容器。先构建ParentProcess对象,然后进行启动容器的初始化工作,并通过创建ParentProcess时的管道与子进程(容器)通信。
  • Destroy(): 销毁容器。先使用cgroup的freezer子系统暂停容器内所有进程,然后向所有进程发送SIGKILL信号。杀死所有容器内进程后,卸载cgroup、删除root路径内容、执行PoststopHooks。
  • Pause(): 使用cgroup的freezer子系统暂停所有运行的进程。
  • Resume(): 使用cgroup的freezer子系统恢复所有运行的进程。
  • NotifyOOM(): 为容器内存超额使用时提供只读的通道。
  • Checkpoint(): 通过rpc的方式CRIU保存容器进程checkpoint快照,可以为容器热迁移做准备。
  • Restore(): 通过rpc的方式恢复checkpoint,可以实现容器热迁移。

3. Process

runC中process有两类,一类时Process,用于容器内进程的配置和IO的管理;另一类是ParentProcess,负责处理容器启动工作,与Container对象直接接触,容器启动完成后作为Process的一部分(即ops对象),执行等待、发信号、获取进程pid等管理工作。如果是新建容器,那ParentProcess的具体实现就是initProcess,如果是已有的容器,具体实现就是setnsProcess。不论是ParentProcess还是Process,拥有的方法的代码逻辑都比较简单,可以自行查看。

Intel RDT拓展:

RDT全称Resource Director Technology,主要用来调控cache(L3缓存)和内存带宽,由5个功能模块构成:

  • CMT(Cache Monitoring Technology)缓存监测技术
  • CAT(Cache Allocation Technology)缓存分配技术
  • MBM(Memory Bandwidth Monitoring)内存带宽监测技术
  • MBA(Memory Bandwidth Allocation)内存带宽分配技术
  • CDP(Code and Data Prioritization)代码和数据分区技术

5个模块分为监控和控制两大类,CMT和MBM为监控技术,CAT、MBA和CDP为控制技术。RDT允许OS或VMM来监控线程,应用OS或VM使用的cache和内存带宽空间,通过分析cache和内存带宽空间,OS或VMM可以优化调度策略提高效能。使用这5个技术,OS可以知道应用使用了多少cache空间和内存带宽,从而给虚拟机的虚拟处理器分配真实的CPU资源。

Written on December 15, 2017