概述

  • 开源容器方案 —— Open Container Initiative 围绕的是把一个给定的 rootfs,按照配置文件的要求启动。当软件组件满足该规范时,它就是一个标准容器。软件的调用方只要理解这个规范并遵守其原则,就能够在没有依赖的情况下运行这个软件单元。下图举例说明了基于 OCI Runtime Spec 的软件生态:
    在这里插入图片描述
  • k8s 在上述软件生态的位置如下:
    在这里插入图片描述
  • OCI 最核心规范有两个:镜像格式标准(OCI Image Spec)和容器运行标准(OCI Runtime Spec)

OCI Image Spec

  • OCI 要求容器必须按照 container bundlerootfs + config.json 组织容器运行的前置依赖,目录结构如下:
bundle/
├── config.json
└── rootfs/

OCI Runtime Spec

  • OCI Runtime Spec 从以下三个方面描述标准容器需要满足的条件:
  1. 配置
  2. 执行环境
  3. 生命周期
  • 本文用 runc 举例,分析 OCI 标准,加深对容器底层实现的理解。

准备工作

下载 go

  • runc,containerd,docker 都是基于 go 语言开发,因此需要安装 go 语言用于编译 runc 源码,命令如下:
yum install -y golang
  • 检查是否安装成功,命令如下:
[root@hy runc]# go version
go version go1.15 linux/amd64
  • go 工具有默认的工作目录 GOPATH,当使用 build 和 get 命令时,GOPATH环境变量决定了 go 工具的查找路径,因此如果需要更改环境为个人的工作目录,首先通过如下命令查看go的环境变量:
go env
  • 然后修改 GOPATH 环境变量,将如下内容写入~/.bashrc中:
export GOPATH="/path/to/mygopath" 

下载 runc

  • 有两种方法下载 runc 的代码,一种是通过 go 工具下载,如下:
go get github.com/opencontainers/runc
  • 以上命令会将 runc 的源码自动下载到 $GOPATH/src/github.com/opencontainers/runc 目录下,另一种方法是直接通过 git 工具克隆代码,如下:
cd $GOPATH/src/github.com/opencontainers
git clone https://github.com/opencontainers/runc
  • 以上两种方法最终下载的代码都在 $GOPATH/src/github.com/opencontainers/runc 目录下,编译并安装 runc,命令如下:
cd $GOPATH/src/github.com/opencontainers/runc
make
make install
  • 检查runc工具是否安装成功:
[root@hy ~]# whereis runc
runc: /usr/local/sbin/runc
[root@hy ~]# runc -v
runc version 1.0.0-rc92+dev
commit: 33faa5d0e2404aaf4ceb1abbfe36c5135179d32f
spec: 1.0.2-dev
go: go1.15
libseccomp: 2.3.1

Image Spec

  • OCI 要求容器运行时只依赖 container bundle ,镜像簇由两个组件构成:
  1. config.json: 描述容器运行时预期的配置,所有容器必须满足 config.json 配置中包含的标准,不同运行时容器内部实现有所不同,但必须满足 config.json 的预期
  2. root filesystem: 描述容器运行所依赖镜像根文件系统,路径由 config.json 的 root->path 字段指定,不同运行时容器实现有所不同,但都围绕如何把一个 root filesystem 运行起来

config.json

  • config.json 经典字段说明:
  1. root
    指定了容器的根文件系统,它有两个属性,path 和 readonly,path 定义了根文件系统在主机上的路径,可以是绝对路径,也可以是相对路径(相对bundle目录的路径),readonly 声明该文件系统是否只读
  2. mount
    字段指定进程的挂载点信息

root filesystem

  • 标准容器基于根文件系统进一步构建自己的存储介质,满足其内部实现需要。

实验

  1. 下载一个学习用的小镜像 Alpine minirootfs tarball,首先使用 curl -I 检查 HTTP Response 头部:
curl -sSI --max-time 10 https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/aarch64/alpine-minirootfs-3.20.3-aarch64.tar.gz | head -5
HTTP/1.1 200 OK
Connection: close
Content-Length: 3947906 // 数据大小:接近 4 MB
Content-Security-Policy: script-src 'self'
Content-Type: application/octet-stream // 数据类型:二进制流
  1. 下载并解压
    使用 curl -O /path/to/image 指定镜像要下载的路径
curl -sSL -o alpine-minirootfs.tar.gz https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/aarch64/alpine-minirootfs-3.20.3-aarch64.tar.gz
mkdir -p bundle/rootfs & tar -xzf alpine-minirootfs.tar.gz -C bundle/rootfs && ls bundle/rootfs/
bin  dev  etc  home  lib  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
  1. 手动编辑一份 config.json 文件:
    目录组织如下:
[root@oe2403-dev bundle]# tree -L 1
.
├── config.json
└── rootfs

2 directories, 1 file
[root@oe2403-dev bundle]# tree -L 2
.
├── config.json
└── rootfs
    ├── bin
    ├── dev
    ├── etc
    ├── home
    ├── lib
    ├── media
    ├── mnt
    ├── opt
    ├── proc
    ├── root
    ├── run
    ├── sbin
    ├── srv
    ├── sys
    ├── tmp
    ├── usr
    └── var

