阶段二 · Docker 工程实践

OCI / runc / containerd / CRI-O / CRI 全景

前置回顾

第 05 篇初步建立了 docker CLI → dockerd → containerd → runc → kernel 这条调用链,讲解了 OCI 基本概念.
第 08 篇末尾提到 Harbor 本身用 Compose 部署,K8s 可以绕过 dockerd 直接对接 containerd.

本篇把运行时这一层从概览推进到深度:

  • OCI 规范到底怎么写
  • runc 的源码级工作原理
  • containerd 和 CRI-O 的架构差异
  • CRI 标准的来龙去脉

回顾:第 05 篇的调用链

先把第 05 篇那张图浓缩成两句话:

┌───────────────────────────────┐
│ Docker 调用链 │
├───────────────────────────────┤
│ • direction LR │
│ • docker CLI │
│ • containerd
(高级运行时) │
│ • runc
(低级运行时) │
│ • Linux Kernel │
└───────────────────────────────┘
┌────────────────────────────────┐
│ K8s 调用链 (去掉产品层) │
├────────────────────────────────┤
│ • direction LR │
│ • kubelet │
│ • runc / crun
(低级运行时) │
│ • Linux Kernel │
└────────────────────────────────┘
层次 做什么 代表项目 遵循什么标准
高级运行时 镜像拉取/存储,容器生命周期管理,对接编排系统 containerd, CRI-O CRI(对接 K8s),OCI Image Specification(镜像格式规范)
低级运行时 读一个 JSON 配置,创建 Namespace/Cgroup/rootfs,启动进程 runc, crun, youki, kata, gVisor OCI Runtime Specification(容器运行时规范)

OCI 规范深度剖析

第 05 篇把 OCI 比作"USB 标准"--任何符合标准的镜像都能被任何符合标准的运行时运行.现在我们把 USB 拆开看接口定义.

Image Spec:一个镜像到底由哪些文件构成

在 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:一个容器应该怎么启动

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 容器生命周期的钩子(创建前/创建后/启动前/启动后/停止前/停止后执行的脚本) 扩展机制

config.json 实例解读

下面是一个真实(但精简过)的 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 是那个照着说明书搭乐高的人.

低级运行时(OCI Runtime)

runc 的工作原理

runc 的定位极度专注:读 config.json → 调系统调用 → 创建容器进程 → 退出.它没有常驻进程,没有 API 服务器,没有网络管理,没有任何"花活".
它的全部工作流程可以拆成以下阶段:

阶段一: Docker准备(在宿主机命名空间中)

# 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 → 确定容器进程允许/禁止哪些系统调用

阶段二:fork 子进程 + 创建 Namespace

# 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 退出,containerd 接管

runc 父进程通过 pipe 收到子进程的就绪信号 → 记下容器进程的宿主机 PID
→ 把这个 PID 报告给 containerd
→ runc 自身退出(它的任务完成了)
→ containerd 持续监控这个 PID:
• 进程还在运行?→ 容器状态 = running
• 进程退出了? → 容器状态 = exited,记录退出码
• 需要暂停? → 通过 Cgroup freezer 冻结进程
• 需要恢复? → 解冻
• 需要删除? → 杀进程 + 清理 Cgroup + umount 所有挂载

替代品:不止 runc 一个选择

OCI Runtime Spec 用 JSON 定义了"什么是容器",任何能读懂这个 JSON 的程序都可以当低级运行时.目前有五个主流选择:

crun - runc 的 C 语言重写版

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 的行为不变

youki - runc 的 Rust 重写版

# 用 Rust 重写的 OCI Runtime
# 核心卖点:Rust 的内存安全保证 → 没有 C/Go 中常见的缓冲区溢出/空指针

# 状态:CNCF Sandbox 项目,仍在快速迭代
# 生产就绪程度:runc > crun > youki
# 值得关注的方向:安全和正确性优先于极致性能的场景

kata-containers - 每个容器有自己的微内核

# 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 - 在用户态模拟 Linux 内核

# 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
安全性 中(共享内核) 中+ 高(独立内核) 中高(系统调用过滤)
成熟度 最成熟 成熟 成长中 成熟 成熟
适用场景 默认选择 大规模集群 安全优先 多租户/强隔离 安全与性能折中

高级运行时(Container Runtime)

containerd 深入

第 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 对象持有:

  • 一个指向 OCI Runtime(如 runc)的引用
  • 容器进程的宿主机 PID
  • 容器的 stdin/stdout/stderr IO 流
  • 退出码(当容器退出后)

CRI-O:K8s 专用运行时

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 vs CRI-O

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)

CRI:Kubernetes 的容器运行时接口

为什么要 CRI

回到第 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 接口的返回值格式正确就行

CRI 在 K8s 架构中的位置

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 → 定义容器配置│
│ │
│ 任何人实现这两份规范, │
│ 就能在容器生态中互操作 │
└─────────────────────────┘

小结 - Docker 完结

Docker·完结

从 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),各自独立可替换

动手练习

  1. nerdctl(containerd 的 Docker 兼容 CLI)运行一个容器,不经过 dockerd,感受直接对接 containerd 的体验:nerdctl run --rm -it alpine sh
  2. 查看 containerd 的 namespace 列表:ctr namespaces ls,然后用 ctr -n default containers ls 查看默认 namespace 下的容器
  3. 如果你用的是 runc,找一个运行的容器,用 runc list 查看所有容器状态,用 runc state 查看单个容器的 Namespace/Cgroup 信息
  4. 在同一台机器上分别用 runc 和 crun 启动同一个容器镜像,用 time 对比启动耗时(如果不想装 crun,至少用 runc 体验一下)
  5. 画出你自己的"容器运行时全景图"--只凭记忆,把从 kubelet 到 Linux kernel 的所有组件和它们之间的关系画出来,然后和本篇的地图对照
  6. 思考题:为什么 K8s 要定义 CRI 这个标准,而不是直接调用 containerd 的 API?如果 K8s 和 containerd 是同一个公司维护的,还需要 CRI 吗?