真正运行容器的工具:深入了解 runc 和 OCI 规范

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 真正运行容器的工具:深入了解 runc 和 OCI 规范

原始容器运行时


如果试图将链从最终用户绘制到实际的容器进程,它可能如下所示:640.png

640.png

runc 是一个命令行客户端,用于运行根据 Open Container Initiative (OCI) 格式打包的应用程序,并且是 Open Container Initiative 规范的兼容实现。


有一个关于如何运行容器和管理容器映像的开放容器计划(OCI) 和规范。runc 符合此规范,但还有其他符合 OCI 的运行时。甚至可以运行符合 OCI 标准的虚拟机,Kata Containers 与gVisor就是符合符合 OCI 标准的虚拟机。gVisor 为代表的用户态 Kernel 方案是安全容器的未来,只是现在还不够完善。


runc 希望提供一个“ OCI 包”,它只是一个根文件系统和一个config.json 文件。而不是Podman 或 Docker 那样有“镜像”概念,所以不能只执行runc run nginx:latest这样来启动一个容器。


Runc 符合 OCI 规范(具体来说,是runtime-spec),这意味着它可以使用 OCI 包并从中运行一个容器。值得重申的是,这些bundle并不是“容器镜像”,它们要简单得多。层、标签、容器注册表和存储库等功能 - 所有这些都不是 OCI 包甚至运行时规范的一部分。有一个单独的 OCI-spec (image-spec )定义镜像。


文件系统包是你下载容器镜像并解压后得到的。所以它是这样的:


OCI Image -> OCI Runtime Bundle -> OCI Runtime


在我们的例子中,这意味着:


Container image -> Root filesystem and config.json -> runc


让我们构建一个应用程序包。我们可以从 config.json 文件开始,因为这部分非常简单:


mkdir my-bundle
cd my-bundle
runc spec


runc spec生成一个虚拟的 config.json。它已经有一个“进程”部分,用于指定在容器内运行哪个进程 - 即使有几个环境变量。


