容器运行时扩展方案技术解析

基于对某容器运行时扩展项目的代码分析,现从架构层面提炼其核心技术实现,聚焦三大核心能力:运行时接入机制、容器根文件系统云端持久化、Docker-in-Docker 安全实现方案


1. 如何接入 Containerd 运行时生态

项目通过 Containerd Proxy Plugin 机制 实现与容器运行时的无缝集成,架构清晰、扩展性强。

▶ 配置层接入

  • containerd 配置中注册名为 custom-snapshotter 的代理插件,通过 Unix Domain Socket 与本地 Agent 通信;
  • 同时注册自定义 Runtime,指向特定二进制执行程序,实现容器生命周期的定制化控制。

▶ 运行时层实现

  • Agent 侧:实现标准 gRPC 服务,响应来自 Containerd 的 Snapshotter 接口调用(如 Prepare、Mount、Remove);
  • 存储层封装:采用 Wrapper 模式封装原生 OverlayFS Snapshotter,在不破坏原有逻辑的前提下注入自定义行为(如镜像预处理、元数据记录等);
  • 通信机制:通过本地 Unix Socket 实现低延迟、高安全性的进程间通信。

✅ 价值:无需修改 Containerd 核心代码,即可实现运行时行为扩展,符合云原生插件化设计哲学。


2. 容器 RootFS 云端持久化方案

项目支持在容器终止时,自动将根文件系统打包并上传至云端存储,实现状态持久化与跨节点恢复。

▶ 触发机制

  • 通过 Kubernetes Pod 标签(如 backup.container.io/enabled=true)声明式触发备份行为;
  • 由控制器监听 Pod 生命周期事件,在容器终止前自动发起备份流程。

▶ 存储架构

  • 支持本地暂存 + 云端迁移双阶段模式,适配不同存储后端(如 POSIX 兼容文件系统、分布式存储等);
  • 使用标准 tar 格式归档容器根目录,兼容性强,支持多版本格式演进;
  • 通过自定义 CRD(Custom Resource Definition)统一管理备份任务状态、元数据及生命周期。

▶ 上传流程

  1. 控制器捕获容器终止事件,匹配备份策略;
  2. 调用 Backup Manager 执行本地打包;
  3. 将归档文件异步上传至云端存储系统;
  4. 更新 CRD 状态,记录存储路径、校验和、时间戳等关键元数据。

✅ 价值:实现“有状态容器”的云原生存储迁移,为故障恢复、环境复现、审计追溯提供基础能力。


3. Docker-in-Docker(DinD)安全实现方案

项目通过轻量化、命名空间隔离的 DinD 架构,为容器内提供完整且安全的 Docker 服务,避免传统 DinD 的权限与资源冲突问题。

▶ 架构设计

  • 为每个需 Docker 能力的业务容器,动态创建专属的 DinD容器;
  • DinD 容器与主容器共享 cgroup 与网络命名空间,确保资源隔离的同时保持网络互通。

▶ 核心实现

  • 容器创建:使用轻量级容器工具(如 nerdctl)启动 DinD 容器,挂载主容器 rootfs 至指定路径,实现上下文共享;
  • 网络配置:通过初始化脚本配置共享网络栈,确保 DinD 内部构建的容器可被主容器访问;
  • 服务暴露:通过 gRPC 接口封装 Docker API,主容器可通过客户端工具调用构建、运行、镜像管理等操作;
  • 命令行工具:提供 CLI 工具,支持与 DinD 服务交互,简化调试与集成。

▶ 关键特性

  • 支持 GPU 设备透传与资源配额管理;
  • 完整兼容 Docker API,业务无感知迁移;
  • 基于 Kubernetes ServiceAccount Token 实现调用鉴权;
  • 内置健康检查与配置热更新机制,保障服务稳定性。

✅ 价值:在安全隔离的前提下,赋予容器内构建与运行 Docker 的能力,适用于 CI/CD、开发环境、模型训练等场景。


总结

本方案通过深度集成 Containerd 与 Kubernetes,实现了三大核心能力:

  1. 标准化运行时扩展 —— 基于 Proxy Plugin 机制,非侵入式增强容器行为;
  2. 云原生存储迁移 —— 声明式触发 + CRD 管理,实现容器状态持久化;
  3. 安全 DinD 架构 —— 命名空间共享,兼顾功能完整与资源隔离。

该架构具备良好的可移植性与扩展性,可作为企业级容器平台增强运行时能力的参考实现。

基于 Containerd Snapshotter 接口的容器镜像操作增强机制

