前置回顾
第 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 } , "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" } ] , "resources" : { "cpu" : { "shares" : 1024 , "quota" : 50000 , "period" : 100000 } , "memory" : { "limit" : 536870912 , "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),各自独立可替换
动手练习
用 nerdctl(containerd 的 Docker 兼容 CLI)运行一个容器,不经过 dockerd,感受直接对接 containerd 的体验:nerdctl run --rm -it alpine sh
查看 containerd 的 namespace 列表:ctr namespaces ls,然后用 ctr -n default containers ls 查看默认 namespace 下的容器
如果你用的是 runc,找一个运行的容器,用 runc list 查看所有容器状态,用 runc state 查看单个容器的 Namespace/Cgroup 信息
在同一台机器上分别用 runc 和 crun 启动同一个容器镜像,用 time 对比启动耗时(如果不想装 crun,至少用 runc 体验一下)
画出你自己的"容器运行时全景图"--只凭记忆,把从 kubelet 到 Linux kernel 的所有组件和它们之间的关系画出来,然后和本篇的地图对照
思考题:为什么 K8s 要定义 CRI 这个标准,而不是直接调用 containerd 的 API?如果 K8s 和 containerd 是同一个公司维护的,还需要 CRI 吗?