开放容器标准(OCI) 内部分享

本文是 Engineering Efficiency 系列的第 2 篇文章, 共 5 篇。

研究工程效率提升必然逃不开容器化,容器化能够屏蔽不同项目的细节,大幅度降低构建持续集成系统的难度,只需要专注于提供平台服务即可,这对我们 Team 来说尤为重要:项目历史包袱重,开发周期长,依赖众多,还正在经历主力开发语言从 PythonGolangRust 的转变。而正式发布于 2017 年的开放容器标准(OCI)的出现使得整个容器社区都在朝着标准化的方向发展,为社区注入了新的动力,很多依托于新标准的项目涌现了出来。在这样的背景下,我在例会之后进行了分享,介绍开放容器标准以及社区向着标准靠拢的努力,然后介绍一些基于标准开发的工具,最后做一些个人的展望。

OCI 是什么?

OCI,Open Container Initiative,是一个轻量级,开放的治理结构(项目),在 Linux 基金会的支持下成立,致力于围绕容器格式和运行时创建开放的行业标准。OCI 项目由 Docker,CoreOS(后来被 Red Hat 收购了,相应的席位被 Red Hat 继承)和容器行业中的其他领导者在 2015 年 6 月的时候启动。OCI 的技术委员会成员包括 Red Hat,Microsoft,Docker,Cruise,IBM,Google,Red Hat 和 SUSE,其中 Docker 公司有两名成员,且其中的一位是现任主席,具体的细节可以查看 OCI Technical Oversight Board

OCI 目前提出的规范有如下这些:

名称版本
Runtime Specificationv1.0.1
Image Formatv1.0.1
Distribution Specificationv1.0.0-rc0

其中 runtime 和 image 的规范都已经正式发布,而 distribution 的还在工作之中。runtime 规范中介绍了如何运行解压缩到磁盘上的 Filesystem Bundle。在 OCI 标准下,运行一个容器的过程就是下载一个 OCI 的镜像,将其解压到某个 Filesystem Bundle 中,然后某个 OCI Runtime 就会运行这个 Bundle。细节此处不再展开,感兴趣的同学可以直接阅读 Spec。

社区演进

标准如果没有人支持的话就只是个 Markdown 文件而已,整个容器社区为了 OCI 标准成为真正的行业标准付出了艰辛的努力。接下来我从几个侧面展开一下容器领域的各个关键组件是如何一步步走向 OCI 标准的,这个过程中也会捋清楚各个组件之间的关系。

OCI in docker

自从 2013 年 docker 发布之后,docker 项目本身逐渐成为了一个庞然大物。为了能够降低项目维护的成本,内部代码能够回馈社区,docker 公司提出了 “基础设施管道宣言” (Infrastructure Plumbing Manifesto):

  • 只要有可能,重新使用现有的管道并提供改进:当您需要创建新的管道时,可以轻松地重复使用并提供改进。 这增加了可用组件的公共池,每个人都受益。
  • 遵循 UNIX 原则:几个简单的组件比一个复杂的组件要好
  • 定义标准接口:可用于将许多简单组件组合到更复杂的系统中

docker 开始自行拆分自己项目中的管道代码并形成一个个新的开源项目:他们于 2014 年开源了 libcontainer,并在随后的几年中陆续开源了 libnetwork, notary, hyperkit 等项目。在 OCI 项目启动后,docker 公司将 libcontainer 的实现移动到 runC 并捐赠给了 OCI。此时,容器社区有了第一个 OCI Runtime 的参考实现。runC 是一个轻量可移植的容器运行时,包括了所有之前 docker 所使用的容器相关的与系统特性的代码,它的目标是:make standard containers available everywhere。随后在 2016 年,docker 开源并将 containerd 捐赠给了 CNCF,containerd 几乎囊括了单机运行一个容器运行时所需要的一切:执行,分发,监控,网络,构建,日志等。为了能够支持多种 OCI Runtime,containerd 内部使用 containerd-shim,每启动一个容器都会创建一个新的 containerd-shim 进程,指定容器 ID,Bundle 目录,运行时的二进制(比如 runc)。