在容器运行时扩展场景中,通过 Wrapper 模式封装原生 Snapshotter,可实现对容器镜像生命周期操作的透明拦截与增强处理。以下从架构设计、操作流程、关键实现与集成方式四个维度,详细解析该技术方案。


一、Wrapper 模式架构:非侵入式扩展

项目采用标准的 Wrapper 设计模式,对 Containerd 原生的 OverlayFS Snapshotter 进行封装,通过 pre / post 钩子函数实现操作拦截,无需修改底层实现。

1
2
3
4
5
6
7
// 示例:封装 Prepare 接口,支持自定义重写逻辑
func (s *WrappedSnapshotter) Prepare(ctx context.Context, req *api.PrepareSnapshotRequest) (*api.PrepareSnapshotResponse, error) {
if s.prepareRewriteFn != nil {
return s.prepareRewriteFn(ctx, s.parent, req) // 自定义预处理
}
return s.parent.Prepare(ctx, req) // 默认调用原生逻辑
}

优势

  • 保持与原生 Snapshotter 的完全兼容;
  • 支持灵活注入自定义行为(如日志、缓存、备份、恢复等);
  • 易于维护与升级,不绑定特定底层实现。

二、标准化操作拦截流程

所有 Snapshotter 操作均遵循统一的 三阶段处理流程Pre → Execute → Post,确保扩展逻辑可预测、可插拔。

1
2
3
4
5
6
7
8
9
10
11
12
13
func (r *OperationRewriter) do(ctx context.Context, s api.SnapshotsServer, req Request) (Response, error) {
if err := r.pre(ctx, req); err != nil { // 预处理(如权限校验、日志记录)
return nil, err
}
resp, execErr := s.Execute(ctx, req) // 执行原生操作
if execErr != nil {
return resp, execErr
}
if err := r.post(ctx, req, resp, execErr); err != nil { // 后处理(如状态更新、触发备份)
return resp, err
}
return resp, nil
}

价值

  • 实现操作前后状态感知;
  • 支持异步、非阻塞扩展逻辑;
  • 便于统一错误处理与审计追踪。

三、关键操作的具体增强实现

1. Prepare(容器准备阶段)

  • PrePrepare:初始化上下文、记录操作日志;
  • PostPrepare
    • 解析挂载参数中的 upperdir 路径,定位容器可写层;
    • 创建容器元数据缓存,记录容器 ID 与存储路径映射;
    • 若检测到需恢复状态(如从云端拉取),触发异步恢复流程。

2. Mounts(挂载访问阶段)

  • PostMounts
    • 初始化容器运行时上下文;
    • 根据策略判断是否需从远程存储恢复 RootFS(如冷启动场景);
    • 挂载完成后更新容器状态为“已就绪”。

3. Remove(快照删除阶段)

  • PreRemove
    • 触发容器状态持久化:调用 Backup Manager 将 RootFS 打包上传至云端存储;
    • 支持失败重试与状态标记(如“备份中”、“备份失败”);
  • PostRemove
    • 清理本地缓存元数据;
    • 释放关联资源(如网络、设备句柄等)。

📌 示例:在 Remove 前自动触发备份

1
2
3
4
5
6
7
8
func (h *SnapshotHandler) OnRemove(ctx context.Context, key string) {
if shouldBackup(key) {
if err := backupManager.TriggerBackup(ctx, containerID); err != nil {
log.WithError(err).Error("Backup failed before snapshot removal")
// 可选:阻塞删除 or 标记状态供后续补偿
}
}
}

四、容器生命周期元数据管理

通过轻量级缓存层管理容器运行时状态,关键函数包括:

▶ InitSnapshotMounts

  • mount.Options 中提取 upperdir 路径;
  • 建立容器 ID → 存储路径 → 状态 的映射关系;
  • 支持多容器并发初始化,线程安全。

▶ OnMounts

  • 检查容器是否需从云端恢复 RootFS;
  • 如需恢复,异步拉取并解压至指定路径;
  • 更新容器状态为“恢复完成”,允许后续启动。

▶ OnRemove + Clear

  • 删除前触发备份(可配置策略);
  • 删除后清理缓存,避免内存泄漏;
  • 支持事件通知(如 Prometheus 指标更新、审计日志)。

五、与 Containerd 的集成方式

  1. Proxy Plugin 机制
    通过 Unix Domain Socket 暴露 Snapshotter gRPC 服务,Containerd 通过插件配置动态加载。

  2. 完整接口实现
    实现 api.SnapshotsServer 所有方法,确保协议兼容性。

  3. 操作透传 + 增强
    默认调用原生 OverlayFS Snapshotter,仅在特定 Hook 注入自定义逻辑。

  4. 异步非阻塞设计
    耗时操作(如备份、恢复)通过 Goroutine 异步执行,避免阻塞容器启动流程。


