基于containerd的容器运行时控制面架构设计 - 第4节
4.1 控制面核心概念全景
containerd 的控制面围绕一组精确定义的核心抽象构建。这些抽象共同构成了容器生命周期的管理骨架,使 containerd 能够在不依赖完整 Docker Engine 的情况下,提供生产级的容器运行时能力。理解这七个核心概念——Namespace、Container、Task、Image、Snapshot、Content、Lease——是掌握 containerd 架构设计的起点。
下表总结了每个核心概念在控制面中的角色定位:
| 核心概念 | gRPC API 服务 | 职责描述 | 生命周期 |
|---|---|---|---|
| Namespace | NamespaceService | 资源隔离与多租户边界 | 持久化,需显式创建/删除 |
| Container | ContainerService | 容器元数据与配置的持久化对象 | 持久化,与 Task 解耦 |
| Task | TaskService | 容器进程的实际运行时实例 | 运行时态,与进程绑定 |
| Image | ImageService | 容器镜像元数据管理 | 持久化,基于 Content 引用 |
| Snapshot | SnapshotService | 容器文件系统快照与分层管理 | 持久化,基于 Content 构建 |
| Content | ContentService | 不可变内容寻址存储(blob) | 持久化,引用计数管理 |
| Lease | LeaseService | 资源引用计数与垃圾回收保护 | 临时性,TTL 或显式释放 |
4.2 Namespace:多租户隔离边界
containerd 的 Namespace(注意与 Linux 内核 Namespace 区分)是控制面的顶层隔离单元。它并非内核级隔离,而是 API 层面的资源分组边界。每个 Namespace 拥有独立的资源视图,包括 Container、Image、Snapshot 等对象的集合。
- 每个 gRPC 请求可以指定 Namespace,未指定时使用
default。 - 资源名称在同一个 Namespace 内唯一,跨 Namespace 可重名。
- Namespace 本身需要显式创建,删除时会级联清理其下所有资源(受 Lease 保护者除外)。
- 典型使用场景:Kubernetes 的 kubelet 为每个节点使用独立 Namespace;多租户平台为每个租户分配独立 Namespace。
# 通过 ctr 命令行工具操作 Namespace
ctr namespace create tenant-alpha
ctr namespace ls
ctr --namespace tenant-alpha images pull docker.io/library/nginx:alpine
4.3 Container 与 Task:元数据与进程的解耦
这是 containerd 架构中最具设计巧思的一对概念。传统容器运行时将"容器"视为一个包含元数据和运行进程的单一对象,而 containerd 将其拆分为两个独立实体:
| 维度 | Container | Task |
|---|---|---|
| 本质 | 持久化的元数据对象 | 运行时进程实例 |
| 生命周期 | 创建后持久存在,直到被删除 | 与进程同生共死,可多次创建/销毁 |
| 包含内容 | RootFS 引用、挂载点、标签、扩展属性 | 进程 PID、标准流、退出状态、资源限制 |
| 典型操作 | Create、Update、Delete |
Start、Kill、Exec、Wait、Pause |
| 关系 | 一个 Container 可以对应 0 或 1 个活跃 Task | Task 必须关联到一个已存在的 Container |
ctr task exec 进入已停止容器)和故障恢复中极为有用。
// Go 伪代码:Container 与 Task 的创建流程
// 1. 创建 Container(仅元数据)
container, err := client.NewContainer(ctx, "my-app",
containerd.WithImage(image),
containerd.WithNewSnapshot("my-app-snap", image),
containerd.WithNewSpec(oci.WithImageConfig(image)))
// 2. 基于 Container 创建 Task(启动进程)
task, err := container.NewTask(ctx, cio.NewCreator(cio.WithStdio))
// 3. 启动 Task
err = task.Start(ctx)
// 4. 停止后,可基于同一 Container 重新创建 Task
task.Kill(ctx, syscall.SIGTERM)
task.Wait(ctx)
task.Delete(ctx)
// 重新创建
newTask, err := container.NewTask(ctx, cio.NewCreator(cio.WithStdio))
newTask.Start(ctx)
4.4 Image:镜像元数据管理
Image 是容器镜像在 containerd 控制面的抽象表示。它不存储镜像的层数据(那属于 Content 的职责),而是维护镜像的元数据信息:
- 名称与标签: 如
docker.io/library/nginx:alpine,支持多标签指向同一镜像。 - 目标描述符: 指向 Manifest 或 Index 的 Content 地址(Digest)。
- 平台信息: 对于多架构镜像,记录支持的 os/arch 列表。
- 镜像配置: 包含 CMD、ENTRYPOINT、Env、ExposedPorts 等 OCI 配置。
# 查看镜像元数据
ctr image list
ctr image info docker.io/library/nginx:alpine
# 输出示例(精简)
{
"name": "docker.io/library/nginx:alpine",
"target": {
"digest": "sha256:abc123...",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 1572
},
"labels": {
"io.containerd.image.created": "2024-01-15T10:30:00Z"
}
}
4.5 Content 与 Snapshot:存储层的双支柱
这两个概念共同构成了 containerd 的存储架构基础,分别对应 不可变内容寻址存储 和 可变文件系统快照。
4.5.1 Content:不可变 Blob 存储
Content 是 containerd 的底层存储抽象,以内容寻址方式管理所有不可变数据块(Blob)。每个 Blob 通过其 Digest(通常是 SHA256 哈希)唯一标识。
| 属性 | 说明 |
|---|---|
| 寻址方式 | Digest(sha256:<hex>),内容不变则地址不变 |
| 存储内容 | 镜像层压缩包(tar.gz)、Manifest、Config JSON、索引文件 |
| 不可变性 | 写入后不可修改,只能删除 |
| 引用计数 | 通过 Lease 机制管理,防止 GC 误删 |
| 后端实现 | 默认本地文件系统(/var/lib/containerd/io.containerd.content.v1.content) |
4.5.2 Snapshot:文件系统快照层
Snapshot 负责管理容器的文件系统视图。它基于 Content 中的镜像层数据,通过快照(snapshot)机制构建出容器的 RootFS。containerd 内置了多种快照器(snapshotter)实现:
- overlayfs: 默认推荐,使用 Linux 内核 OverlayFS,性能最优。
- native: 不使用联合文件系统,每个层独立目录,适合调试。
- devmapper: 基于 Device Mapper 的块设备快照,适合大规模场景。
- fuse-overlayfs: 用户态 OverlayFS,用于无 root 权限环境。
// 快照生命周期示例
// 1. 从镜像创建快照(准备 RootFS)
snapshotter := client.SnapshotService("overlayfs")
mounts, err := snapshotter.Prepare(ctx, "my-snap-key", image.Target().Digest)
// 2. 挂载到容器
container, err := client.NewContainer(ctx, "my-app",
containerd.WithNewSnapshot("my-snap-key", image))
// 3. 提交快照变更(生成新层)
newDigest, err := snapshotter.Commit(ctx, "my-committed-snap-key", "my-snap-key")
// 4. 删除快照
snapshotter.Remove(ctx, "my-snap-key")
4.6 Lease:资源生命周期保护
Lease 是 containerd 引入的引用计数与垃圾回收保护机制。它的核心作用是:防止正在被使用的资源被垃圾回收误删。
Lease 的工作原理:
- 创建一个 Lease 对象,可设置 TTL(生存时间)或手动管理。
- 将需要保护的资源(Content Blob、Snapshot)关联到该 Lease。
- 只要 Lease 存在且未过期,关联的资源就不会被 GC 清理。
- Lease 被删除或过期后,资源引用计数归零,GC 可以安全回收。
// Go 代码:使用 Lease 保护资源
ctx := context.Background()
// 1. 创建 Lease,设置 1 小时 TTL
lease, err := client.LeasesService().Create(ctx,
leases.WithTTL(1 * time.Hour),
leases.WithLabels(map[string]string{"owner": "batch-job-123"}))
// 2. 拉取镜像时关联 Lease
image, err := client.Pull(ctx, "docker.io/library/redis:7",
containerd.WithPullLease(lease))
// 3. 创建容器时自动关联 Lease
container, err := client.NewContainer(ctx, "redis-cache",
containerd.WithImage(image),
containerd.WithNewSnapshot("redis-snap", image),
containerd.WithLease(lease))
// 4. 任务完成后释放 Lease(关联资源将被 GC 清理)
client.LeasesService().Delete(ctx, lease)
| 场景 | 无 Lease 的问题 | 有 Lease 的解决方案 |
|---|---|---|
| 镜像拉取中发生 GC | 层数据被部分删除,拉取失败 | Lease 保护拉取中的 Blob |
| 容器运行中 GC 扫描 | RootFS 快照被误删,容器崩溃 | Lease 保护活跃容器的 Snapshot |
| 批量任务临时镜像 | 任务完成后镜像残留,占用磁盘 | Lease 过期后自动清理 |
| 多租户资源隔离 | 租户 A 的镜像被租户 B 的 GC 误删 | 每个租户独立 Lease 空间 |
4.7 核心概念协作流程
理解每个概念单独的角色后,最关键的是掌握它们如何协作完成一个容器的完整生命周期。以下是一个典型的容器创建流程,展示了七个核心概念的交互顺序:
- Namespace 确定: 客户端在 gRPC 请求中指定 Namespace,确定资源隔离边界。
- Content 拉取: 从镜像仓库拉取镜像层 Blob,存储到 Content 服务,通过 Lease 保护拉取中的 Blob。
- Image 注册: 拉取完成后,创建 Image 对象,记录 Manifest 和 Config 的 Content 引用。
- Snapshot 准备: 基于 Image 的层信息,调用 Snapshotter 的
Prepare方法,挂载 RootFS。 - Container 创建: 创建 Container 对象,关联 Snapshot 和 Image 元数据。
- Task 创建与启动: 基于 Container 创建 Task,实际启动容器进程(runc 或其他 runtime)。
- Lease 管理: 整个过程中,Lease 保护所有活跃资源,防止 GC 误删。