{
        "ociVersion": "1.0.1-dev",
        "process": {
             "terminal": true,
                "user": {
                        "uid": 0,
                        "gid": 0
                },
                "args": [
                        "sh"
                ],
                "env": [
                        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                        "TERM=xterm"
                ],
...


它还定义了在哪里查找根文件系统...


...
        "root": {
                "path": "rootfs",
                "readonly": true
        },
...


...以及其他许多内容,包括容器内的默认挂载、功能、主机名等。如果检查此文件,会注意到,许多部分与平台无关,并且特定于具体操作系统的部分嵌套在适当的内部部分。例如,会注意到有一个带有 Linux 特定选项的“linux”部分。


如果我们尝试运行这个包,我们会得到一个错误:


# runc run test
rootfs (/root/my-bundle/rootfs) does not exist


如果我们简单地创建文件夹,我们会得到另一个错误:


# mkdir rootfs
# runc run test
container_linux.go:345: starting container process caused "exec: \"sh\": executable file not found in $PATH"


这完全有道理 - 空文件夹并不是真正有用的根文件系统,我们的容器没有机会做任何有用的事情。我们需要创建一个真正的 Linux 根文件系统。这里可以使用如下命令解压rootfs:


$ docker export $(docker create busybox) | tar -C /mycontainer/rootfs -xvf -


这里我们使用skopeo 和 umoci 获取 OCI 应用程序包。


如何使用 skopeo 和 umoci 获取 OCI 应用程序包


从头开始创建 rootfilesystem 是一种相当麻烦的事情,因此让我们使用现有的最小映像之一  busybox。


要拉取镜像,我们首先需要安装skopeo。我们也可以使用 Buildah,但它的功能太多,无法满足我们的需求。Buildah 专注于构建镜像,甚至具有运行容器的基本功能。由于我们今天尽可能地低级别,我们将使用 skopeo:


  • skopeo 是一个命令行程序,可对容器镜像和镜像存储库执行各种操作。
  • skopeo 可以在不同来源和目的地之间复制镜像、检查镜像甚至删除它们。
  • skopeo 无法构建映像,它不知道如何处理 Containerfile。它非常适合自动化容器镜像升级的 CI/CD 管道。


yum install skopeo -y


然后复制busybox镜像:


skopeo copy docker://busybox:latest oci:busybox:latest


没有“拉取”——我们需要告诉 skopeo 镜像的来源和目的地。skopeo 支持几乎十几种不同类型的来源和目的地。请注意,此命令将创建一个新busybox文件夹,将在其中找到所有 OCI 镜像文件,具有不同的镜像层、清单等。


不要混淆 Image manifest 和 Application runtime bundle manifest,它们是不一样的。


我们复制的是一个 OCI Image,但是我们已经知道,runc 需要 OCI Runtime Bundle。我们需要一个将镜像转换为解压包的工具。这个工具将是umoci - 一个 openSUSE 实用程序,其唯一目的是操作 OCI 镜像。要安装它,请从 Github Releases获取最新版本的PATH。在撰写本文时,最新版本是0.4.5. umoci unpack获取 OCI 镜像并从中制作一个包:


umoci unpack --image busybox:latest bundle


让我们看看bundle文件夹里面有什么:


# ls bundle
config.json
rootfs
sha256_73c6c5e21d7d3467437633012becf19e632b2589234d7c6d0560083e1c70cd23.mtree
umoci.json


让我们将rootfs目录复制到之前创建的my-bundle目录。如果你好奇,这是rootfs的内容,如下:


bin  dev  etc  home  root  tmp  usr  var


如果它看起来像一个基本的 Linux 根文件系统,那么就是对的。


根据 OCI Runtime 规范,Linux ABI 下的应用程序会期望 Linux 环境提供以下特殊的文件系统:


  • /proc 文件夹,挂载 proc 文件系统。
  • /sys 文件夹,挂载 sysfs 文件系统。
  • /dev/pts 文件夹,挂载 devpts 文件系统。
  • /dev/shm 文件夹,挂载 tmpfs 文件系统。


这几个文件夹的作用这里略去,有兴趣的读者可以自行查阅 man7.org。runc 文档中还额外要求提供:


  • n
  • /dev/mqueue 文件夹,挂载 mqueue 文件系统。


runc 是 OCI Runtime 规范的参考实现,规范为容器的创建提供了整洁的接口,只需要为 runc 提供一份 config.json [1]。


使用 runc 运行 OCI 应用程序包


我们准备好将我们的应用程序包作为名为 的容器运行test:


runc run test


接下来发生的事情是我们最终进入了一个新创建的容器内的 shell!


# runc run test
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var


我们以默认foreground模式运行前一个容器。在这种模式下,每个容器进程都成为一个长时间运行的runc进程的子进程:


6801   997  \_ sshd: root [priv]
6805  6801      \_ sshd: root@pts/1
6806  6805          \_ -bash
6825  6806              \_ zsh
7342  6825                  \_ runc run test
7360  7342                  |   \_ runc run test


如果我终止与该服务器的 ssh 会话,runc 进程也会终止,最终杀死容器进程。让我们通过sleep infinite在 config.json 中替换 command并将终端选项设置为“false”来更仔细地检查这个容器。


runc不提供大量的命令行参数。它有类似start,stop和 run的命令来做容器的生命周期管理,但是容器的配置总是来自文件,而不是来自命令行:


{
        "ociVersion": "1.0.1-dev",
        "process": {
                "terminal": false,
                "user": {
                        "uid": 0,
                        "gid": 0
                },
                "args": [
                        "sleep",
                        "infinite"
                ]
...


这次让我们以分离模式运行容器:


runc run test --detach


我们可以看到正在运行的容器runc list:


ID          PID         STATUS      BUNDLE            CREATED                          OWNER
test        4258        running     /root/my-bundle   2020-04-23T20:29:39.371137097Z   root


在 Docker 的情况下,有一个Docker Daemon守护进程知道关于容器的一切。runc 如何找到我们的容器?事实证明,它只是在文件系统上保持状态,默认情况下在里面/run/runc/CONTAINER_NAME/state.json:


# cat /run/runc/test/state.json
{"id":"test","init_process_pid":4258,"init_process_start":9561183,"created":"2020-04-23T20:29:39.371137097Z","config":{"no_pivot_root":false,"parent_death_signal":0,"rootfs":"/root/my-bundle/rootfs","readonlyfs":true,"rootPropagation":0,"mounts"....


当我们在分离模式下运行时,原始runc run命令(不再有这样的进程)和这个容器

间没有关系。如果我们查看进程表,我们会看到容器的父进程是PID 1:


# ps axfo pid,ppid,command
4258     1 sleep infinite


Docker、containerd、CRI-O 等使用分离模式。它的目的是简化 runc 和全功能容器管理工具之间的集成。值得一提的是 runc 本身并不是某种类型的库——它是一个 CLI。当其他工具使用 runc 时,它们会调用我们刚刚在操作中看到的相同 runc 命令。


在runc 文档中阅读有关前台模式和分离模式之间差异的更多信息。虽然容器进程的PID是4258,但在容器内部PID显示为1:


# runc exec test ps                     
PID   USER     TIME  COMMAND
    1 root      0:00 sleep infinite
   13 root      0:00 ps


这要归功于Linux 命名空间,它是真正的容器背后的基本技术之一。我们可以通过lsns在主机系统上执行来列出所有当前的命名空间 :


# lsns
NS TYPE   NPROCS   PID USER COMMAND
4026532219 mnt         1  4258 root sleep infinite
4026532220 uts         1  4258 root sleep infinite
4026532221 ipc         1  4258 root sleep infinite
4026532222 pid         1  4258 root sleep infinite
4026532224 net         1  4258 root sleep infinite


runc 负责我们容器进程的进程、网络、挂载和其他命名空间。


容器世界的影子统治者


Podman、Docker 和所有其他工具,包括在那里运行的大多数 Kubernetes 集群,都归结为runc启动容器进程的二进制文件。


在实际工作中,几乎永远不会做我刚刚给你展示的事情 - 除非正在开发或者调试自己的或现有的容器工具。不能从容器映像中组装应用程序包,并且使用 Podman 而不是直接使用 runc 会更好。


runc就是Low-Level实现的实现,我们了解幕后发生的事情以及运行容器真正涉及的内容是非常有帮助的。最终用户和最终容器过程之间仍然有很多层,但是如果了解最后一层,那么容器将不再是神奇的东西,有时也很奇怪。最后你会发现容器它只是 runc 在命名空间中生成一个进程。当然最后一层是Linux内核,相比宇宙中有无数层。


runc 最重要的部分是它跟踪 OCI运行时规范。尽管几乎每一个容器,这些天与runc催生,它不具有与runc催生。可以将其与遵循运行时规范的任何其他容器运行时交换,并且容器引擎(如 CRI-O)应该以相同的方式工作。


High-Level容器运行时可以不依赖于 runc 本身。它们依赖于一些遵循 OCI 规范的容器运行时。这是当今容器世界真正美丽的部分。

相关实践学习
通过容器镜像仓库与容器服务快速部署spring-hello应用
本教程主要讲述如何将本地Java代码程序上传并在云端以容器化的构建、传输和运行。
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。     相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
相关文章
|
15天前
|
安全 Docker 容器
Docker中运行容器时Operation not permitted报错问题解决
【10月更文挑战第2天】Docker中运行容器时Operation not permitted报错问题解决
84 3
|
1月前
|
Ubuntu Linux pouch
Docker容器管理工具
文章介绍了Docker容器管理工具,以及早期使用的LXC容器管理工具,包括它们的安装、使用和相关技术特点。
65 10
Docker容器管理工具
|
1月前
|
Linux pouch 容器
CentOS7部署阿里巴巴开源的pouch容器管理工具实战
关于如何在CentOS 7.6操作系统上安装和使用阿里巴巴开源的Pouch容器管理工具的实战教程。
86 2
CentOS7部署阿里巴巴开源的pouch容器管理工具实战
|
17天前
|
应用服务中间件 Shell nginx
Docker容器运行
Docker容器运行
22 0
|
17天前
|
运维 Prometheus 监控
提升运维效率:容器化技术与自动化工具的结合
在当今信息技术飞速发展的时代,运维工作面临着前所未有的挑战。为了应对这些挑战,本文将探讨如何通过结合容器化技术和自动化工具来提升运维效率。我们将介绍容器化技术的基本概念和优势,然后分析自动化工具在运维中的应用,并给出一些实用的示例。通过阅读本文,您将了解到如何利用这些先进技术来优化您的运维工作流程,提高生产力。
|
2月前
|
JSON JavaScript 开发者
Composerize神器:自动化转换Docker运行命令至Compose配置,简化容器部署流程
【8月更文挑战第7天】Composerize神器:自动化转换Docker运行命令至Compose配置,简化容器部署流程
Composerize神器:自动化转换Docker运行命令至Compose配置,简化容器部署流程
|
1月前
|
Shell Docker 容器
10-19|使用date命令: 你可以在容器内使用date命令来设置时间,但为了防止这个更改影响宿主机,你不能以特权模式运行容器。我没有加特权模式的时候,使用此命令告诉我没权限啊
10-19|使用date命令: 你可以在容器内使用date命令来设置时间,但为了防止这个更改影响宿主机,你不能以特权模式运行容器。我没有加特权模式的时候,使用此命令告诉我没权限啊
|
2月前
|
存储 Kubernetes Cloud Native
容器管理工具Containerd
容器管理工具Containerd
|
2月前
|
Kubernetes Shell 测试技术
在Docker中,可以在一个容器中同时运行多个应用进程吗?
在Docker中,可以在一个容器中同时运行多个应用进程吗?
|
2月前
|
Shell Docker 容器
在Docker中,如何停止所有正在运行的容器?
在Docker中,如何停止所有正在运行的容器?