欢迎光临
我们一直在努力

容器运行时从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_name=pod_name, namespace=namespace,
    issue_type=CompatibilityIssueType.DOCKER_SOCKET_MOUNT,
    description=f"Pod挂载Docker socket: {host_path}",
    remediation="改用Kubernetes原生API替代Docker CLI",
    priority="high"
    ))

    # 检查容器内的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(f"Docker 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_name=pod_name, namespace=namespace,
    issue_type=CompatibilityIssueType.DOCKER_LOGGING_DRIVER,
    description=f"Pod使用Docker日志驱动: {driver}",
    remediation="切换到Kubernetes CRI标准日志收集(Fluentd/Fluent Bit)",
    priority="medium"
    ))

    logger.info(f"兼容性扫描完成,发现 {len(issues)} 个问题")
    return issues

    except Exception as e:
    logger.error(f"兼容性扫描异常: {e}", exc_info=True)
    return []

    兼容性扫描结果:在 5000+ Pod 中发现 47 个兼容性问题,其中 23 个高风险(Docker socket 挂载)、15 个中风险(非标准日志驱动)、9 个低风险(Docker Compose 依赖)。

    2.2 性能基准测试

    迁移前我们搭建了两个对照集群(各 50 Node),分别运行 Docker 和 containerd,执行标准化基准测试:

    基准测试覆盖四个维度:

    维度Dockercontainerd差异结论
    镜像拉取延迟(100MB) 12.3s 8.1s ↓34% containerd更快
    容器启动延迟 2.8s 1.9s ↓32% containerd更快
    Pod调度延迟 5.2s 4.1s ↓21% containerd更快
    Node CPU开销(运行时) 3.5% 1.8% ↓49% containerd更轻
    日志收集吞吐 15MB/s 18MB/s ↑20% containerd更快
    构建排队时间 45s 38s ↓16% containerd更快

    结论:containerd 在所有测试维度上均优于 Docker,性能差异范围 16%-49%,迁移不会带来性能回退风险。

    2.3 逐节点滚动迁移方案

    核心迁移流程是"逐节点滚动切换"——每次选择一批 Node(batch 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_selector=None):
    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(f"Node {node} 迁移成功")
    else:
    results["failed"] += 1
    logger.error(f"Node {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_info=True)
    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步:优雅驱逐所有Pod(drain)
    logger.info(f"[{node_name}] Step 2: drain节点,优雅期 {self.GRACE_PERIOD_SECONDS}s")
    self.k8s_client.drain_node(
    node_name,
    grace_period=self.GRACE_PERIOD_SECONDS,
    ignore_daemonsets=True, # DaemonSet Pod不驱逐
    delete_emptydir_data=True # 清理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-runtime=remote –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_info=True)
    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(f"Node {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 API 15 原通过Docker CLI操作容器,改为调用K8s API
    改用BuildKit 5 原通过Docker socket构建镜像,改为远程BuildKit
    移除挂载 3 CI/CD Pipeline不再需要Docker CLI,直接移除

    15 个中风险日志驱动 Pod 全部切换到 Fluent Bit(K8s CRI 标准日志收集)。

    9 个低风险 Docker Compose 依赖通过迁移到 K8s Manifest 解决。

    3.2 迁移过程数据

    指标数据
    总Node数 1200
    每批Node数 60(5%)
    总批次 20
    迁移总时长 4周
    成功迁移Node 1197
    失败回退Node 3
    Pod驱逐成功率 99.8%
    服务中断次数 0

    3 个失败回退的 Node 原因分析:

    • 2 个 Node:containerd 安装后镜像缓存损坏,回退后重新迁移成功
    • 1 个 Node:kubelet 配置修改后启动失败,回退后排查为配置语法错误

    3.3 迁移后性能对比数据(生产环境)

    指标Docker(迁移前)containerd(迁移后)变化
    镜像拉取延迟 15.2s 9.3s ↓39%
    容器启动延迟 3.1s 2.0s ↓35%
    Node CPU开销 3.8% 2.1% ↓45%
    Node内存开销 280MB 165MB ↓41%
    日志收集吞吐 14MB/s 17MB/s ↑21%
    Pod调度延迟 5.8s 4.3s ↓26%
    镜像存储占用 45GB/Node 38GB/Node ↓16%

    3.4 迁移后的运维变化

    迁移后最显著的变化是运维复杂度的降低:

    • 不再需要维护 Docker Engine:containerd 是 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 exec + crictl 替代 Docker CLI
    • 安全扫描:改用 Trivy 的远程扫描模式,不需要在容器内运行 Docker CLI

    4.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 CLI(docker ps、docker logs、docker exec),containerd 使用 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)功能,进一步优化镜像拉取延迟。

    赞(0)
    未经允许不得转载:171主机测评 » 容器运行时从Docker到containerd的迁移复盘:兼容性验证、性能对比与零宕机切换方案
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址