OCI(Open Container Initiative,开放容器倡议)/ 低级运行时 / 高级运行时 / CRI — 容器生态的完整"发动机舱"
前置回顾
第 05 篇初步建立了 docker CLI → dockerd → containerd → runc → kernel 这条调用链,讲解了 OCI 基本概念。第 08 篇末尾提到 Harbor 本身用 Compose 部署,K8s 可以绕过 dockerd 直接对接 containerd。本篇把运行时这一层从概览推进到深度:OCI 规范到底怎么写、runc 的源码级工作原理、containerd 和 CRI-O 的架构差异、CRI 标准的来龙去脉。读完这篇,你对容器生态的认知将完成从"会用"到"理解设计"的跃迁。
先把第 05 篇那张图浓缩成两句话,然后我们向下钻取:
docker CLI → dockerd → containerd → runc → Linux Kernel
│ │ │
│ │ └── 低级运行时(OCI Runtime)
│ └── 高级运行时(Container Runtime)
└── 产品层(Docker 特有)
# 去掉产品层后,K8s 直接对接高级运行时:
kubelet → CRI → containerd/CRI-O → runc/crun → Linux Kernel
│ │
│ └── 低级运行时
└── 高级运行时
| 层次 | 做什么 | 代表项目 | 遵循什么标准 |
|---|---|---|---|
| 高级运行时 | 镜像拉取/存储、容器生命周期管理、对接编排系统 | containerd, CRI-O | CRI(对接 K8s)、OCI Image Specification(镜像格式规范) |
| 低级运行时 | 读一个 JSON 配置,创建 Namespace/Cgroup/rootfs,启动进程 | runc, crun, youki, kata, gVisor | OCI Runtime Specification(容器运行时规范) |
第 05 篇把 OCI 比作"USB 标准"——任何符合标准的镜像都能被任何符合标准的运行时运行。现在我们把 USB 拆开看接口定义。
你在 Docker Hub 上 pull 一个镜像时,实际下载的是这样一组文件:
nginx:1.25 镜像在 Registry 上的存储结构:
nginx/
└─ 1.25/
├── index.json ← 镜像的"目录":列出所有可用的 manifest
│ {
│ "manifests": [
│ {
│ "mediaType": "application/vnd.oci.image.manifest.v1+json",
│ "digest": "sha256:abc123...",
│ "platform": { "architecture": "amd64", "os": "linux" }
│ },
│ {
│ "mediaType": "application/vnd.oci.image.manifest.v1+json",
│ "digest": "sha256:def456...",
│ "platform": { "architecture": "arm64", "os": "linux" }
│ }
│ ]
│ }
│ ← 一个 tag 下可以有多个 manifest(amd64 一个,arm64 一个)
│ docker pull 时自动选择匹配你机器架构的那个
│
├── manifest.json ← 一个具体架构的"配置单"
│ {
│ "config": {
│ "mediaType": "application/vnd.oci.image.config.v1+json",
│ "digest": "sha256:cfg123...",
│ "size": 2048
│ },
│ "layers": [ ← 这一架构下的所有层
│ { "digest": "sha256:layer001...", "size": 75123456 },
│ { "digest": "sha256:layer002...", "size": 1234567 },
│ { "digest": "sha256:layer003...", "size": 456789 }
│ ]
│ }
│
├── config.json ← 镜像元数据
│ {
│ "created": "2024-01-01T00:00:00Z",
│ "config": {
│ "Env": ["PATH=/usr/local/sbin:..."],
│ "Cmd": ["nginx", "-g", "daemon off;"],
│ "ExposedPorts": { "80/tcp": {} }
│ },
│ "rootfs": {
│ "diff_ids": ["sha256:layer001...", "sha256:layer002...", ...]
│ }
│ }
│ ← 注意:这个 config.json 是 Image Spec 的 config.json
│ 和 Runtime Spec 的 config.json 是两个不同的文件!
│
└── blobs/sha256/ ← 各层的实际数据(tar.gz 压缩包)
├── layer001... ← Layer 1(基础镜像的文件)
├── layer002... ← Layer 2
└── layer003... ← Layer 3
两个 config.json 不要搞混
OCI 生态里有两个都叫 config.json 但完全不同的文件:Image Spec 的 config.json 存在 Registry 上,记录镜像的元数据(ENV/CMD 等),是构建时生成的、不变的。Runtime Spec 的 config.json 由 containerd 动态生成,描述"怎么用这个镜像创建容器",是每次 docker run 时新生成的。前者描述"镜像是什么",后者描述"容器应该怎么跑"。
Runtime Spec 定义了一个容器的完整启动参数,输出是一个 JSON 文件。我们把它涉及的所有维度整理出来:
| 配置维度 | 干了什么 | 回顾前四篇的哪篇 |
|---|---|---|
| root | 指定 rootfs 路径(镜像所有层 + 可写层叠好后的 merged 目录) | 第 04 篇 OverlayFS |
| process | 容器启动后执行的命令(如 nginx)、工作目录、环境变量、终端设置 | 第 01 篇 fork + exec |
| linux.namespaces | 要创建哪些 Namespace,以及每个 NS 的路径(如 /proc/PID/ns/net) | 第 02 篇 Namespace |
| linux.resources | CPU 配额、内存限制、IO 权重、PID 数量上限等 | 第 03 篇 Cgroups |
| linux.capabilities | 容器进程有哪些内核能力(CAP_NET_BIND_SERVICE 等) | 安全相关 |
| linux.seccomp | 系统调用白名单/黑名单(禁止容器调某些危险系统调用) | 安全相关 |
| mounts | 除了 rootfs 之外还要挂载什么(/proc, /sys, tmpfs, bind mount 等) | 第 01 篇 mount |
| hooks | 容器生命周期的钩子(创建前/创建后/启动前/启动后/停止前/停止后执行的脚本) | 扩展机制 |
下面是一个真实(但精简过)的 OCI Runtime Spec 的 config.json,containerd 生成它,runc 读取它:
{
"ociVersion": "1.1.0",
"root": {
"path": "/var/lib/containerd/io.containerd.runtime.v2.task/default/.../rootfs",
"readonly": false
// ↑ 这就是 merged 目录,OverlayFS 把 lowerdir + upperdir 合并后的结果
},
"process": {
"terminal": false,
"user": { "uid": 101, "gid": 101 },
"args": ["nginx", "-g", "daemon off;"],
"env": ["PATH=/usr/local/sbin:/usr/local/bin:..."],
"cwd": "/",
"capabilities": {
"bounding": ["CAP_NET_BIND_SERVICE", "CAP_KILL"],
"effective": ["CAP_NET_BIND_SERVICE", "CAP_KILL"]
}
},
"linux": {
"namespaces": [
{ "type": "pid" },
{ "type": "network", "path": "/var/run/netns/cni-xxx" },
{ "type": "mount" },
{ "type": "uts" },
{ "type": "ipc" }
],
// 注:User NS 没出现 = 不隔离 UID(容器 root = 宿主机 root,rootful 模式)
"resources": {
"cpu": {
"shares": 1024,
"quota": 50000, // ← 对应 docker run --cpus=0.5
"period": 100000
},
"memory": {
"limit": 536870912, // ← 512MB
"swap": 1073741824
},
"pids": {
"limit": 100
}
},
"cgroupsPath": "/sys/fs/cgroup/system.slice/docker-abc123.scope",
"mounts": [
{ "destination": "/proc", "type": "proc", "source": "proc" },
{ "destination": "/dev/shm", "type": "tmpfs", "source": "tmpfs" },
{ "destination": "/etc/hosts", "type": "bind",
"source": "/var/lib/docker/containers/abc123/hosts" }
]
}
}
OCI Runtime Spec 的 config.json = 乐高说明书。它精确描述了最终成品(容器)应该长什么样、用哪些零件(rootfs 路径 / Namespace 类型 / Cgroup 限制值 / 挂载点),但不负责动手拼——runc 是那个照着说明书搭乐高的人。
runc 的定位极度专注:"读 config.json → 调系统调用 → 创建容器进程 → 退出"。它没有常驻进程,没有 API 服务器,没有网络管理,没有任何"花活"。它的全部工作流程可以拆成以下阶段:
# runc 启动时,它自己还在宿主机默认的 Namespace 中
# 这一步它准备"容器进程将要在什么样的环境中运行"
1. 读取 config.json → 解析出 rootfs 路径、Namespace 列表、Cgroup 限制值
2. 准备好 rootfs 挂载点(如果还没挂的话,mount -t overlay)
3. 准备好 cgroup 目录(在 /sys/fs/cgroup 下创建容器专属的子目录)
4. 创建一对 pipe(管道)——一种进程间通信机制
5. 准备 hooks(Prestart / CreateRuntime / CreateContainer 阶段的钩子脚本)
6. 读取 OCI seccomp profile → 确定容器进程允许/禁止哪些系统调用
# runc 调用 clone() / unshare() 创建新进程 + 新 Namespace
# 这个过程和你在第 02 篇手动做的 unshare 本质完全一样
runc clone() 调用的参数(Go 代码概念示意):
cmd.SysProcAttr = &unix.SysProcAttr{
Cloneflags: unix.CLONE_NEWPID | // 创建新的 PID Namespace
unix.CLONE_NEWNET | // 创建新的 Network Namespace
unix.CLONE_NEWNS | // 创建新的 Mount Namespace
unix.CLONE_NEWUTS | // 创建新的 UTS Namespace
unix.CLONE_NEWIPC, // 创建新的 IPC Namespace
}
# 子进程在这步诞生,它已经置身于全新的 Namespace 中
# 但此时它还不能直接运行用户的程序——还需要配 Cgroup、挂文件系统
# 现在 runc 的子进程已经在新的 PID/Mount/Network NS 中了
# 它在自己的 Mount NS 里做以下操作(对外部不可见):
1. pivot_root → 把 rootfs(merged 目录)变成 /(新根目录)
→ 相当于 chroot,但更彻底、更安全
→ 容器进程从此只看到 merged 目录下的内容
2. mount /proc → mount -t proc proc /proc
3. mount /sys → mount -t sysfs sys /sys
4. mount tmpfs → mount -t tmpfs tmpfs /dev/shm
5. mount bind → bind mount hosts/resolv.conf 等
6. 设置 hostname → sethostname(config.Hostname)
7. 设置 rlimit → 限制文件描述符数、内存锁等
8. exec 用户的程序 → exec("nginx", "-g", "daemon off;")
→ 用 nginx 进程替换掉当前的 runc 子进程
→ nginx 成为容器 PID NS 中的 PID 1
→ runc 父进程(宿主机上)等子进程就绪后自己退出
# 记住第 01 篇的 fork + exec:runc fork 出子进程(在新 NS 中)
# 然后子进程 exec 成用户的程序——一模一样
runc 父进程通过 pipe 收到子进程的就绪信号 → 记下容器进程的宿主机 PID
→ 把这个 PID 报告给 containerd
→ runc 自身退出(它的任务完成了)
→ containerd 持续监控这个 PID:
• 进程还在运行?→ 容器状态 = running
• 进程退出了? → 容器状态 = exited,记录退出码
• 需要暂停? → 通过 Cgroup freezer 冻结进程
• 需要恢复? → 解冻
• 需要删除? → 杀进程 + 清理 Cgroup + umount 所有挂载
OCI Runtime Spec 用 JSON 定义了"什么是容器",任何能读懂这个 JSON 的程序都可以当低级运行时。目前有五个主流选择:
runc (Go) vs crun (C)
速度:crun 比 runc 快约 20-40%(C 语言运行时更轻量,无 GC 开销)
内存:crun 内存占用约是 runc 的 1/3
fork 延迟:crun fork 一个子进程的耗时约为 runc 的一半
# 为什么 Red Hat 主导 crun?
# 因为大规模 K8s 集群里,每天可能有几十万次容器启动
# 每次启动快 20ms → 一年省下的时间可换算为大量 CPU 资源
# 切换方式(在 containerd 或 CRI-O 的配置中):
# /etc/containerd/config.toml
# [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
# runtime_type = "io.containerd.runc.v2"
# 改为:
# [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
# runtime_type = "io.containerd.runc.v2"
# binary_name = "crun"
# 对用户完全透明——docker run 和 kubectl apply 的行为不变
# 用 Rust 重写的 OCI Runtime
# 核心卖点:Rust 的内存安全保证 → 没有 C/Go 中常见的缓冲区溢出/空指针
# 状态:CNCF Sandbox 项目,仍在快速迭代
# 生产就绪程度:runc > crun > youki
# 值得关注的方向:安全和正确性优先于极致性能的场景
# kata 的隔离理念和 runc 完全不同
runc / crun:
容器 = 宿主机上的普通进程 + Namespace/Cgroup 包装
所有容器共享同一个 Linux 内核
如果 Namespace 隔离有漏洞 → 容器逃逸 → 宿主机失守
kata-containers:
每个容器运行在独立的轻量级虚拟机里
每个容器有自己的微型 Linux 内核(约 20MB)
容器之间完全隔离,即使 Namespace 被突破也影响不到宿主机
┌─ Pod A ──────────────┐ ┌─ Pod B ──────────────┐
│ ┌── VM ──────────┐ │ │ ┌── VM ──────────┐ │
│ │ mini kernel │ │ │ │ mini kernel │ │
│ │ ┌── 容器 ────┐ │ │ │ │ ┌── 容器 ────┐ │ │
│ │ │ nginx │ │ │ │ │ redis │ │ │
│ │ └───────────┘ │ │ │ │ └───────────┘ │ │
│ └────────────────┘ │ │ └────────────────┘ │
└──────────────────────┘ └──────────────────────┘
# 代价:
# • 启动比 runc 慢(需要启动 VM + guest kernel)
# • 内存开销更大(每个 Pod 多 ~20MB)
# • 适合:多租户场景、需要强隔离的安全敏感应用
# K8s 中使用 kata 的方式:
# 创建 RuntimeClass 资源,指定某些 Pod 走 kata 运行时
# 其他 Pod 继续走 runc(避免不必要的虚拟化开销)
# gVisor (Google) 的思路介于 runc 和 kata 之间
runc:容器共享宿主机内核(快,但隔离弱)
kata:每个容器独立 VM + 独立内核(隔离强,但慢)
gVisor:用 Go 写了一个"假内核"(Sentry),在用户态拦截系统调用
容器进程
│ 系统调用(如 write(), open())
▼
Sentry(用户态"内核")
│ 自己处理大部分系统调用(不经过宿主机内核!)
│ 只有少数必要调用才转发给宿主机内核
▼
宿主机内核(受限访问)
# gVisor 的优势:
# • 比 kata 启动快(不用起 VM)
# • 比 runc 安全(系统调用被 Sentry 拦截和过滤)
# • 兼容大部分应用(但不兼容所有——某些复杂的系统调用 Sentry 没实现)
# gVisor 的劣势:
# • 性能比 runc 差一截(系统调用要经过 Sentry)
# • 某些应用可能不兼容(如使用了特定系统调用或 /proc 文件)
| runc | crun | youki | kata | gVisor | |
|---|---|---|---|---|---|
| 语言 | Go | C | Rust | Go + Rust | Go |
| 隔离级别 | 进程级 | 进程级 | 进程级 | VM 级 | 用户态内核 |
| 启动速度 | 快 | 最快 | 快 | 慢 | 中等 |
| 内存开销 | 几乎零 | 几乎零 | 几乎零 | 每 Pod +20MB | 每容器 +30MB |
| 安全性 | 中(共享内核) | 中 | 中+ | 高(独立内核) | 中高(系统调用过滤) |
| 成熟度 | 最成熟 | 成熟 | 成长中 | 成熟 | 成熟 |
| 适用场景 | 默认选择 | 大规模集群 | 安全优先 | 多租户/强隔离 | 安全与性能折中 |
第 05 篇说了 containerd 管理镜像和容器生命周期。现在补充它的内部架构:
containerd 内部架构:
┌──────────────────────────────────────────────────────────┐
│ containerd │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ gRPC API Server │ │
│ │ 接收来自 dockerd / kubelet / crictl 的请求 │ │
│ └──────────────────────┬──────────────────────────┘ │
│ │ │
│ ┌──────────────────────┴──────────────────────────┐ │
│ │ Core Services │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ │
│ │ │ Images │ │Containers│ │ Snapshots │ │ │
│ │ │(镜像管理) │ │(容器管理) │ │ (存储快照) │ │ │
│ │ ├──────────┤ ├──────────┤ ├───────────────┤ │ │
│ │ │• pull │ │• create │ │• OverlayFS │ │ │
│ │ │• push │ │• start │ │• devicemapper │ │ │
│ │ │• list │ │• stop │ │• btrfs │ │ │
│ │ │• delete │ │• delete │ │• ... │ │ │
│ │ └──────────┘ └──────────┘ └───────────────┘ │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────────────────────────┐ │ │
│ │ │ Content │ │ Tasks │ │ │
│ │ │(内容寻址)│ │ (任务执行) │ │ │
│ │ │ │ │ 每个运行中的容器 = 一个 Task │ │ │
│ │ └──────────┘ │ Task 负责与 OCI Runtime 交互 │ │ │
│ │ └──────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────┴──────────┐ │
│ ▼ ▼ │
│ ┌────────────┐ ┌──────────────┐ │
│ │ BoltDB │ │ Snapshotter │ │
│ │(元数据存储) │ │ (文件系统层) │ │
│ └────────────┘ └──────────────┘ │
│ │
│ containerd 的数据目录(概念示意): │
│ /var/lib/containerd/ │
│ ├── io.containerd.content.v1.content/ ← 镜像层数据 │
│ │ └── blobs/sha256/ │
│ ├── io.containerd.snapshotter.v1.overlayfs/ ← 快照 │
│ └── io.containerd.metadata.v1.bolt/ ← 元数据 DB │
└──────────────────────────────────────────────────────────┘
关键理解:containerd 内部有一个 Task 的概念。一个 Task = 一个运行中的容器实例。Task 对象持有:
CRI-O 的全称是 CRI + OCI,即"把 K8s 的 CRI 接口翻译成 OCI Runtime 能懂的命令"。它的定位比 containerd 更窄:
CRI-O 的设计哲学:
"只做 K8s 需要的事,多的一点都不做"
• 没有 docker build(给 K8s 用,构建在其他地方做)
• 没有 docker compose(K8s 自己编排)
• 没有 docker network(K8s 用 CNI)
• 没有 docker volume(K8s 用 CSI)
• 只有:拉镜像、存镜像、创建容器、启动容器、停止容器、删除容器
代码量:containerd 约 40 万行,CRI-O 约 15 万行
→ CRI-O 更小、更简单、更聚焦
→ 但 containerd 有更广泛的生态(不仅 K8s 用,Docker 也用)
| containerd | CRI-O | |
|---|---|---|
| 主导方 | Docker → CNCF | Red Hat |
| 设计目标 | 通用容器运行时(Docker + K8s 都能用) | 专注 K8s CRI 接口,其他不管 |
| 代码复杂度 | 较高(要兼容 Docker 的历史功能) | 较低(只有一个目标:服务 K8s) |
| 与 Docker 兼容 | 是(dockerd 通过 containerd 创建容器) | 否(不能代替 Docker 下的 containerd) |
| OCI Runtime 支持 | runc / crun / kata / gVisor | runc / crun / kata / gVisor |
| 典型用户 | Docker 用户 + 大部分 K8s 集群 | OpenShift + 部分 K8s 集群 |
| CLI 工具 | ctr / nerdctl(Docker 兼容 CLI) | crictl(K8s 风格 CLI) |
回到第 05 篇的历史线:Docker 最初是唯一选择 → K8s 通过 dockershim 适配 Docker → 容器生态开始分化(rkt、containerd、CRI-O 等)→ K8s 需要一套标准接口,让任何高级运行时都能接入。
这就是 CRI(Container Runtime Interface)——它是一套 gRPC + protobuf 协议,定义了两个服务:
CRI = 两个 gRPC Service:
┌─────────────────────────────────────────────┐
│ RuntimeService │
│ │
│ • RunPodSandbox 创建 Pod 的沙箱环境 │
│ (创建 Network NS + 配置 CNI 网络) │
│ │
│ • StopPodSandbox 停止 Pod │
│ • RemovePodSandbox 删除 Pod │
│ │
│ • CreateContainer 在 Pod 内创建容器 │
│ • StartContainer 启动容器 │
│ • StopContainer 停止容器 │
│ • RemoveContainer 删除容器 │
│ │
│ • ListContainers 列出容器 │
│ • ContainerStatus 查看容器状态 │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ ImageService │
│ │
│ • PullImage 拉取镜像 │
│ • ListImages 列出本地镜像 │
│ • RemoveImage 删除镜像 │
│ • ImageStatus 查看镜像状态 │
└─────────────────────────────────────────────┘
任何高级运行时只要实现这两个 gRPC 服务,就可以被 K8s 使用。containerd 和 CRI-O 都实现了 CRI,所以它们都能直接对接 kubelet:
kubelet
│
│ 调用 CRI gRPC 接口(RunPodSandbox / CreateContainer / PullImage ...)
│
├──→ containerd(内置 CRI 插件)→ runc → kernel
│
└──→ CRI-O → runc → kernel
# 从 K8s 的视角,containerd 和 CRI-O 是完全等价的
# 它不关心底下是哪个,只要 CRI 接口的返回值格式正确就行
Kubernetes Node(一个工作节点):
┌──────────────────────────────────────────────────────────┐
│ kubelet │
│ "这个节点上的总管" │
│ 向 API Server 汇报状态,接收 Pod 调度指令 │
│ │
│ 对外:通过 CRI 管理容器 │
│ 通过 CNI 管理网络 │
│ 通过 CSI 管理存储 │
└──────────┬──────────────────┬──────────────────┬──────────┘
│ CRI │ CNI │ CSI
│ gRPC │ 可执行文件 │ gRPC
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ containerd │ │ Calico/ │ │ AWS EBS │
│ 或 CRI-O │ │ Flannel/ │ │ Driver 或 │
│ │ │ Cilium │ │ NFS Driver │
└──────┬───────┘ └──────────────┘ └──────────────┘
│ fork+exec
▼
┌──────────────┐
│ runc / crun │
│ / kata / │
│ gVisor │
└──────┬───────┘
│ clone/unshare/mount/cgroup
▼
┌──────────────┐
│ Linux Kernel │
└──────────────┘
┌─────────────┐
│ Kubernetes │ ← 编排层
└──────┬──────┘
│ CRI (gRPC)
┌─────────────────┼─────────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ containerd │ │ CRI-O │ ← 高级运行时
│ (CNCF Graduate) │ │ (CNCF Incubating)│
└────────┬─────────┘ └────────┬─────────┘
│ OCI Runtime Spec (config.json) │
└─────────────────┬──────────────────┘
│
┌─────────────────────┼──────────────────────┐
▼ ▼ ▼
┌────────────┐ ┌──────────────┐ ┌────────────────┐
│ runc │ │ crun │ │ kata-containers│ ← 低级运行时
│ (Go, 默认) │ │ (C, 最快) │ │ gVisor │
└────────────┘ └──────────────┘ └────────────────┘
│ │ │
└─────────────────────┼───────────────────────┘
│ clone / unshare / mount / cgroup / pivot_root
▼
┌──────────────────┐
│ Linux Kernel │
└──────────────────┘
┌────────────┴────────────┐
│ OCI 标准 │
│ │
│ Image Spec → 定义镜像格式│
│ Runtime Spec → 定义容器配置│
│ │
│ 任何人实现这两份规范, │
│ 就能在容器生态中互操作 │
└─────────────────────────┘
阶段一·完结
从 Linux 底层三件套(Namespace + Cgroups + OverlayFS)出发,到 Docker 镜像/容器/仓库的产品逻辑,到 Dockerfile 最佳实践和 Compose 编排,到 Harbor 企业镜像管理,最终到容器运行时的全景生态。你学了一条完整链路:内核机制 → 容器产品 → 工程实践 → 编排平台接口。
| 要点 | 一句话概括 |
|---|---|
| OCI Image Spec | 定义镜像的磁盘格式(index → manifest → config + layers),由构建工具生成,Registry 存储,运行时消费 |
| OCI Runtime Spec | 定义如何启动容器的 config.json,包含 rootfs 路径 / Namespace / Cgroup / 挂载点 / 安全策略——containerd 生成,runc 读取 |
| runc 工作流程 | 准备 → clone 创建 NS → pivot_root + mount 配环境 → exec 用户程序 → 退出,containerd 接管监控 |
| crun | runc 的 C 语言重写版,更快、更省内存——大规模集群下累计收益显著 |
| kata / gVisor | 比 runc 更强的隔离——kata 用独立微内核 VM,gVisor 用用户态拦截系统调用 |
| containerd 内部 | Images(镜像)+ Containers(容器)+ Snapshots(存储)+ Tasks(运行实例,持 OCI Runtime 引用) |
| CRI-O | K8s 专用,只做 CRI 需要的事——拉镜像、管容器,不做 Docker 独有的功能 |
| CRI | K8s 定义的两组 gRPC 服务(RuntimeService + ImageService),任何高级运行时实现了就能接入 |
| CRI + CNI + CSI | K8s 的三接口设计:容器怎么造(CRI)+ 网络怎么配(CNI)+ 数据怎么存(CSI),各自独立可替换 |
nerdctl(containerd 的 Docker 兼容 CLI)运行一个容器,不经过 dockerd,感受直接对接 containerd 的体验:nerdctl run --rm -it alpine shctr namespaces ls,然后用 ctr -n default containers ls 查看默认 namespace 下的容器runc list 查看所有容器状态,用 runc state <container-id> 查看单个容器的 Namespace/Cgroup 信息time 对比启动耗时(如果不想装 crun,至少用 runc 体验一下)