于是,现代 docker 启动一个标准化容器需要经历这样的流程:

OCI in Kubernetes

Kubernetes 最初只支持 docker 作为运行时,为了能够让 Kubernetes 变得更具有可扩展性,在 1.5 版本增加了 CRI: the Container Runtime Interface,在随后的演进中,CRI 被抽出来做成了独立的项目:https://github.com/kubernetes/cri-api/

CRI 是一套通过 protocol buffers 定义的 API,如下图:

kubelet 实现了 client 端,CRI shim 实现 server 端。只要实现了对应的接口,就能接入 k8s 作为 Container Runtime。

k8s 1.5 中自己实现了 docker CRI shim,此时启动容器的流程如下:

从 containerd 1.0 开始,为了能够减少一层调用的开销,containerd 开发了一个新的 daemon,叫做 CRI-Containerd,直接与 containerd 通信,从而取代了 dockershim:

但是这仍然多了一个独立的 daemon,从 containerd 1.1 开始,社区选择在 containerd 中直接内建 CRI plugin,通过方法调用来进行交互,从而减少一层 gRPC 的开销,最终的容器启动流程如下:

最终的结果是 k8s 的 Pod 启动延迟得到了降低,CPU 和内存占用率都有不同程度的降低。

但是这还不是终点,为了能够直接对接 OCI 的 runtime 而不是 containerd,社区孵化了 CRI-O 并加入了 CNCF。CRI-O 的目标是让 kubelet 与运行时直接对接,减少任何不必要的中间层开销。CRI-O 运行时可以替换为任意 OCI 兼容的 Runtime,镜像管理,存储管理和网络均使用标准化的实现,目前还在积极开发中,前途无量。

@xuxinkun 的文章中有个图将他们之间的关系描绘的很清楚:

项目介绍

接下来会介绍一些支持 OCI 或者 OCI 相关的开源项目,为读者们提供一些新选择。

Runtime

  • opencontainers/runc:前面已经提到过很多次了,是 OCI Runtime 的参考实现。
  • kata-containers/runtime:容器标准反攻虚拟机,前身是 clearcontainers/runtimehyperhq/runv,通过 virtcontainers 提供高性能 OCI 标准兼容的硬件虚拟化容器,Linux Only,且需要特定硬件。
  • google/gvisor:gVisor 是一个 Go 实现的用户态内核,包含了一个 OCI 兼容的 Runtime 实现,目标是提供一个可运行非受信代码的容器运行时沙盒,目前是 Linux Only,其他架构可能会支持。

Image Build

  • moby/buildkit:从 docker build 拆分出来的项目,支持自动 GC,多种输入和输出格式,并发依赖解析,分布式 Worker 和 Rootless 执行等特性
  • genuinetools/img:对 buildkit 的一层封装,单独的二进制,没有 daemon,支持 Rootless 执行,会自动创建 SUBUID,比 buildkit 使用起来更加容易
  • uber/makisu:uber 开源的内部镜像构建工具,目标是在 Mesos 或 Kubernetes 上进行 Rootless 构建,支持的 Dockerfile 有些许不兼容,在非容器环境下运行会有问题,比如 Image failed to build without modifyfs
  • GoogleContainerTools/kaniko:Google 出品,目标是 Daemon free build on Kubernetes,要求运行镜像 gcr.io/kaniko-project/executor 进行构建,直接在别的镜像中使用二进制可能会不工作,很蠢
  • containers/buildah:开源组织 Containers 推出的项目,目标是构建 OCI 容器镜像,Daemon free,支持 Rootless 构建

Tools

  • containers/skopeo:这是一个用来查看容器镜像信息的工具,可以在不用下载到本地的前提下查看远端 Registry 中的镜像信息
  • containers/libpod:二进制名为 podman,支持管理 Pod,容器,镜像和存储卷,命令行与 docker CLI 完全兼容,基本上能视为 docker CLI 的 drop-in replace,镜像部分的代码主要使用了 buildah,未来还会支持 cgroups v2,人类文明之光

