低代码到底是不是行业毒瘤?一线大厂怎么做的?戳此了解>>> 了解详情
写点什么

Docker 背后的标准化容器执行引擎——runC

2015 年 10 月 09 日

随着容器技术发展的愈发火热,Linux 基金会于 2015 年 6 月成立 OCI(Open Container Initiative)组织,旨在围绕容器格式和运行时制定一个开放的工业化标准。该组织一成立便得到了包括谷歌、微软、亚马逊、华为等一系列云计算厂商的支持。而 runC 就是 Docker 贡献出来的,按照该开放容器格式标准(OCF, Open Container Format)制定的一种具体实现。本文作者及浙大团队将在接下来的容器系列文章中,从架构和源码层面详细解读这个开源项目的设计思想和实现原理,敬请关注。

1. 容器格式标准是什么?

制定容器格式标准的宗旨概括来说就是不受上层结构的绑定,如特定的客户端、编排栈等,同时也不受特定的供应商或项目的绑定,即不限于某种特定操作系统、硬件、CPU 架构、公有云等。

该标准目前由 libcontainer appc 的项目负责人(maintainer)进行维护和制定,其规范文档就作为一个项目在 GitHub 上维护,地址为 https://github.com/opencontainers/specs

1.1 容器标准化宗旨

标准化容器的宗旨具体分为如下五条。

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

1.2 容器标准包(bundle)和配置

一个标准的容器包具体应该至少包含三块部分:

  • config.json: 基本配置文件,包括与宿主机独立的和应用相关的特定信息,如安全权限、环境变量和参数等。具体如下:
    • 容器格式版本
    • rootfs 路径及是否只读
    • 各类文件挂载点及相应容器内挂载目录(此配置信息必须与runtime.json配置中保持一致)
    • 初始进程配置信息,包括是否绑定终端、运行可执行文件的工作目录、环境变量配置、可执行文件及执行参数、uid、gid 以及额外需要加入的 gid、hostname、低层操作系统及 cpu 架构信息。
  • runtime.json: 运行时配置文件,包含运行时与主机相关的信息,如内存限制、本地设备访问权限、挂载点等。除了上述配置信息以外,运行时配置文件还提供了“钩子 (hooks)”的特性,这样可以在容器运行前和停止后各执行一些自定义脚本。hooks 的配置包含执行脚本路径、参数、环境变量等。
  • rootfs/:根文件系统目录,包含了容器执行所需的必要环境依赖,如/bin/var/lib/dev/usr等目录及相应文件。rootfs 目录必须与包含配置信息的config.json文件同时存在容器目录最顶层。

1.3 容器运行时和生命周期

容器标准格式也要求容器把自身运行时的状态持久化到磁盘中,这样便于外部的其他工具对此信息使用和演绎。该运行时状态以 JSON 格式编码存储。推荐把运行时状态的 json 文件存储在临时文件系统中以便系统重启后会自动移除。

基于 Linux 内核的操作系统,该信息应该统一地存储在/run/opencontainer/containers目录,该目录结构下以容器 ID 命名的文件夹(/run/opencontainer/containers/<containerID>/state.json)中存放容器的状态信息并实时更新。有了这样默认的容器状态信息存储位置以后,外部的应用程序就可以在系统上简便地找到所有运行着的容器了。

state.json文件中包含的具体信息需要有:

  • 版本信息:存放 OCI 标准的具体版本号。
  • 容器 ID:通常是一个哈希值,也可以是一个易读的字符串。在state.json文件中加入容器 ID 是为了便于之前提到的运行时 hooks 只需载入state.json就可以定位到容器,然后检测state.json,发现文件不见了就认为容器关停,再执行相应预定义的脚本操作。
  • PID:容器中运行的首个进程在宿主机上的进程号。
  • 容器文件目录:存放容器 rootfs 及相应配置的目录。外部程序只需读取state.json就可以定位到宿主机上的容器文件目录。

标准的容器生命周期应该包含三个基本过程。

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

1.4 基于开放容器格式(OCF)标准的具体实现

从上述几点中总结来看,开放容器规范的格式要求非常宽松,它并不限定具体的实现技术也不限定相应框架,目前已经有基于 OCF 的具体实现,相信不久后会有越来越多的项目出现。

2. runC 工作原理与实现方式

runC 的前身实际上是 Docker 的 libcontainer 项目,笔者曾经写过一篇文章《Docker 背后的容器管理——Libcontainer 深度解析》专门对libcontainer 进行源码分析和解读,感兴趣的读者可以先阅读一下,目前runC 也是对libcontainer 包的调用,libcontainer 包变化并不大。所以此文将不再花费太多笔墨分析其源码,我们将着重讲解其中的变化。

