容器运行时全景

OCI(Open Container Initiative,开放容器倡议)/ 低级运行时 / 高级运行时 / CRI — 容器生态的完整"发动机舱"

阶段一 · 09(完结篇) · 返回阶段大纲

前置回顾

第 05 篇初步建立了 docker CLI → dockerd → containerd → runc → kernel 这条调用链,讲解了 OCI 基本概念。第 08 篇末尾提到 Harbor 本身用 Compose 部署,K8s 可以绕过 dockerd 直接对接 containerd。本篇把运行时这一层从概览推进到深度:OCI 规范到底怎么写、runc 的源码级工作原理、containerd 和 CRI-O 的架构差异、CRI 标准的来龙去脉。读完这篇,你对容器生态的认知将完成从"会用"到"理解设计"的跃迁。

回顾:第 05 篇的调用链,今天我们往深处走

先把第 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-OCRI(对接 K8s)、OCI Image Specification(镜像格式规范)
低级运行时读一个 JSON 配置,创建 Namespace/Cgroup/rootfs,启动进程runc, crun, youki, kata, gVisorOCI 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.resourcesCPU 配额、内存限制、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 服务器,没有网络管理,没有任何"花活"。它的全部工作流程可以拆成以下阶段:

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

# 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 文件)

五大运行时对比

runccrunyoukikatagVisor
语言GoCRustGo + RustGo
隔离级别进程级进程级进程级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 对象持有:

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

containerdCRI-O
主导方Docker → CNCFRed Hat
设计目标通用容器运行时(Docker + K8s 都能用)专注 K8s CRI 接口,其他不管
代码复杂度较高(要兼容 Docker 的历史功能)较低(只有一个目标:服务 K8s)
与 Docker 兼容是(dockerd 通过 containerd 创建容器)否(不能代替 Docker 下的 containerd)
OCI Runtime 支持runc / crun / kata / gVisorrunc / 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 → 定义容器配置│
          │                         │
          │  任何人实现这两份规范,   │
          │  就能在容器生态中互操作   │
          └─────────────────────────┘

小结 — 阶段一完结

阶段一·完结

从 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 接管监控
crunrunc 的 C 语言重写版,更快、更省内存——大规模集群下累计收益显著
kata / gVisor比 runc 更强的隔离——kata 用独立微内核 VM,gVisor 用用户态拦截系统调用
containerd 内部Images(镜像)+ Containers(容器)+ Snapshots(存储)+ Tasks(运行实例,持 OCI Runtime 引用)
CRI-OK8s 专用,只做 CRI 需要的事——拉镜像、管容器,不做 Docker 独有的功能
CRIK8s 定义的两组 gRPC 服务(RuntimeService + ImageService),任何高级运行时实现了就能接入
CRI + CNI + CSIK8s 的三接口设计:容器怎么造(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 <container-id> 查看单个容器的 Namespace/Cgroup 信息
  4. 在同一台机器上分别用 runc 和 crun 启动同一个容器镜像,用 time 对比启动耗时(如果不想装 crun,至少用 runc 体验一下)
  5. 画出你自己的"容器运行时全景图"——只凭记忆,把从 kubelet 到 Linux kernel 的所有组件和它们之间的关系画出来,然后和本篇的地图对照
  6. 思考题:为什么 K8s 要定义 CRI 这个标准,而不是直接调用 containerd 的 API?如果 K8s 和 containerd 是同一个公司维护的,还需要 CRI 吗?