未来展望

技术的发展永远看不到尽头,也没有人知道会不会横空出现一个 docker 硬生生改变了 PaaS 平台发展的轨迹,企图当预言家的人最后都被刀了。这里列出来的是容器未来发展方向中我比较感兴趣的方面,他们更多的是现在进行时,而不是将来时,未来一年内可能就会落地。

OCI Artifacts

伴随着 image spec 与 distribution spec 的演化,人们开始逐步认识到除了 Container Images 之外,Registries 还能够用来分发 Kubernetes Deployment Files, Helm Charts, docker-compose, CNAB 等产物。它们可以共用同一套 API,同一套存储,将 Registries 作为一个云存储系统。这就为带来了 OCI Artifacts 的概念,用户能够把所有的产物都存储在 OCI 兼容的 Registiry 当中并进行分发。为此,Microsoft 将 oras 作为一个 client 端实现捐赠给了社区,包括 Harbor 在内的多个项目都在积极的参与。

到目前为止, 2.7+ 版本 Docker Distribution 和 Azure Container Registry 已经支持, quay.io 也在跟进。

Rootless Container

因为 Linux 下的 user namespace 过于复杂,所以 docker 刚发布的时候就没有做支持,docker 运行需要 root 权限,带来了大量的安全问题。在之后的几年中 userns 的支持被逐渐实现,尽管现在的配置还比较复杂,需要升级 runc 到特定版本,要设置 sysctl,需要安装特定的二进制,包括 newuidmap,newgidmap,还要 slirp4netns 来提供用户态网络栈支持。社区也在努力提升 Rootless Container 的体验和性能,未来大部分的工作负载都将会运行在 Rootless Container 当中。

我最近的一项工作就是在 CentOS 7.5 上实现对 Rootless Container 的支持,目前我们 QingStor Team 的 CI 全部由 Rootless Container 来完成,相关的介绍将会单独成文与大家分享。

dockerd free build

随着 docker 进入越来越多企业的生产和测试环境,依赖 dockerd 来进行容器构建的机制带来的问题变得越来越严重,人们开始不断寻找和开发出不依赖 dockerd 进行构建的项目。容器镜像构建最复杂的地方在于如何处理 RUN 指令,之前有些项目选择放在容器或者新的 namespace 中执行,但是随着 rootless container 的逐步完善,大家开始选择创建一个新的 userns 来执行命令,比如 buildah。

我预计未来绝大多数容器构建都将会脱离 dockerd,转而使用 buildah 或者 buildkit 之类的方案。至于 kaniko 和 makisu 那种方案,我觉得没有什么发展的空间,论方便好用拼不过 buildah,论功能全面打不过 buildkit,迟早凉凉。

cgroups v2

容器社区与 systemd && cgroups 的爱恨情仇简直能写成一本书,而 cgroups v2 就像是《怪物猎人:世界》雪原 这样的超大型扩充 DLC。

早在 2016 年 3 月,Linux 4.5 内核(cgroups v2 become official)发布后没多久,就有人提出要求支持 cgroups v2:support cgroup v2 (unified hierarchy)。然而至今进展缓慢,最开始是因为 cgroups v2 本身功能不太完善,无法满足 runc 的要求,后来是因为发行版(或者直接说是 systemd) 还没有实现真正的 cgroups v2 支持,现在是卡在了 OCI 标准强依赖于 cgroups v1 的某些实现,社区需要更新 OCI 标准来适应 cgroups v2 的变更。

困难是有的,但是我还是抱有期待,相信明年的今天 (#flag) 我就能用上支持 cgroups v2 的 runtime。

总结

这篇文章只是简单了介绍了开放容器标准和相关的一些项目,没有涉及到过多的细节,各位读者可以针对感兴趣的点向下继续探索。此外,除了附上引用地址和参考资料的片段外,其余观点均是我一家之言,各位读者请自行判断成色。

参考资料

comments powered by Disqus