2.1 runC 从 libcontainer 的变迁

从本质上来说,容器是提供一个与宿主机系统共享内核但与系统中的其他进程资源相隔离的执行环境。Docker 通过调用 libcontainer 包对 namespaces、cgroups、capabilities 以及文件系统的管理和分配来“隔离”出一个上述执行环境。同样的,runC 也是对 libcontainer 包进行调用,去除了 Docker 包含的诸如镜像、Volume 等高级特性,以最朴素简洁的方式达到符合 OCF 标准的容器管理实现。

总体而言,从 libcontainer 项目转变为 runC 项目至今,其功能和特性并没有太多变化,具体有如下几点。

  1. 把原先的nsinit移除,放到外面,命令名称改为runc,同样使用 cli.go实现,一目了然。
  2. 按照开放容器标准把原先所有信息混在一起的一个配置文件拆分成config.jsonruntime.json两个。
  3. 增加了按照开放容器标准设定的容器运行前和停止后执行的hook脚本功能。
  4. 相比原先的nsinit时期的指令,增加了runc kill命令,用于发送一个SIG_KILL信号给指定容器 ID 的init进程。

总体而言,runC 希望包含的特征有:

  • 支持所有的 Linux namespaces,包括 user namespaces。目前 user namespaces 尚未包含。
  • 支持 Linux 系统上原有的所有安全相关的功能,包括 Selinux、 Apparmor、seccomp、cgroups、capability drop、pivot_root、 uid/gid dropping 等等。目前已完成上述功能的支持。
  • 支持容器热迁移,通过 CRIU 技术实现。目前功能已经实现,但是使用起来还会产生问题。
  • 支持 Windows 10 平台上的容器运行,由微软的工程师开发中。目前只支持 Linux 平台。
  • 支持 Arm、Power、Sparc 硬件架构,将由 Arm、Intel、Qualcomm、IBM 及整个硬件制造商生态圈提供支持。
  • 计划支持尖端的硬件功能,如 DPDK、sr-iov、tpm、secure enclave 等等。
  • 生产环境下的高性能适配优化,由 Google 工程师基于他们在生产环境下的容器部署经验而贡献。
  • 作为一个正式真实而全面具体的标准存在!

2.2 runC 是如何启动容器的?

从开放容器标准中我们已经定义了关于容器的两份配置文件和一个依赖包,runc 就是通过这些来启动一个容器的。首先我们按照官方的步骤来操作一下。

runc 运行时需要有 rootfs,最简单的就是你本地已经安装好了 Docker,通过

复制代码
docker pull busybox

下载一个基本的镜像,然后通过

复制代码
docker export $(docker create busybox) > busybox.tar

导出容器镜像的 rootfs 文件压缩包,命名为busybox.tar。然后解压缩为rootfs目录。

复制代码
mkdir rootfs
tar -C rootfs -xf busybox.tar

这时我们就有了 OCF 标准的 rootfs 目录,需要说明的是,我们使用 Docker 只是为了获取 rootfs 目录的方便,runc 的运行本身不依赖 Docker。

接下来你还需要config.jsonruntime.json,使用

复制代码
runc spec

可以生成一份标准的config.jsonruntime.json配置文件,当然你也可以按照格式自己编写。

如果你还没有安装runc,那就需要按照如下步骤安装一下,目前runc暂时只支持 Linux 平台。

复制代码
# create a 'github.com/opencontainers' in your GOPATH/src
cd github.com/opencontainers
git clone https://github.com/opencontainers/runc
cd runc
make
sudo make install

最后执行

复制代码
runc start

你就启动了一个容器了。

可以看到,我们对容器的所有定义,均包含在两份配置文件中,一份简略的config.json配置文件类似如下,已用省略号省去部分信息,完整的可以查看官方 github

复制代码
{
"version": "0.1.0",
"platform": {
"os": "linux",
"arch": "amd64"
},
"process": {
"terminal": true,
"user": {
"uid": 0,
"gid": 0,
"additionalGids": null
},
"args": [
"sh"
],
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm"
],
"cwd": ""
},
"root": {
"path": "rootfs",
"readonly": true
},
"hostname": "shell",
"mounts": [
{
"name": "proc",
"path": "/proc"
},
……
{
"name": "cgroup",
"path": "/sys/fs/cgroup"
}
],
"linux": {
"capabilities": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
]
}
}