config.json 内容如下:

{
	"ociVersion": "1.2.0",
	"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"
		],
		"cwd": "/",
		"capabilities": {
			"bounding": [
				"CAP_AUDIT_WRITE",
				"CAP_KILL",
				"CAP_NET_BIND_SERVICE"
			],
			"effective": [
				"CAP_AUDIT_WRITE",
				"CAP_KILL",
				"CAP_NET_BIND_SERVICE"
			],
			"permitted": [
				"CAP_AUDIT_WRITE",
				"CAP_KILL",
				"CAP_NET_BIND_SERVICE"
			],
			"ambient": [
				"CAP_AUDIT_WRITE",
				"CAP_KILL",
				"CAP_NET_BIND_SERVICE"
			]
		},
		"rlimits": [
			{
				"type": "RLIMIT_NOFILE",
				"hard": 1024,
				"soft": 1024
			}
		],
		"noNewPrivileges": true
	},
	"root": {
		"path": "rootfs", // 指定根文件系统位置,当前目录下的 rootfs
		"readonly": false
	},
	"hostname": "oci-demo",
	"mounts": [  // 指定文件系统的挂载点
		{
			"destination": "/proc",
			"type": "proc",
			"source": "proc"
		},
		{
			"destination": "/dev",
			"type": "tmpfs",
			"source": "tmpfs",
			"options": [
				"nosuid",
				"strictatime",
				"mode=755",
				"size=65536k"
			]
		},
		{
			"destination": "/dev/pts",
			"type": "devpts",
			"source": "devpts",
			"options": [
				"nosuid",
				"noexec",
				"newinstance",
				"ptmxmode=0666",
				"mode=0620",
				"gid=5"
			]
		},
		{
			"destination": "/dev/shm",
			"type": "tmpfs",
			"source": "shm",
			"options": [
				"nosuid",
				"noexec",
				"nodev",
				"mode=1777",
				"size=65536k"
			]
		},
		{
			"destination": "/dev/mqueue",
			"type": "mqueue",
			"source": "mqueue",
			"options": [
				"nosuid",
				"noexec",
				"nodev"
			]
		},
		{
			"destination": "/sys",
			"type": "sysfs",
			"source": "sysfs",
			"options": [
				"nosuid",
				"noexec",
				"nodev",
				"ro"
			]
		},
		{
			"destination": "/sys/fs/cgroup",
			"type": "cgroup",
			"source": "cgroup",
			"options": [
				"nosuid",
				"noexec",
				"nodev",
				"relatime",
				"ro"
			]
		}
	],
	"linux": {
		"resources": {
			"devices": [
				{
					"allow": false,
					"access": "rwm"
				}
			]
		},
		"namespaces": [
			{ "type": "pid" },
			{ "type": "network" },
			{ "type": "ipc" },
			{ "type": "uts" },
			{ "type": "mount" },
			{ "type": "cgroup" }
		],
		"maskedPaths": [
			"/proc/acpi",
			"/proc/asound",
			"/proc/kcore",
			"/proc/keys",
			"/proc/latency_stats",
			"/proc/timer_list",
			"/proc/timer_stats",
			"/proc/sched_debug",
			"/sys/firmware",
			"/proc/scsi"
		],
		"readonlyPaths": [
			"/proc/bus",
			"/proc/fs",
			"/proc/irq",
			"/proc/sys",
			"/proc/sysrq-trigger"
		]
	}
}

Runtime Spec

Configure

  • 标准规定容器运行时必须理解 config.json,典型内容如下:
{
  "process": {
    "args": ["/bin/bash"] // 启动哪个 Linux process
  },
  "root": {
    "path": "rootfs"  // process 使用哪个根文件系统
  },
  "linux": {  // 给 process 设置哪些 namespace
    "namespaces": [
      {"type": "pid"},
      {"type": "network"},
      {"type": "mount"}
    ]
  }
}
  • 从配置文件可以看出,OCI 的理解中 Container = Process + Isolation,至于使用什么方式、什么技术来隔离进程使其具备容器的特征,完全是具体运行时实现考虑的事情(runc、kata 等)

Execution environment

Lifecycle

生命周期

  • 生命周期描述了容器从创建到最终停止退出的整个时间线,OCI 兼容运行时方案必须实现以下生命周期中定义的动作。

create

  • 运行时的创建命令(create)需要关联到bundle路径和唯一的容器ID。也就是说,创建命令的核心动作必须包含创建容器ID的动作和保存bundle路径的动作。

environment

  • 运行时环境在启动容器时必须按照配置文件 config.json 中配置好环境,包括环境变量,挂载点等等。在创建资源的时不允许用户定义的程序运行。当环境好之后,所有对 config.json 文件的更新都不能影响容器。举例如下:
  • config.json 中配置环境变量和挂载点:
"env": [
	"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
	"TERM=xterm"
],

"mounts": [
	{
		"destination": "/dev/pts",
		"type": "devpts",
		"source": "devpts",
		"options": [
			"nosuid",
			"noexec",
			"newinstance",
			"ptmxmode=0666",
			"mode=0620",
			"gid=5"
		]
	}
]
  • 在运行时启动容器之前,必须按照此配置在容器内部准备好这些资源,容器内部看到的信息如下:
/ # env
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

/ # mount |grep pts
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666)

start

  • 容器启动操作,核心实现视具体容器方案而定,但必须关联一个容器 ID。

delete

  • 容器启动操作,核心实现视具体容器方案而定,但必须关联一个容器I D。

hooks

  • OCI 兼容的运行时还必须设计容器生命周期中各个阶段的回调函数,供上层使用者注册自己的程序,包括如下 hook:
  1. Prestart:这个 hook 在 start 命令之后,用户定义的程序执行之前调用,比如在 linux 平台上,对于 runc 运行时方案,prestart hook 在容器命令空间之后被执行,这样 hook 可以有机会定制即将创建的容器。
  2. CreateRuntime:这个 hook 需要作为 create 操作的一部分在执行 create 操作时被调用。它的执行时间介于环境变量配置之后,改变当前所有进程/线程工作目录之前(pivot_root)
  3. CreateContainer:同CreateRuntime hook执行阶段相同,但必须在它之后。
  4. StartContainer:在用户定义的程序执行之前执行。
  5. Poststart:在用户定义的程序执行之后之前。
  6. Poststop:在容器删除的核心操作之后,删除动作返回之前执行。

状态查询

  • 运行时规范规定了容器必须包含的字段如下:
  1. ociVersion:描述容器遵循的 OCI 规范版本
  2. id:描述容器 ID,用于区分同主机上的容器。对于跨主机的容器,id字段可以相同
  3. status:容器的生命周期状态,可以是 creating,created,running,stopped,这些状态在生命周期中定义
  4. pid:容器进程 ID,在 linux 平台上,进程 ID 是必选的。它是容器内部运行的应用程序对应进程的ID
  5. bundle:容器的 bundle 目录,bundle 目录主要存放容器运行时的配置文件和容器的根文件系统
  6. annotations: 字段是可选的,存放容器的注释信息。
    容器的状态信息除以上字段以外,具体的 OCI 兼容容器方案还可以定义其它字段,视具体的实现而定。
  • 容器的状态可以通过 state 操作来查询,runc 的查询命令如下:
[root@PC-Hyman ~]# runc state 1234
{
  "ociVersion": "1.0.2-dev",
  "id": "1234",
  "pid": 267441,
  "status": "created",
  "bundle": "/home/ubuntuVM/containerd/demo/runc",
  "rootfs": "/home/ubuntuVM/containerd/demo/runc/busyboxfs",
  "created": "2020-09-24T07:58:46.28138205Z",
  "owner": ""
}

容器操作

create

create <container-id> <path-to-bundle>

  • 容器创建操作需要指定容器ID和bundle目录,对于runc命令来说,bundle目录是可选的,如果不指定,默认去当前目录下查找。/home/ubuntuVM/containerd/demo/runc目录下放置了一个busybox容器的配置文件config.json以及这个容器的根文件系统
[root@PC-Hyman runc]# ls /home/ubuntuVM/containerd/demo/runc
busyboxfs config.json
  • 指定容器的id和bundle目录,创建该容器:
[root@PC-Hyman demo]# pwd
/home/ubuntuVM/containerd/demo
[root@PC-Hyman demo]# ls
containerd-1.4.1 main main.go runc tools v1.4.1.zip
  • 传入容器ID创建容器,由于当前目录没有配置文件,报错
[root@PC-Hyman demo]# runc create 12345
ERRO[0000] JSON specification file config.json not found
  • 指定bundle目录创建容器
[root@PC-Hyman demo]# runc create 12345 -b /home/ubuntuVM/containerd/demo/runc
[root@PC-Hyman demo]# runc list
ID PID STATUS BUNDLE CREATED OWNER
12345 262814 created /home/ubuntuVM/containerd/demo/runc 2020-09-24T02:23:23.388415943Z root

start

start <container-id>

  • 容器创建之后,其状态时 created,通过 start 操作可以启动容器,启动后容器状态变为 running,如果容器内部启动的应用程序是需要长久运行的,比如交互式程序sh,那么容器会一直运行直到被kill掉。如果内部应用程序是运行一段时间就结束的普通程序,那容器状态也会随之变为 stopped。

kill

kill <container-id> <signal>

  • 对于一个一直运行的容器,可以通过kill命令向它内部的应用发信号,通知其结束

delete

delete <container-id>

  • 删除一个容器是创建容器的逆操作,它将容器的 ID 和配置信息容 runc 得管理中删除

总结

  • 总体来看,OCI的运行时规范比较简单,它将容器运行过程中一些状态的定义,动作的执行规范化,以便于兼容不同的运行时实现方案。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