✅ 方案价值总结

能力维度 实现效果
无缝集成 无需修改 Containerd 核心,通过标准插件机制接入
透明增强 对上层(如 Kubernetes、CRI)完全透明,无感知使用增强功能
状态持久化 容器删除前自动备份 RootFS 至云端,支持跨节点恢复
生命周期感知 通过 Hook 机制精确捕获 Prepare/Mount/Remove 事件,实现精细化控制
可扩展性 所有操作支持插件化 Hook,便于后续扩展审计、加密、压缩、策略路由等功能

🧩 适用场景

  • 有状态容器的云原生存储迁移;
  • 容器环境快速复现与调试(如 CI/CD、训练任务);
  • 安全合规场景下的操作审计与数据留存;
  • 边缘计算中容器状态的云端同步与恢复。

dind这部分

dind容器中有docker守护进程

构建基础Dind镜像时,下载特定版本的Docker二进制文件

dind容器的网络命名空间是跟main容器一样的

dind容器中执行docker命令的技术原理和逻辑链路:

核心技术

  1. Docker-in-Docker (DinD) 技术: 使用特权容器运行Docker守护进程
  2. Containerd管理: 通过containerd API创建和管理容器
  3. 网络共享: 使用container:<main_container_id>网络模式共享网络命名空间
  4. 文件系统映射: 通过bind mount将主容器的rootfs映射到dind容器中
  5. 请求劫持(Hijack): 拦截和修改Docker API请求

逻辑链路

1. dind容器创建流程

  • 权限验证: 检查Pod注解 k8s.io/dind=“enabled”
  • 容器创建: 使用nerdctl创建特权dind容器
  • 网络配置: 使用container:<main_id>共享网络命名空间
  • 文件系统映射: 将主容器rootfs挂载到dind容器的/mainroot目录
  • 安全配置: 设置seccomp=unconfined, apparmor=unconfined等安全选项

2. Docker命令执行流程

  • API代理: Gin框架接收Docker API请求
  • 请求劫持: 拦截create/update/exec请求并修改配置
  • 路径重写: 将主机路径重写为容器内路径 (如 /host/path/mainroot/host/path)
  • 反向代理: 将修改后的请求转发到真正的Docker守护进程socket

3. 关键技术点

网络隔离突破:

  • dind容器使用--network=container:<main_id>启动,共享主容器的网络命名空间
  • 这使得dind容器中的Docker守护进程能够访问主容器的网络环境

文件系统访问:

  • 主容器的rootfs通过bind mount挂载到dind容器的/mainroot目录
  • Docker volume和bind mount的源路径会被重写为容器内路径

安全限制:

  • 禁止privileged模式、capabilities修改等危险操作
  • 验证挂载目标路径的合法性,防止路径遍历攻击

容器生命周期管理:

  • 通过containerd事件监听自动管理dind容器的创建和清理
  • 主容器删除时自动清理对应的dind容器

执行流程示例

  1. 用户在Pod中执行docker run命令
  2. Docker client连接到/mainroot/run/docker.sock (被dind server劫持)
  3. dind server修改请求配置(路径重写、安全验证)
  4. 请求被转发到真正的Docker守护进程(/run/docker.sock)
  5. Docker守护进程在dind容器中创建目标容器
  6. dind容器与主容器共享网络命名空间

这种设计使得在Kubernetes Pod中安全地运行Docker命令成为可能,同时保持了良好的隔离性和安全性。

dind容器网络配置的具体实现

pkg/dind/controller/create_helpers.go 中,网络配置的具体实现如下:

1
2
3
4
5
6
func DindContainerNetworkOptions(mainID apis.ContainerID, hostname string) types.NetworkOptions {
return types.NetworkOptions{
NetworkSlice: []string{"container:" + mainID.String()},
Hostname: hostname,
}
}

网络配置流程

  1. 网络选项创建 (pkg/dind/controller/dind_manager.go)

    1
    networkOptions := DindContainerNetworkOptions(apis.ContainerID(mainContainer.ID()), mainSpec.Hostname)
  2. 网络管理器创建 (pkg/dind/controller/dind_manager.go)

    1
    netManager, err := containerutil.NewNetworkingOptionsManager(createOptions.GOptions, networkOptions, client)
  3. 容器创建 (pkg/dind/controller/dind_manager.go)

    1
    c, gc, err := container.Create(ctx, client, args, netManager, createOptions)