各部分表示的意思在 1.2 节中已经讲解,针对具体的内容我们可以看到,版本是 0.10,该配置文件针对的是 AMD64 架构下的 Linux 系统,启动容器后执行的命令就是sh,配置的环境变量有两个,是PATHTERM,启动后 user 的 uid 和 gid 都为 0,表示进入后是 root 用户。cwd项为空表示工作目录为当前目录。capabilities 能力方面则使用白名单的形式,从配置上可以看到只允许三个能力,功能分别为允许写入审计日志、允许发送信号、允许绑定 socket 到网络端口。

一份简略的runtime.json配置则如下,同样用省略号省去了部分内容:

复制代码
{
"mounts": {
"proc": {
"type": "proc",
"source": "proc",
"options": null
},
……
"cgroup": {
"type": "cgroup",
"source": "cgroup",
"options": [
"nosuid",
"noexec",
"nodev",
"relatime",
"ro"
]
}
},
"hooks": {
"prestart": null,
"poststop": null
},
"linux": {
"uidMappings": null,
"gidMappings": null,
"rlimits": [
{
"type": "RLIMIT_NOFILE",
"hard": 1024,
"soft": 1024
}
],
"sysctl": null,
"resources": {
"disableOOMKiller": false,
"memory": {
"limit": 0,
"reservation": 0,
"swap": 0,
"kernel": 0,
"swappiness": -1
},
"cpu": {
"shares": 0,
"quota": 0,
"period": 0,
"realtimeRuntime": 0,
"realtimePeriod": 0,
"cpus": "",
"mems": ""
},
"pids": {
"limit": 0
},
"blockIO": {
"blkioWeight": 0,
"blkioWeightDevice": "",
"blkioThrottleReadBpsDevice": "",
"blkioThrottleWriteBpsDevice": "",
"blkioThrottleReadIopsDevice": "",
"blkioThrottleWriteIopsDevice": ""
},
"hugepageLimits": null,
"network": {
"classId": "",
"priorities": null
}
},
"cgroupsPath": "",
"namespaces": [
{
"type": "pid",
"path": ""
},
{
"type": "network",
"path": ""
},
{
"type": "ipc",
"path": ""
},
{
"type": "uts",
"path": ""
},
{
"type": "mount",
"path": ""
}
],
"devices": [
{
"path": "/dev/null",
"type": 99,
"major": 1,
"minor": 3,
"permissions": "rwm",
"fileMode": 438,
"uid": 0,
"gid": 0
},
……
{
"path": "/dev/urandom",
"type": 99,
"major": 1,
"minor": 9,
"permissions": "rwm",
"fileMode": 438,
"uid": 0,
"gid": 0
}
],
"apparmorProfile": "",
"selinuxProcessLabel": "",
"seccomp": {
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": []
},
"rootfsPropagation": ""
}
}

可以看到基本的几项配置分别为挂载点信息、启动前与停止后 hooks 脚本、然后就是针对 Linux 的特性支持的诸如用户 uid/gid 绑定,rlimit 配置、namespace 设置、cgroups 资源限额、设备权限配置、apparmor 配置项目录、selinux 标记以及 seccomp 配置。其中, namespaces cgroups 笔者均有写文章详细介绍过。

再下面的工作便都由 libcontainer 完成了,大家可以阅读这个系列前一篇文章《Docker 背后的容器管理——Libcontainer 深度解析》或者购买书籍《Docker 容器与容器云》,里面均有详细介绍。

简单来讲,有了配置文件以后,runC 就开始借助 libcontainer 处理以下事情:

  • 创建 libcontainer 构建容器需要使用的进程,称为 Process;
  • 设置容器的输出管道,这里使用的就是 daemon 提供的 pipes;
  • 使用名为 Factory 的工厂类,通过 factory.Create(< 容器 ID>, < 填充好的容器模板 container>) 创建一个逻辑上的容器,称为 Container;
  • 执行 Container.Start(Process) 启动物理的容器;
  • runC 等待 Process 的所有工作都完成。

可以看到,具体的执行者是 libcontainer,它是对容器的一层抽象,它定义了 Process 和 Container 来对应 Linux 中“进程”与“容器”的关系。一旦上述物理的容器创建成功,其他调用者就可以通过 ID 获取这个容器,接着使用 Container.Stats 得到容器的资源使用信息,或者执行 Container.Destory 来销毁这个容器。

综上,runC 实际上是把配置交给 libcontainer,然后由 libcontainer 完成容器的启动,而 libcontainer 中最主要的内容是 Process、Container 以及 Factory 这 3 个逻辑实体的实现原理。runC 或者其他调用者只要依次执行“使用 Factory 创建逻辑容器 Container”、“用 Process 启动逻辑容器 Container”即可。

