容器运行时从Docker到containerd的迁移复盘:兼容性验证、性能对比与零宕机切换方案

发布时间:2026/7/26 18:40:17
容器运行时从Docker到containerd的迁移复盘:兼容性验证、性能对比与零宕机切换方案 容器运行时从Docker到containerd的迁移复盘兼容性验证、性能对比与零宕机切换方案一、项目背景与业务挑战Kubernetes 1.24 正式移除了 dockershim这意味着 K8s 不再内置 Docker 支持。我们的 3 个生产集群共 1200 Node全部使用 Docker 作为容器运行时必须在升级到 K8s 1.26 之前完成 Docker → containerd 的迁移。迁移面临的三个核心挑战兼容性风险1200 Node 上运行着 5000 Pod部分 Pod 使用了 Docker 特有功能如 Docker socket 挂载、docker-compose 依赖、自定义 Docker logging driver这些功能在 containerd 中不可用或行为不同。性能不确定性containerd 和 Docker 的容器生命周期管理性能差异有多大构建排队、镜像拉取、容器启动延迟是否可控缺乏生产环境基准数据。零宕机要求迁移过程中不能有服务中断。逐节点迁移意味着每个 Node 需要短暂停止调度、驱逐 Pod、切换运行时、恢复调度整个过程必须在业务容忍窗口内完成。我们做了全面的评估和测试最终用 4 周时间完成了 1200 Node 的全量迁移期间零服务中断。二、核心方案兼容性验证 性能基准 逐节点滚动迁移2.1 Docker vs containerd 兼容性差异梳理迁移前必须识别所有 Docker 依赖点。我们扫描了集群中所有 Pod 的 YAML 配置识别了三类兼容性问题import logging from dataclasses import dataclass, field from enum import Enum from typing import Dict, List, Optional logger logging.getLogger(__name__) class CompatibilityIssueType(Enum): 兼容性问题类型 DOCKER_SOCKET_MOUNT docker_socket_mount # Docker socket挂载 DOCKER_LOGGING_DRIVER docker_logging_driver # Docker特有日志驱动 DOCKER_COMPOSE_DEPENDENCY docker_compose_dep # docker-compose依赖 DOCKER_NETWORK_PLUGIN docker_network_plugin # Docker网络插件 DOCKER_VOLUME_DRIVER docker_volume_driver # Docker存储驱动 dataclass class CompatibilityIssue: 兼容性问题记录 pod_name: str # Pod名称 namespace: str # Namespace issue_type: CompatibilityIssueType # 问题类型 description: str # 问题描述 remediation: str # 修复方案 priority: str medium # 优先级high/medium/low resolved: bool False # 是否已修复 class CompatibilityScanner: 兼容性扫描器识别Docker特有依赖 # Docker socket 挂载检测规则 DOCKER_SOCKET_PATHS: List[str] [ /var/run/docker.sock, /run/docker.sock, /var/lib/docker, ] # Docker 特有日志驱动列表 DOCKER_LOGGING_DRIVERS: List[str] [ json-file, # Docker默认日志驱动containerd使用CRI日志 journald, # Docker支持的journald驱动 splunk, # Docker Splunk日志驱动 gelf, # Docker GELF日志驱动 ] # Docker Compose 特征标记 COMPOSE_INDICATORS: List[str] [ docker-compose, compose file, depends_on, docker-compose.yml, ] def scan_cluster(self, pod_list: List[dict]) - List[CompatibilityIssue]: 扫描集群中所有Pod的Docker依赖 Args: pod_list: Pod配置列表 Returns: 兼容性问题列表 issues [] try: for pod in pod_list: pod_name pod.get(metadata, {}).get(name, ) namespace pod.get(metadata, {}).get(namespace, ) # 检查Docker socket挂载 for volume in pod.get(spec, {}).get(volumes, []): host_path volume.get(hostPath, {}).get(path, ) if host_path in self.DOCKER_SOCKET_PATHS: issues.append(CompatibilityIssue( pod_namepod_name, namespacenamespace, issue_typeCompatibilityIssueType.DOCKER_SOCKET_MOUNT, descriptionfPod挂载Docker socket: {host_path}, remediation改用Kubernetes原生API替代Docker CLI, priorityhigh )) # 检查容器内的Docker socket挂载 for container in pod.get(spec, {}).get(containers, []): for mount in container.get(volumeMounts, []): if mount.get(name, ) in [v.get(name, ) for v in pod.get(spec, {}).get(volumes, [])]: vol next((v for v in pod.get(spec, {}).get(volumes, []) if v.get(name) mount.get(name)), {}) host_path vol.get(hostPath, {}).get(path, ) if host_path in self.DOCKER_SOCKET_PATHS: logger.warning(fDocker socket挂载: {pod_name}/{namespace}) # 检查Docker特有日志驱动通过Annotations检测 annotations pod.get(metadata, {}).get(annotations, {}) for key, value in annotations.items(): if docker in key.lower() or logging in key.lower(): for driver in self.DOCKER_LOGGING_DRIVERS: if driver in value and driver ! json-file: issues.append(CompatibilityIssue( pod_namepod_name, namespacenamespace, issue_typeCompatibilityIssueType.DOCKER_LOGGING_DRIVER, descriptionfPod使用Docker日志驱动: {driver}, remediation切换到Kubernetes CRI标准日志收集Fluentd/Fluent Bit, prioritymedium )) logger.info(f兼容性扫描完成发现 {len(issues)} 个问题) return issues except Exception as e: logger.error(f兼容性扫描异常: {e}, exc_infoTrue) return []兼容性扫描结果在 5000 Pod 中发现 47 个兼容性问题其中 23 个高风险Docker socket 挂载、15 个中风险非标准日志驱动、9 个低风险Docker Compose 依赖。2.2 性能基准测试迁移前我们搭建了两个对照集群各 50 Node分别运行 Docker 和 containerd执行标准化基准测试基准测试覆盖四个维度维度Dockercontainerd差异结论镜像拉取延迟(100MB)12.3s8.1s↓34%containerd更快容器启动延迟2.8s1.9s↓32%containerd更快Pod调度延迟5.2s4.1s↓21%containerd更快Node CPU开销(运行时)3.5%1.8%↓49%containerd更轻日志收集吞吐15MB/s18MB/s↑20%containerd更快构建排队时间45s38s↓16%containerd更快结论containerd 在所有测试维度上均优于 Docker性能差异范围 16%-49%迁移不会带来性能回退风险。2.3 逐节点滚动迁移方案核心迁移流程是逐节点滚动切换——每次选择一批 Nodebatch size 5%执行以下步骤import logging from typing import List, Optional logger logging.getLogger(__name__) class RuntimeMigrationExecutor: 容器运行时迁移执行器逐节点滚动切换 # 迁移批次大小每批迁移5%的Node BATCH_PERCENTAGE 0.05 # Pod驱逐优雅期 GRACE_PERIOD_SECONDS 300 # 5分钟 def __init__(self, k8s_client, node_selectorNone): self.k8s_client k8s_client self.node_selector node_selector # Node选择器可按区域/角色分组 def execute_migration(self, total_nodes: int) - dict: 执行全量滚动迁移 Args: total_nodes: 总Node数量 Returns: 迁移结果统计 batch_size max(1, int(total_nodes * self.BATCH_PERCENTAGE)) total_batches total_nodes // batch_size 1 results { total_nodes: total_nodes, batch_size: batch_size, total_batches: total_batches, migrated: 0, failed: 0, skipped: 0, } try: for batch_num in range(1, total_batches 1): logger.info(f开始迁移第 {batch_num}/{total_batches} 批) # 选择当前批次的Node列表 batch_nodes self._select_batch_nodes(batch_num, batch_size) logger.info(f本批次Node: {batch_nodes}) for node in batch_nodes: success self._migrate_single_node(node) if success: results[migrated] 1 logger.info(fNode {node} 迁移成功) else: results[failed] 1 logger.error(fNode {node} 迁移失败标记跳过) # 每批次迁移后暂停10分钟观察稳定性 logger.info(f第 {batch_num} 批完成暂停10分钟观察) # 实际实现等待10分钟 监控健康检查 logger.info(f迁移完成: 成功 {results[migrated]}, 失败 {results[failed]}) return results except Exception as e: logger.error(f迁移执行异常: {e}, exc_infoTrue) return results def _migrate_single_node(self, node_name: str) - bool: 迁移单个Node的容器运行时 Args: node_name: Node名称 Returns: 迁移是否成功 try: # 第1步标记Node为不可调度cordon logger.info(f[{node_name}] Step 1: cordon节点) self.k8s_client.cordon_node(node_name) # 第2步优雅驱逐所有Poddrain logger.info(f[{node_name}] Step 2: drain节点优雅期 {self.GRACE_PERIOD_SECONDS}s) self.k8s_client.drain_node( node_name, grace_periodself.GRACE_PERIOD_SECONDS, ignore_daemonsetsTrue, # DaemonSet Pod不驱逐 delete_emptydir_dataTrue # 清理emptyDir数据 ) # 第3步停止Docker服务 logger.info(f[{node_name}] Step 3: 停止Docker服务) # SSH执行: systemctl stop docker docker.socket # 第4步安装并启动containerd logger.info(f[{node_name}] Step 4: 安装containerd) # SSH执行: apt install containerd systemctl start containerd # 第5步配置kubelet指向containerd logger.info(f[{node_name}] Step 5: 配置kubelet运行时为containerd) # 修改kubelet配置: --container-runtimeremote --container-runtime-endpoint/run/containerd/containerd.sock # 第6步重启kubelet logger.info(f[{node_name}] Step 6: 重启kubelet) # systemctl restart kubelet # 第7步验证Node健康 logger.info(f[{node_name}] Step 7: 验证Node健康状态) if not self._verify_node_health(node_name): logger.error(f[{node_name}] 健康检查失败回退到Docker) self._rollback_node(node_name) return False # 第8步恢复Node调度uncordon logger.info(f[{node_name}] Step 8: uncordon节点恢复调度) self.k8s_client.uncordon_node(node_name) return True except Exception as e: logger.error(f[{node_name}] 迁移异常: {e}, exc_infoTrue) self._rollback_node(node_name) return False def _verify_node_health(self, node_name: str) - bool: 验证Node迁移后的健康状态 检查项kubelet Ready、containerd运行、Pod可调度 try: node_status self.k8s_client.get_node_status(node_name) # 检查kubelet是否Ready conditions node_status.get(conditions, []) ready any(c.get(type) Ready and c.get(status) True for c in conditions) if not ready: logger.error(fNode {node_name} kubelet未Ready) return False # 检查containerd进程是否运行 # SSH执行: systemctl is-active containerd return True except Exception as e: logger.error(f健康检查异常: {e}) return False def _rollback_node(self, node_name: str) - bool: 回退Node到Docker运行时 try: logger.warning(f回退Node {node_name} 到Docker) # 停止containerd恢复Docker配置重启kubelet # systemctl stop containerd # 修改kubelet配置恢复Docker参数 # systemctl restart kubelet docker return True except Exception as e: logger.error(f回退异常: {e}) return False三、实践落地迁移过程与效果数据3.1 47 个兼容性问题的修复23 个高风险 Docker socket 挂载 Pod 的修复方式分类修复方式数量说明改用K8s Client API15原通过Docker CLI操作容器改为调用K8s API改用BuildKit5原通过Docker socket构建镜像改为远程BuildKit移除挂载3CI/CD Pipeline不再需要Docker CLI直接移除15 个中风险日志驱动 Pod 全部切换到 Fluent BitK8s CRI 标准日志收集。9 个低风险 Docker Compose 依赖通过迁移到 K8s Manifest 解决。3.2 迁移过程数据指标数据总Node数1200每批Node数605%总批次20迁移总时长4周成功迁移Node1197失败回退Node3Pod驱逐成功率99.8%服务中断次数03 个失败回退的 Node 原因分析2 个 Nodecontainerd 安装后镜像缓存损坏回退后重新迁移成功1 个 Nodekubelet 配置修改后启动失败回退后排查为配置语法错误3.3 迁移后性能对比数据生产环境指标Docker迁移前containerd迁移后变化镜像拉取延迟15.2s9.3s↓39%容器启动延迟3.1s2.0s↓35%Node CPU开销3.8%2.1%↓45%Node内存开销280MB165MB↓41%日志收集吞吐14MB/s17MB/s↑21%Pod调度延迟5.8s4.3s↓26%镜像存储占用45GB/Node38GB/Node↓16%3.4 迁移后的运维变化迁移后最显著的变化是运维复杂度的降低不再需要维护 Docker Enginecontainerd 是 K8s 原生运行时配置简单升级与 K8s 版本对齐日志收集统一化从 Docker json-file Fluentd 改为 CRI 标准日志 Fluent Bit配置更统一镜像管理简化containerd 使用 CRI image manager无需额外维护 Docker image prune 等清理任务四、关键挑战与应对策略4.1 Docker socket 挂载的深度依赖23 个 Pod 挂载 Docker socket 的用途各异CI/CD 构建镜像、运维调试工具、安全扫描容器。不能简单移除挂载必须找到替代方案。应对策略CI/CD场景迁移到 Tekton BuildKit不再依赖 Docker socket运维调试改用kubectl execcrictl替代 Docker CLI安全扫描改用 Trivy 的远程扫描模式不需要在容器内运行 Docker CLI4.2 大规模滚动迁移的调度压力每批 60 个 Node 同时驱逐 Pod可能导致其他 Node 的调度压力骤增。应对策略分区域滚动按可用区AZ分批迁移同一AZ内最多迁移 5% Node跨AZ分散压力PDB保护关键服务设置了 PodDisruptionBudget确保驱逐时最低可用实例数不低于阈值扩容缓冲迁移期间临时扩容 10% Node为驱逐的 Pod 提供额外调度空间4.3 回退机制的可靠性万一 containerd 运行出现系统性问题需要在短时间内回退到 Docker。应对策略双运行时共存期迁移初期每个 Node 保留 Docker Engine 的安装包5分钟内可回退渐进式清理全部 Node 迁移完成后保留 Docker Engine 30 天冷备期再统一卸载4.4 containerd 的运维工具差异运维团队习惯了 Docker CLIdocker ps、docker logs、docker execcontainerd 使用 crictl命令和输出格式完全不同。应对策略迁移培训制作 Docker → crictl 命令对照表2周集中培训工具适配开发运维脚本包装层将 docker 命令自动翻译为 crictl 命令监控适配Prometheus 指标从 docker_* 改为 containerd_*Grafana Dashboard 全部更新五、总结从 Docker 到 containerd 的迁移本质是从外部依赖回归K8s原生的架构精简。Docker 作为容器运行时的时代正在终结containerd 作为 CNCF 标准运行时是 K8s 的长期方向。三个关键经验兼容性扫描必须先于迁移47 个 Docker 特有依赖如果在迁移后才发现将导致大面积 Pod 失败。扫描和修复工作必须提前完成。逐批次滚动而非一次性切换5% 批次大小 10分钟观察窗口的策略让我们在发现 3 个失败 Node 时能立即暂停排查避免了更大范围的故障。性能对比数据是最好的说服工具containerd 在所有维度上优于 Docker 的基准数据消除了团队对迁移是否带来性能回退的疑虑加速了决策过程。下一步计划基于 containerd 的 CRI 排错能力增强——开发基于 crictl 的智能排障脚本替代传统 Docker 排障流程同时探索 containerd 的镜像懒拉取lazy pulling功能进一步优化镜像拉取延迟。