容器运行时扩展方案技术解析
容器运行时扩展方案技术解析
基于对某容器运行时扩展项目的代码分析,现从架构层面提炼其核心技术实现,聚焦三大核心能力:运行时接入机制、容器根文件系统云端持久化、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)统一管理备份任务状态、元数据及生命周期。
▶ 上传流程
- 控制器捕获容器终止事件,匹配备份策略;
- 调用 Backup Manager 执行本地打包;
- 将归档文件异步上传至云端存储系统;
- 更新 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,实现了三大核心能力:
- 标准化运行时扩展 —— 基于 Proxy Plugin 机制,非侵入式增强容器行为;
- 云原生存储迁移 —— 声明式触发 + CRD 管理,实现容器状态持久化;
- 安全 DinD 架构 —— 命名空间共享,兼顾功能完整与资源隔离。
该架构具备良好的可移植性与扩展性,可作为企业级容器平台增强运行时能力的参考实现。
基于 Containerd Snapshotter 接口的容器镜像操作增强机制
在容器运行时扩展场景中,通过 Wrapper 模式封装原生 Snapshotter,可实现对容器镜像生命周期操作的透明拦截与增强处理。以下从架构设计、操作流程、关键实现与集成方式四个维度,详细解析该技术方案。
一、Wrapper 模式架构:非侵入式扩展
项目采用标准的 Wrapper 设计模式,对 Containerd 原生的 OverlayFS Snapshotter 进行封装,通过 pre / post 钩子函数实现操作拦截,无需修改底层实现。
1 | // 示例:封装 Prepare 接口,支持自定义重写逻辑 |
✅ 优势:
- 保持与原生 Snapshotter 的完全兼容;
- 支持灵活注入自定义行为(如日志、缓存、备份、恢复等);
- 易于维护与升级,不绑定特定底层实现。
二、标准化操作拦截流程
所有 Snapshotter 操作均遵循统一的 三阶段处理流程:Pre → Execute → Post,确保扩展逻辑可预测、可插拔。
1 | func (r *OperationRewriter) do(ctx context.Context, s api.SnapshotsServer, req Request) (Response, error) { |
✅ 价值:
- 实现操作前后状态感知;
- 支持异步、非阻塞扩展逻辑;
- 便于统一错误处理与审计追踪。
三、关键操作的具体增强实现
1. Prepare(容器准备阶段)
- PrePrepare:初始化上下文、记录操作日志;
- PostPrepare:
- 解析挂载参数中的
upperdir路径,定位容器可写层; - 创建容器元数据缓存,记录容器 ID 与存储路径映射;
- 若检测到需恢复状态(如从云端拉取),触发异步恢复流程。
- 解析挂载参数中的
2. Mounts(挂载访问阶段)
- PostMounts:
- 初始化容器运行时上下文;
- 根据策略判断是否需从远程存储恢复 RootFS(如冷启动场景);
- 挂载完成后更新容器状态为“已就绪”。
3. Remove(快照删除阶段)
- PreRemove:
- 触发容器状态持久化:调用 Backup Manager 将 RootFS 打包上传至云端存储;
- 支持失败重试与状态标记(如“备份中”、“备份失败”);
- PostRemove:
- 清理本地缓存元数据;
- 释放关联资源(如网络、设备句柄等)。
📌 示例:在 Remove 前自动触发备份
1 | func (h *SnapshotHandler) OnRemove(ctx context.Context, key string) { |
四、容器生命周期元数据管理
通过轻量级缓存层管理容器运行时状态,关键函数包括:
▶ InitSnapshotMounts
- 从
mount.Options中提取upperdir路径; - 建立容器 ID → 存储路径 → 状态 的映射关系;
- 支持多容器并发初始化,线程安全。
▶ OnMounts
- 检查容器是否需从云端恢复 RootFS;
- 如需恢复,异步拉取并解压至指定路径;
- 更新容器状态为“恢复完成”,允许后续启动。
▶ OnRemove + Clear
- 删除前触发备份(可配置策略);
- 删除后清理缓存,避免内存泄漏;
- 支持事件通知(如 Prometheus 指标更新、审计日志)。
五、与 Containerd 的集成方式
-
Proxy Plugin 机制
通过 Unix Domain Socket 暴露 Snapshotter gRPC 服务,Containerd 通过插件配置动态加载。 -
完整接口实现
实现api.SnapshotsServer所有方法,确保协议兼容性。 -
操作透传 + 增强
默认调用原生 OverlayFS Snapshotter,仅在特定 Hook 注入自定义逻辑。 -
异步非阻塞设计
耗时操作(如备份、恢复)通过 Goroutine 异步执行,避免阻塞容器启动流程。
✅ 方案价值总结
| 能力维度 | 实现效果 |
|---|---|
| 无缝集成 | 无需修改 Containerd 核心,通过标准插件机制接入 |
| 透明增强 | 对上层(如 Kubernetes、CRI)完全透明,无感知使用增强功能 |
| 状态持久化 | 容器删除前自动备份 RootFS 至云端,支持跨节点恢复 |
| 生命周期感知 | 通过 Hook 机制精确捕获 Prepare/Mount/Remove 事件,实现精细化控制 |
| 可扩展性 | 所有操作支持插件化 Hook,便于后续扩展审计、加密、压缩、策略路由等功能 |
🧩 适用场景
- 有状态容器的云原生存储迁移;
- 容器环境快速复现与调试(如 CI/CD、训练任务);
- 安全合规场景下的操作审计与数据留存;
- 边缘计算中容器状态的云端同步与恢复。
dind这部分
dind容器中有docker守护进程
构建基础Dind镜像时,下载特定版本的Docker二进制文件
dind容器的网络命名空间是跟main容器一样的
dind容器中执行docker命令的技术原理和逻辑链路:
核心技术
- Docker-in-Docker (DinD) 技术: 使用特权容器运行Docker守护进程
- Containerd管理: 通过containerd API创建和管理容器
- 网络共享: 使用
container:<main_container_id>网络模式共享网络命名空间 - 文件系统映射: 通过bind mount将主容器的rootfs映射到dind容器中
- 请求劫持(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容器
执行流程示例
- 用户在Pod中执行
docker run命令 - Docker client连接到
/mainroot/run/docker.sock(被dind server劫持) - dind server修改请求配置(路径重写、安全验证)
- 请求被转发到真正的Docker守护进程(/run/docker.sock)
- Docker守护进程在dind容器中创建目标容器
- dind容器与主容器共享网络命名空间
这种设计使得在Kubernetes Pod中安全地运行Docker命令成为可能,同时保持了良好的隔离性和安全性。
dind容器网络配置的具体实现
在 pkg/dind/controller/create_helpers.go 中,网络配置的具体实现如下:
1 | func DindContainerNetworkOptions(mainID apis.ContainerID, hostname string) types.NetworkOptions { |
网络配置流程
-
网络选项创建 (
pkg/dind/controller/dind_manager.go)1
networkOptions := DindContainerNetworkOptions(apis.ContainerID(mainContainer.ID()), mainSpec.Hostname)
-
网络管理器创建 (
pkg/dind/controller/dind_manager.go)1
netManager, err := containerutil.NewNetworkingOptionsManager(createOptions.GOptions, networkOptions, client)
-
容器创建 (
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 | if config.HostConfig.NetworkMode.IsBridge() { |
这确保了在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 | r.Any("*path", func(c *gin.Context) { |
3. 请求劫持逻辑
劫持类型识别:
1 | func GetHijackType(method string, path string) HijackType { |
4. 具体劫持操作
创建请求劫持
- 路径重写:将主机路径重写为容器内路径 (
/host/path→/mainroot/host/path) - 安全验证:禁止privileged模式、capabilities修改等危险操作
- 挂载点验证:确保挂载目标路径的合法性
执行请求劫持 :
- 验证执行权限和安全设置
5. 完整的劫持流程
- 客户端连接: Docker client连接到
/mainroot/run/docker.sock - 请求拦截: dind server (Gin框架) 接收所有API请求
- 请求分析: 识别需要劫持的请求类型 (create/update/exec)
- 配置修改: 重写路径、验证安全设置、修改容器配置
- 代理转发: 将修改后的请求转发到真正的Docker守护进程
- 响应返回: 将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 | createOptions.Volume = append(createOptions.Volume, |
这里 MAINROOT = "/mainroot",所以主容器的rootfs被挂载到dind容器的 /mainroot 目录。
2. Socket创建机制
dind server会在 /mainroot/run/docker.sock 创建一个代理socket:
1 | _ = os.RemoveAll(s.serveSocket) // 清理旧的socket |
这里的 s.serveSocket 就是 /mainroot/run/docker.sock
3. 完整的访问路径
- 主容器中: 用户执行
docker -H unix:///run/docker.sock ... - 路径映射:
/run/docker.sock(主容器) →/mainroot/run/docker.sock(dind容器) - socket代理: dind server监听在
/mainroot/run/docker.sock - 请求转发: 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 命令执行流程
- 用户执行命令: 如
dockerctl start - gRPC调用: 调用
ServiceCall方法 - 服务识别: 指定
Service: "docker"和相应的Action(start/stop/restart/status/show-config)
2.2 服务端处理
在agent的runtime service中,ServiceCall 方法处理请求:
1 | case "docker": |
2.3 dind manager识别
dind controller的 DindCall 方法根据action参数进行识别:
1 | switch action { |
3. 权限验证机制
dind manager通过以下方式验证权限:
- Token验证: 使用Kubernetes TokenReview API验证token有效性
getPodByToken() - Pod注解检查: 检查Pod是否有
mizar.k8s.io/dind: "enabled"注解judgeDindEnabled() - 容器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. 完整的识别流程
- 客户端发起请求: dockerctl通过gRPC发送ServiceRequest
- 服务端接收: runtime service的ServiceCall方法接收请求
- 服务类型判断: 检查
req.Service == "docker" - 权限验证: 验证token和Pod注解
- action分发: 根据action参数调用相应的dind manager方法
- 结果返回: 将执行结果封装成anypb.Any返回给客户端
这种设计使得dockerctl命令能够被dind manager准确识别和处理,同时保证了安全性和权限控制。