3. 总结

本文从 OCI 组织的成立开始讲起,描述了开放容器格式的标准及其宗旨,这其实就是 runC 的由来。继而针对具体的 runC 特性及其启动进行了详细介绍。笔者在后续的文章中还将针对 runC 中诸如 CRIU 热迁移、selinux、apparmor 及 seccomp 配置等特性进行具体的介绍。可以看到 OCI 的成立表明了社区及各大厂商对容器技术的肯定以及加快容器技术发展进步的强烈决心,相信在不久的将来,符合 OCI 标准的开放容器项目会越来越多,容器技术将更加欣欣向荣地不断前进。

4. 作者简介

孙健波,浙江大学SEL 实验室硕士研究生,《Docker 容器与容器云》主要作者之一,目前在云平台团队从事科研和开发工作。浙大团队对PaaS、Docker、大数据和主流开源云计算技术有深入的研究和二次开发经验,团队现将部分技术文章贡献出来,希望能对读者有所帮助。


感谢郭蕾对本文的策划和审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群)。

2015 年 10 月 09 日 10:3814175
用户头像

发布了 22 篇内容, 共 27.8 次阅读, 收获喜欢 97 次。

关注

评论

发布
暂无评论
发现更多内容

DevSecOps安全检查清单

啸天

安全 DevSecOps 应用安全

“反垄断”来袭,对产业区块链有什么启发

CECBC区块链专委会

市场垄断

生产者与消费者模式,数组阻塞队列(ArrayBlockingQueue)

码农架构

Java 学习 并发编程 架构、

一文解析DDD中台和微服务设计

欧创新

中台 微服务 领域驱动设计 DDD 微服务划分

架构师训练营第十三周作业

李日盛

PageRank

一文带你探究Sentinel的独特初始化

华为云开发者社区

redis sentinel 框架

即构小程序直播组件集成教程

ZEGO即构

矿机挖矿APP系统模式开发平台

v16629866266

解决div里面img图片下方有空白的问题

学习委员

CSS html html5 前端 28天写作

『CDN』让你的网站访问起来更加柔顺丝滑

古时的风筝

CDN

架构师训练营W13作业

Geek_f06ede

遇到代码缺陷不要慌,马上教你快速检测和修复

华为云开发者社区

代码 bug 缺陷检测 代码缺陷

Volcano架构设计与原理介绍

华为云原生团队

大数据 AI 云原生 高性能 批量计算

我在极客时间录课的故事(一):从源码管理聊到一体化学习环境

李艺

我在极客时间录课的故事

公安指挥中心大屏可视化系统开发,情报研判分析平台建设

WX13823153201

不同公司产品经理岗位对比

LouisN

万字多图 | UML 入门指南

白色蜗牛

Java 程序员 架构设计 UML 后端编程

自动量化搬砖套利交易机器人系统软件APP开发

开發I852946OIIO

系统开发

区块链科普系列:区块链是什么?

CECBC区块链专委会

区块链

敏捷里为何倡导固定迭代周期?

万事ONES

敏捷开发 研发管理 迭代

第十三周课后练习

晴空万里

架构师训练营第2期

面试官:你真的了解Redis分布式锁吗?

鄙人薛某

redis 分布式锁 线程安全 RedLock

用AI「驯服」人类幼崽,手头有娃的可以试试

博文视点Broadview

人工智能 联邦学习 强化学习 集成学习 技术宅

区块链十年与传统金融的变化

CECBC区块链专委会

区块链 金融

来不及解释!Linux常用命令大全,先收藏再说

华为云开发者社区

Linux 编程 命令行 命令

当音乐学博士搞起编程...

程序猿DD

Spring Frame

为什么我认为 Deno 是一个迈向错误方向的 JavaScript 运行时?

hylerrix

typescript rust nodejs deno V8

「产品经理训练营」第一章作业

Sòrγy_じò ぴé

产品经理训练营

4大应用场景,16张高阶布局大屏,最具价值的数据可视化都在这里!

一只数据鲸鱼

物联网 数据可视化 智慧大屏可视化 3D可视化

古有诸葛亮八卦阵阻敌,今有iptables护网安

华为云开发者社区

安全 防火墙 网络 iptables 数据包

Kubernetes概念篇:基本概念和术语

xcbeyond

Kubernetes 容器 pod 28天写作 Kubernetes从入门到精通

2021 ThoughtWorks 技术雷达峰会

2021 ThoughtWorks 技术雷达峰会

Docker背后的标准化容器执行引擎——runC-InfoQ