概述

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

OCI Image Spec

  • OCI 要求容器必须按照 container bundle 即 rootfs + 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

  • Image Spec 提出镜像簇 —— Container Bundle 的概念,规定由两个组件构成:
  1. config.json: 描述容器运行时预期的配置,所有容器必须满足 config.json 配置中包含的标准,不同运行时容器内部实现有所不同,但必须满足 config.json 的预期
  2. root filesystem: 描述容器运行所依赖镜像根文件系统,路径由 config.json 的 root->path 字段指定,不同运行时使用的进程容器化技术有所不同,但都需要按照 config.json 的预期把一个 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

  • 容器运行时必须让进程运行过程中满足配置文件声明的执行环境,典型的执行环境相关字段如:
{
  "process": {
		"env": [  // 进程运行时使用的环境变量
			"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
			"TERM=xterm"
		],
         "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"
			]
		},
         ...
}
  • 容器运行时必须保证执行环境在容器整个生命周期内都不变化,容器内看到的信息如下:
# 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)

Lifecycle

  • 生命周期描述容器进程从创建到最终停止过程中各个事件的预期行为:
  1. 容器运行时暴露 create 接口用于创建一个容器,create 依赖两个输入信息,一是 bundle 的位置,二是容器 ID。所有 OCI 运行时创建容器的命令如下:
create <container-id> <path-to-bundle>
  1. 容器运行时的环境必须满足 config.json 的声明预期,如果运行时无法创建这样的环境,必须 generate an error
  2. prestart hook 在容器运行时准备好环境之后被运行,它被看做 create 的一个步骤,如果 prestart 执行错误,必须 generate an error 并跳转到 step 12,prestart 在 CreateRuntime hook 之前运行
  3. CreateRuntime hook 在容器运行时准备好环境之后被运行,同样被看做 create 的一个步骤,执行错误后必须 generate an error 并跳转到 step 12
  4. createContainer hook 同 CreateRuntime hook 一样,被看做 create 的一个步骤,执行错误后必须 generate an error 并跳转到 step 12
  5. start 操作启动一个容器,依赖一个输入信息即容器 ID。所有 OCI 运行时启动容器的命令如下:
start <container-id>
  1. startContainer hook 在进程启动前被执行,执行错误后必须 generate an error 并跳转到 step 12
  2. 容器运行时按照 config.json 中配置的预期属性启动进程
  3. poststart hook 在进程启动后被执行,执行错误后必须 generate an error 并跳转到 step 12
  4. 容器进程终止,退出原因可能多种,异常退出,crash 或者被 kill,所有 OCI 运行时 kill 容器的命令如下:
kill <container-id> <signal>
  1. delete 命令在容器进程终止后被执行,所有 OCI 运行时 delete 容器命令如下:
delete <container-id>
  1. 容器运行时在终止后必须回退之前的所有操作
  2. poststop hook 在容器进程终止后被调用,如果失败,运行时必须 log a warning

State Query

  • 运行时规范规定了容器必须包含的字段如下,以便输出容器状态:
  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 兼容容器方案还可以定义其它字段,视具体的实现而定,所有 OCI 运行时的状态查询命令如下:
state <container-id>

hooks

  • 在 linux 平台上,配置文件支持 hook 字段的配置,方便用户在容器运行时的生命周期插入自己的代码逻辑,hook 点的执行时间总结如下:
NameNamespacewhen
prestart (Deprecated)runtime容器创建期间
createRuntimeruntime容器创建期间
createContainercontainer容器创建期间
startContainercontainer在启动容器命令后,进程运行之前
poststartruntime在进程运行后,进程结束之前
poststopruntime在容器 delete 动作期间,容器真正删除后,但 delete 动作返回前
  • config.json 中典型的 hooks 字段示例如下:
"hooks": {
    "prestart": [
        {
            "path": "/usr/bin/fix-mounts",
            "args": ["fix-mounts", "arg1", "arg2"],
            "env":  [ "key1=value1"]
        },
        {
            "path": "/usr/bin/setup-network"
        }
    ],
    "createRuntime": [
        {
            "path": "/usr/bin/fix-mounts",
            "args": ["fix-mounts", "arg1", "arg2"],
            "env":  [ "key1=value1"]
        },
        {
            "path": "/usr/bin/setup-network"
        }
    ],
    "createContainer": [
        {
            "path": "/usr/bin/mount-hook",
            "args": ["-mount", "arg1", "arg2"],
            "env":  [ "key1=value1"]
        }
    ],
    "startContainer": [
        {
            "path": "/usr/bin/refresh-ldcache"
        }
    ],
    "poststart": [
        {
            "path": "/usr/bin/notify-start",
            "timeout": 5
        }
    ],
    "poststop": [
        {
            "path": "/usr/sbin/cleanup.sh",
            "args": ["cleanup.sh", "-f"]
        }
    ]
}

实验

  • TBD

总结

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

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

更多推荐