技术细节

container:<main_id> 网络模式:

  • 这是Docker的容器网络模式,允许一个容器共享另一个容器的网络命名空间
  • 在nerdctl/containerd中,这通过设置 NetworkSlice: []string{"container:" + mainID.String()} 实现

网络命名空间共享:

  • dind容器启动时会使用主容器的网络命名空间
  • 这使得dind容器中的Docker守护进程能够看到和访问主容器的网络环境
  • 所有网络接口、路由、iptables规则等都会被共享

实际执行效果:
相当于执行了类似这样的docker命令:

1
docker run --network=container:<main_container_id> --name dind-<main_id_prefix> ...

验证逻辑

还有网络模式的验证逻辑:

1
2
3
if config.HostConfig.NetworkMode.IsBridge() {
return ErrBridgeNetworkNotSupported
}

这确保了在dind环境中只能使用容器网络模式,不能使用bridge等其他网络模式。

这种网络配置使得dind容器能够完全共享主容器的网络环境,为在容器内运行Docker命令提供了必要的网络隔离突破。

Docker命令劫持的逻辑

Docker命令能够被劫持的核心逻辑在于socket重定向和API请求拦截,具体实现如下:

1. Socket重定向机制

在dind容器中,Docker守护进程监听的是真正的 /run/docker.sock,但是:

客户端连接路径

1
address := "/mainroot/run/docker.sock"

服务端代理路径

1
dockerSocket := "/run/docker.sock"

2. 反向代理劫持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
r.Any("*path", func(c *gin.Context) {
// 1. 找到真正的docker socket
_ = s.findDockerSocket()

// 2. 请求劫持 - 关键步骤!
if err := HijackRequest(c.Request); err != nil {
c.String(http.StatusBadRequest, "%v", err)
return
}

// 3. 创建反向代理到真正的docker守护进程
proxy := &httputil.ReverseProxy{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return net.Dial("unix", s.dockerSocket) // 连接到真正的docker socket
},
},
Director: func(req *http.Request) {
req.URL = u // 保持原始URL路径
},
}
proxy.ServeHTTP(c.Writer, c.Request)
})

3. 请求劫持逻辑

劫持类型识别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func GetHijackType(method string, path string) HijackType {
if method == http.MethodPost {
path = filepath.Clean(path)
path = strings.TrimPrefix(path, "/")
fields := strings.Split(path, "/")
if len(fields) < 3 {
return HijackNone
}
if fields[1] == "containers" {
if fields[2] == "create" {
return HijackCreate
}
if len(fields) == 4 && fields[3] == "update" {
return HijackUpdate
}
if len(fields) == 4 && fields[3] == "exec" {
return HijackExec
}
}
}
return HijackNone
}

4. 具体劫持操作

创建请求劫持

  • 路径重写:将主机路径重写为容器内路径 (/host/path/mainroot/host/path)
  • 安全验证:禁止privileged模式、capabilities修改等危险操作
  • 挂载点验证:确保挂载目标路径的合法性

执行请求劫持 :

  • 验证执行权限和安全设置

5. 完整的劫持流程

  1. 客户端连接: Docker client连接到 /mainroot/run/docker.sock
  2. 请求拦截: dind server (Gin框架) 接收所有API请求
  3. 请求分析: 识别需要劫持的请求类型 (create/update/exec)
  4. 配置修改: 重写路径、验证安全设置、修改容器配置
  5. 代理转发: 将修改后的请求转发到真正的Docker守护进程
  6. 响应返回: 将Docker守护进程的响应返回给客户端

技术优势

这种设计实现了:

  • 透明劫持: 客户端无感知,仍然使用标准Docker API
  • 安全控制: 在转发前进行安全验证和配置修改
  • 灵活性: 可以根据需要劫持特定的API端点
  • 性能: 反向代理模式开销极小

这就是为什么Docker命令能够被劫持的核心逻辑 - 通过socket重定向和HTTP API拦截来实现透明的请求修改和转发。

问题1: 真正的dockerd socket位置

真正的Docker守护进程socket在dind容器中的 /run/docker.sock

真正的docker socket路径:

1
dockerSocket := "/run/docker.sock"

反向代理直接连接到这个socket:

1
return net.Dial("unix", s.dockerSocket) // 连接到真正的docker socket

问题2: 为什么能访问 “/mainroot/run/docker.sock”

这是因为文件系统映射和socket代理机制

1. 文件系统映射

在dind容器创建时

1
2
3
createOptions.Volume = append(createOptions.Volume,
fmt.Sprintf("%v:%v", rootfsMount.Mountpoint, MAINROOT),
)

这里 MAINROOT = "/mainroot",所以主容器的rootfs被挂载到dind容器的 /mainroot 目录。

2. Socket创建机制

dind server会在 /mainroot/run/docker.sock 创建一个代理socket:

pkg/dind/server/server.go):

1
2
3
4
5
6
7
8
_ = os.RemoveAll(s.serveSocket)  // 清理旧的socket

l, err := net.Listen("unix", s.serveSocket) // 创建新的socket监听
if err != nil {
return err
}
defer os.Remove(s.serveSocket)
_ = os.Chmod(s.serveSocket, 0777) // 设置权限

这里的 s.serveSocket 就是 /mainroot/run/docker.sock

3. 完整的访问路径

  1. 主容器中: 用户执行 docker -H unix:///run/docker.sock ...
  2. 路径映射: /run/docker.sock (主容器) → /mainroot/run/docker.sock (dind容器)
  3. socket代理: dind server监听在 /mainroot/run/docker.sock
  4. 请求转发: dind server将请求转发到真正的 /run/docker.sock (dind容器内的真实docker socket)

技术实现

这实际上是一个 Unix socket代理

  • /mainroot/run/docker.sock 创建一个代理socket
  • 所有连接到这个socket的请求都被dind server处理
  • dind server进行安全验证和请求修改后,转发到真正的docker socket
  • 响应再通过代理socket返回给客户端

这样设计的好处是:

  • 对客户端透明: 用户仍然使用标准Docker命令行
  • 安全控制: 可以在转发前进行安全验证
  • 灵活性: 可以劫持和修改特定的API请求

所以本质上,/mainroot/run/docker.sock 是一个代理socket,而 /run/docker.sock 是真正的Docker守护进程socket。

dockerctl命令能够被dind manager识别是通过以下机制实现的:

1. gRPC通信机制

dockerctl命令通过gRPC与dind manager进行通信:

  • 客户端连接: dockerctl通过 getClient() 连接到Unix socket /run/aeon/container/container-service.sock
  • 认证机制: 使用Pod的service account token进行认证 getTokenContent()

2. ServiceCall处理流程

2.1 命令执行流程

  1. 用户执行命令: 如 dockerctl start
  2. gRPC调用: 调用 ServiceCall 方法
  3. 服务识别: 指定 Service: "docker" 和相应的 Action (start/stop/restart/status/show-config)

2.2 服务端处理

在agent的runtime service中,ServiceCall 方法处理请求:

1
2
case "docker":
message, err := s.dc.DindCall(ctx, token, req.Action, req.Args)

2.3 dind manager识别

dind controller的 DindCall 方法根据action参数进行识别:

1
2
3
4
5
6
7
8
9
10
11
12
switch action {
case "start":
_, err := dc.dcm.CreateDindContainer(ctx, mainID)
case "stop":
err := dc.dcm.ClearDindContainer(ctx, mainID)
case "restart":
// 先stop再start
case "status":
tasks, mem, cpu, err := dc.dcm.GetDindStatus(ctx, mainID)
case "show-config":
config, err := dc.dcm.GetDindConfig(ctx, mainID)
}

3. 权限验证机制

dind manager通过以下方式验证权限:

  1. Token验证: 使用Kubernetes TokenReview API验证token有效性 getPodByToken()
  2. Pod注解检查: 检查Pod是否有 mizar.k8s.io/dind: "enabled" 注解 judgeDindEnabled()
  3. 容器ID提取: 从Pod状态中提取主容器的container ID getMainIDFromPod()

4. 命令与action映射

dockerctl命令与dind manager action的映射关系:

dockerctl命令 dind manager action 功能描述
dockerctl start "start" 创建dind容器
dockerctl stop "stop" 停止并清理dind容器
dockerctl restart "restart" 重启dind容器
dockerctl status "status" 获取dind容器状态
dockerctl show-config "show-config" 显示dind配置

5. 完整的识别流程

  1. 客户端发起请求: dockerctl通过gRPC发送ServiceRequest
  2. 服务端接收: runtime service的ServiceCall方法接收请求
  3. 服务类型判断: 检查 req.Service == "docker"
  4. 权限验证: 验证token和Pod注解
  5. action分发: 根据action参数调用相应的dind manager方法
  6. 结果返回: 将执行结果封装成anypb.Any返回给客户端

这种设计使得dockerctl命令能够被dind manager准确识别和处理,同时保证了安全性和权限控制。