1. 这不是“选哪个”的问题,而是“谁在什么位置干活”的问题

刚入行那会儿,我常被客户问:“我们该用 Kubernetes 还是 Docker?”——问得特别认真,眼神里带着技术选型的沉重感。后来我才明白,这问题本身就像在问“该用扳手还是螺丝刀”,听起来合理,实则错位。Docker 是个容器运行时,它负责把一个应用连同它的依赖、配置、文件系统一起打包成一个可移植的镜像,然后在宿主机上拉起一个隔离的进程环境来运行它。说白了,Docker 就是那个拧螺丝的工人,干的是单点执行的活。Kubernetes 则完全不同,它压根不碰具体的应用怎么启动、怎么读配置;它只管调度、编排、扩缩容、故障自愈、服务发现和流量治理——它是整个工地的项目经理兼调度中心,手里攥着几十上百台服务器的资源池,盯着成百上千个 Docker 容器(或其他兼容 OCI 的运行时)在哪儿跑、跑得稳不稳、要不要加人手、坏了谁来顶上。所以,“Kubernetes vs. Docker”这个标题,本质上是个伪命题。真正该问的是:我的应用规模、团队结构、发布节奏和稳定性要求,是否已经超出了单机或小集群手动管理 Docker 容器的能力边界?如果你还在用 docker run 启动三个服务,配个 docker-compose.yml 拉起来,那 Kubernetes 不仅是杀鸡用牛刀,更是给厨房装了一套核电站控制系统。但如果你每天要发布 20+ 次、服务间调用链超过 15 层、峰值流量是平日的 8 倍、要求 99.99% 的可用性,那 Docker 就只是你交付物的“出厂包装”,而 Kubernetes 才是你生产环境的“操作系统”。关键词 Kubernetes Docker 容器编排 微服务运维 云原生基础设施 ,全在这层分工逻辑里扎了根。这篇文章不是教你怎么二选一,而是带你亲手拆开这两个工具的“工作界面”,看清它们各自在哪条流水线上拧哪颗螺丝,以及当你的业务从手工作坊升级为智能工厂时,这条流水线该怎么重新布局。

2. 核心设计哲学与能力边界的硬核拆解

2.1 Docker:专注“单体交付”的极致封装者

Docker 的设计哲学,可以用一句话概括: 让软件运行环境从“描述文档”变成“可执行文件” 。在 Docker 出现之前,部署一个 Python Web 应用,你需要写一份《部署手册》,里面写着“请安装 Python 3.9、pip install -r requirements.txt、配置 Nginx 反向代理到 8000 端口、确保 /var/log/myapp 目录有写权限……”,这份文档在不同人的电脑上执行,十次有八次会出错。Docker 把所有这些“环境要求”全部固化进一个分层的、只读的镜像文件里。它的核心机制是 Union File System(联合文件系统) ,比如 Overlay2 驱动,它把基础镜像(如 ubuntu:22.04)、运行时依赖(如 python3.9、pip)、应用代码、配置文件,一层层叠在一起,最上层是可写的容器层。这种分层设计带来了两个关键优势:一是镜像复用率极高,100 个基于同一基础镜像的微服务,宿主机上只需存一份基础层;二是构建速度快,修改代码只重做最上层,下层缓存直接复用。我实测过一个中等规模的 Java 应用,从源码构建镜像,使用 Docker BuildKit 的 cache 机制,增量构建时间能从 6 分钟压到 42 秒。但 Docker 的能力边界也极其清晰:它不关心容器 A 和容器 B 之间怎么通信。 docker run --link 是早期的临时方案,早已废弃; docker network create 能建一个网桥,让同网络下的容器通过容器名互相 ping 通,但这只是 L2/L3 连通性,没有服务发现、没有负载均衡、没有健康检查。你无法告诉 Docker:“当用户访问 /api/orders 时,请把请求轮询转发给所有正在运行的 order-service 容器”,它没这个模块。它的 API 里也没有 “scale to 5 replicas” 这个命令。这就是为什么 docker-compose up -d --scale web=3 看似能扩缩容,但它本质是启动 3 个独立的、彼此无感知的容器实例,一旦其中一台宿主机宕机,这 3 个实例就全挂了,Docker 自己不会去另一台机器上补一个。它的可靠性模型,是单机维度的。

2.2 Kubernetes:面向“大规模协同”的分布式操作系统

如果说 Docker 解决了“软件怎么打包”,那么 Kubernetes 解决的就是“打包好的软件,在成百上千台机器上,怎么活着、怎么协作、怎么自我修复”。它的设计哲学是 声明式 API + 控制循环(Control Loop) 。你不需要告诉 Kubernetes “先创建 Pod A,再创建 Service B,再更新 Ingress C”,你只需要提交一个 YAML 文件,声明你想要的终态:“我需要 5 个 nginx 实例,暴露 80 端口,通过域名 myapp.com 访问”。Kubernetes 的各个控制器(Controller)——比如 ReplicaSet Controller、EndpointSlice Controller、Ingress Controller——会持续地“观察”集群当前状态,并与你声明的目标状态比对。如果发现只有 3 个 Pod 在运行,ReplicaSet Controller 就会立刻创建 2 个新的;如果某个 Pod 的健康探针失败,它会被自动驱逐并重建;如果新 Pod 启动成功,EndpointSlice Controller 就会把它的 IP 加入后端列表。这个“观测-比对-干预”的闭环,每秒都在集群内数以千计地并行发生。Kubernetes 的核心抽象对象,就是围绕这个闭环设计的:

  • Pod :最小的调度单元,不是容器,而是一个或多个紧密耦合的容器的集合(比如主应用容器 + 一个 sidecar 日志收集容器),共享网络命名空间和存储卷。这是它区别于 Docker 单容器模型的根本。
  • Service :一个稳定的网络端点(ClusterIP 或 NodePort),背后是一组动态变化的 Pod IP。它内置了 kube-proxy 组件,通过 iptables 或 IPVS 规则,实现集群内部的服务发现和四层负载均衡。你永远不用记住 Pod 的 IP,只认 Service 名。
  • Deployment :声明式地管理 Pod 的副本集和滚动更新策略。你可以定义 maxSurge=1, maxUnavailable=0,这意味着更新时最多多启 1 个新 Pod,且保证旧 Pod 一个都不能少,直到新 Pod 完全就绪——这是零停机发布的底层保障。
  • Ingress :七层(HTTP/HTTPS)的流量入口网关,支持基于 host 和 path 的路由规则、TLS 终止、重写等,把外部流量精准导入到不同的 Service。

提示:Kubernetes 本身不运行容器,它需要一个容器运行时(Container Runtime)。Docker Engine 曾是默认选择,但自 v1.20 起,Kubernetes 宣布弃用 dockershim,转而拥抱更轻量、更符合 OCI(Open Container Initiative)标准的运行时,如 containerd(Docker 自身也已将其作为底层运行时)或 CRI-O。这意味着,你在 Kubernetes 集群里看到的“容器”,其底层可能是 containerd 拉起的,Docker CLI 只是开发者本地的构建和调试工具。这个演进恰恰印证了二者关系的本质:Docker 是构建和分发的“前端”,Kubernetes 是运行和编排的“后端”。

2.3 关键能力对比:一张表看懂谁管什么

下表不是功能罗列,而是按“生产环境真实痛点”归类,告诉你每个能力在哪个环节起作用:

生产环境典型需求 Docker (单机/小集群) 是否原生支持 Kubernetes 是否原生支持 关键说明与实操影响
单机快速启动与调试 ✅ 原生强大 ⚠️ 过重,需 minikube/k3s docker run -p 8080:80 nginx 5 秒搞定;K8s 需先 kubectl apply -f pod.yaml ,再 kubectl port-forward ,至少 30 秒起步。
镜像构建与分发 ✅ 核心能力(Dockerfile + Registry) ❌ 无此模块 K8s 不管你怎么造轮子,只管轮子运过来后怎么装车。镜像构建仍是 Docker Build 或 BuildKit 的主场。
跨主机容器网络互通 ❌ 需第三方插件(Weave, Flannel) ✅ 原生(CNI 插件) Docker 默认 bridge 网络只限本机;K8s 通过 CNI(如 Calico)自动为每个 Pod 分配集群唯一 IP,跨节点直通。
服务发现与 DNS ❌ 仅限同网络容器名解析 ✅ 原生(CoreDNS) 在 K8s 里, curl http://orders-service:8080 在任何 Pod 里都有效;Docker 需手动维护 hosts 或用 Consul。
自动扩缩容(HPA) ❌ 无 ✅ 原生(Horizontal Pod Autoscaler) 基于 CPU/内存或自定义指标(如 QPS),自动调整 Deployment 的副本数。Docker Compose 的 scale 是静态指令。
滚动更新与回滚 ⚠️ Compose 支持,但无健康检查保障 ✅ 原生(Deployment 策略) K8s 更新时,新 Pod 必须通过 readinessProbe 才加入流量,livenessProbe 失败则重启,回滚 kubectl rollout undo 一键完成。
存储卷生命周期管理 ⚠️ docker volume 本地持久化 ✅ 原生(PV/PVC/StorageClass) K8s 抽象出持久卷(PV)和申领(PVC),可对接 NFS、AWS EBS、Ceph 等,生命周期独立于 Pod,数据不随 Pod 消亡而丢失。
多租户与资源配额 ❌ 无 ✅ 原生(Namespace + ResourceQuota) 一个集群可划分为 dev/test/prod Namespace,为每个 NS 设置 CPU/Memory 上限,避免一个项目吃光整台机器。

这张表的核心启示是:Docker 的价值在“交付链前端”,Kubernetes 的价值在“运行时后端”。它们不是竞争对手,而是上下游的协作者。一个健康的云原生交付流水线,必然是:开发者用 Docker 构建和测试镜像 → 镜像推送到私有 Registry → CI/CD 流水线触发 Kubernetes 的 kubectl apply → K8s 调度运行。试图用 Docker 替代 K8s 去管百台机器上的千个服务,就像用 Excel 表格去管理一个上市公司的财务总账——不是不能记,而是每次打开表格都要卡死,而且根本没法审计、没法预警、没法自动化。

3. 从零搭建一个可验证的对比实验环境

3.1 环境准备:轻量级但真实的双轨对照

为了让你亲手触摸两者的差异,我推荐一个零成本、零污染的本地实验方案: k3s + Docker Desktop 。k3s 是 Rancher 开源的轻量级 Kubernetes 发行版,专为边缘、CI/CD 和开发测试设计,二进制只有 50MB,内存占用 < 512MB, curl -sfL https://get.k3s.io | sh - 一条命令即可安装,1 分钟内启动一个单节点集群。Docker Desktop 则自带一个嵌入式的 k3s 集群(在设置里开启),同时保留完整的 Docker CLI 功能。这样,你能在同一台 Mac/Windows 电脑上,左手 docker run ,右手 kubectl apply ,实时对比。

注意:不要用 Minikube!它基于 VirtualBox/Vmware 创建完整 VM,启动慢、资源占用高、网络调试复杂。k3s 直接运行在宿主机 Linux 内核上(Docker Desktop 内部也是 LinuxKit),网络透明, localhost:30000 就能访问 NodePort,省去所有端口映射烦恼。

安装步骤精简如下:

  1. 下载并安装 Docker Desktop (最新版,已内置 k3s)。
  2. 启动 Docker Desktop,在右下角鲸鱼图标上右键 → “Settings” → “Kubernetes” → 勾选 “Enable Kubernetes”,点击 “Apply & Restart”。等待状态变为绿色 “Kubernetes is running”。
  3. 打开终端,验证:
    # 检查 Docker 是否就绪
    docker --version  # 应输出 Docker version 24.x.x
    docker run hello-world  # 第一次会下载镜像,看到欢迎信息即成功
    
    # 检查 Kubernetes 是否就绪
    kubectl version --short  # 应输出 Client & Server 版本
    kubectl get nodes  # 应看到一个 Ready 状态的节点
    kubectl get pods -A  # 应看到 coredns、local-path-provisioner 等系统 Pod Running
    
    此时,你的电脑就是一个微型的“混合云”:Docker 引擎负责构建和运行单个容器;k3s 集群负责编排和管理一组容器。接下来,我们用同一个应用——一个极简的 Python Flask API —— 来走通两条路径。

3.2 实验一:Docker 单机模式——从构建到运行的全流程

我们创建一个名为 simple-api 的应用,它只有一个 /health 接口返回 JSON。

  1. 创建项目目录:
    mkdir simple-api && cd simple-api
    
  2. 编写 app.py
    from flask import Flask
    import os
    app = Flask(__name__)
    
    @app.route('/health')
    def health():
        return {"status": "ok", "host": os.getenv('HOSTNAME', 'unknown')}
    
    if __name__ == '__main__':
        app.run(host='0.0.0.0:5000')
    
  3. 编写 requirements.txt
    Flask==2.3.3
    
  4. 编写 Dockerfile
    FROM python:3.11-slim
    WORKDIR /app
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    COPY . .
    EXPOSE 5000
    CMD ["python", "app.py"]
    
  5. 构建并运行:
    # 构建镜像,打标签为 simple-api:latest
    docker build -t simple-api:latest .
    
    # 启动容器,映射宿主机 5000 端口到容器 5000 端口
    docker run -d -p 5000:5000 --name api-v1 simple-api:latest
    
    # 验证
    curl http://localhost:5000/health  # 返回 {"status": "ok", "host": "a1b2c3d4e5"}
    docker ps  # 查看容器 ID 和状态
    
    这就是 Docker 的全部:构建(Build)、推送(Push,此处省略)、运行(Run)。现在,你有了一个运行中的服务。但问题来了:如果这个容器意外退出(比如 kill -9 它的 PID),它不会自动重启。你得手动 docker start api-v1 。如果想扩容到 3 个实例,得运行三次 docker run ,并手动管理三个不同的端口(5000, 5001, 5002),再自己搭个 Nginx 做负载均衡。这就是单机 Docker 的“天花板”。

3.3 实验二:Kubernetes 模式——声明式编排的威力

现在,我们用 Kubernetes 来管理同一个 simple-api 镜像,体验声明式的力量。

  1. 首先,确保镜像在 k3s 节点上可用。由于 k3s 和 Docker Desktop 共享同一个镜像仓库( /var/lib/rancher/k3s/agent/images ),我们直接将本地构建的镜像加载进去:

    # 将镜像保存为 tar 文件
    docker save simple-api:latest > simple-api.tar
    
    # 加载到 k3s 的镜像仓库(Docker Desktop 内置 k3s 的路径)
    sudo k3s ctr images import simple-api.tar
    

    实操心得:在生产环境,你绝不会手动 ctr import 。正确的流程是: docker build docker push 到私有 Registry(如 Harbor),然后在 K8s 的 Deployment YAML 中指定 image: harbor.example.com/myproject/simple-api:v1.0 。这里手动加载,只为实验快速验证。

  2. 编写 deployment.yaml ,定义应用的“终态”:

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: simple-api
      labels:
        app: simple-api
    spec:
      replicas: 3  # 我要 3 个副本
      selector:
        matchLabels:
          app: simple-api
      template:
        metadata:
          labels:
            app: simple-api
        spec:
          containers:
          - name: api
            image: simple-api:latest  # 使用我们刚加载的镜像
            ports:
            - containerPort: 5000
            livenessProbe:  # 存活探针:每 10 秒检查一次 /health
              httpGet:
                path: /health
                port: 5000
              initialDelaySeconds: 5
              periodSeconds: 10
            readinessProbe:  # 就绪探针:启动后立即检查,通过才加入流量
              httpGet:
                path: /health
                port: 5000
              initialDelaySeconds: 2
              periodSeconds: 5
    ---
    # 定义 Service,为这 3 个 Pod 提供稳定入口
    apiVersion: v1
    kind: Service
    metadata:
      name: simple-api-service
    spec:
      selector:
        app: simple-api
      ports:
      - protocol: TCP
        port: 80
        targetPort: 5000
      type: NodePort  # 暴露到宿主机端口,便于本地访问
    

    这个 YAML 文件,就是你对 Kubernetes 的“下单”。它没有一句“启动”、“停止”、“重启”的命令,只有“我要 3 个,它们必须健康,必须就绪,必须通过 80 端口提供服务”。

  3. 应用配置并验证:

    # 应用 YAML
    kubectl apply -f deployment.yaml
    
    # 查看 Deployment 状态
    kubectl get deployments  # 应显示 simple-api 的 READY 3/3
    
    # 查看 Pod 状态(会看到 3 个 Running 的 Pod)
    kubectl get pods -l app=simple-api
    
    # 查看 Service,找到分配的 NodePort(通常是 30000-32767 之间)
    kubectl get service simple-api-service
    
    # 通过 NodePort 访问(假设 NodePort 是 31234)
    curl http://localhost:31234/health
    

    你会得到类似 {"status": "ok", "host": "simple-api-7c8d9b4f5-abcde"} 的响应, host 字段是 Pod 的名字,证明流量确实打到了后端的某个 Pod 上。

  4. 亲手破坏,见证自愈

    • 找到一个 Pod 的名字: kubectl get pods -l app=simple-api
    • 删除它: kubectl delete pod simple-api-7c8d9b4f5-abcde
    • 立刻执行 kubectl get pods -l app=simple-api ,你会发现:旧 Pod 状态变为 Terminating ,几秒后,一个新的 Pod(名字不同)以 ContainerCreating 状态出现,很快变成 Running 。整个过程无需人工干预,ReplicaSet Controller 自动补足了副本数。
    • 再次 curl http://localhost:31234/health ,服务依然可用。这就是 Kubernetes 的“韧性”。

3.4 实验三:横向对比——一次发布,两种体验

现在,我们模拟一次真实发布:将应用升级到 v2 版本,返回不同的健康状态。

  1. 修改 app.py ,添加版本标识:

    @app.route('/health')
    def health():
        return {"status": "ok", "version": "v2", "host": os.getenv('HOSTNAME', 'unknown')}
    
  2. 重新构建镜像并打新标签:

    docker build -t simple-api:v2 .
    sudo k3s ctr images import simple-api.tar  # 再次加载
    
  3. Docker 方式升级

    • 停止旧容器: docker stop api-v1
    • 删除旧容器: docker rm api-v1
    • 启动新容器: docker run -d -p 5000:5000 --name api-v2 simple-api:v2
    • 验证: curl http://localhost:5000/health → 看到 "version": "v2"
    • 问题 :这期间有服务中断(stop 到 run 的间隙),且旧容器的 v1 镜像还占着磁盘空间,需手动 docker rmi 清理。
  4. Kubernetes 方式升级(滚动更新)

    • 修改 deployment.yaml 中的 image: simple-api:v2 ,然后:
      kubectl apply -f deployment.yaml
      
    • 实时观察滚动过程:
      kubectl rollout status deployment/simple-api  # 等待 "deployment \"simple-api\" successfully rolled out"
      kubectl get pods -l app=simple-api  # 会看到旧 Pod 逐步 Terminating,新 Pod 逐步 Running
      
    • 关键验证 :在整个滚动过程中,持续执行 curl http://localhost:31234/health ,你会发现响应始终成功,只是 version 字段从 v1 逐渐变为 v2 。这是因为 K8s 严格遵循 maxSurge maxUnavailable 策略(默认值),确保流量不中断。
    • 一键回滚 :如果 v2 版本上线后发现问题, kubectl rollout undo deployment/simple-api ,瞬间回到 v1 版本,所有 Pod 重新拉起。

这个实验的价值在于:它剥离了所有云厂商的包装,让你在自己电脑上,用最原始的命令,亲手触摸到两种范式的温度。Docker 给你的是“确定性”——你敲下 run ,它就启动;Kubernetes 给你的是“确定性之上的弹性”——你声明“我要 3 个”,它就给你 3 个,无论机器重启、网络抖动、进程崩溃,它都默默守护着这个数字。选择哪一个,从来不是技术优劣的辩论,而是你当前业务水位线的诚实映射。

4. 真实世界中的选型决策树与避坑指南

4.1 什么时候坚决用 Docker,而不是 Kubernetes?

我见过太多团队,因为“Kubernetes 很火”就盲目上马,结果半年后 DevOps 工程师离职,整个集群没人敢动,CI/CD 流水线瘫痪,最后降级回 docker-compose 。这不是技术倒退,而是回归理性。以下场景,Docker(或 docker-compose )是更优解:

  • 个人开发者或小团队(< 3 人)的日常开发与测试 :你正在写一个博客系统,本地用 docker-compose up 启动 MySQL、Redis、Nginx 和你的 PHP 应用,所有服务通过 docker-compose.yml networks depends_on 关联。此时,Kubernetes 的 YAML 文件、 kubectl 命令、Namespace 概念,只会增加认知负担,毫无收益。 docker-compose build up logs exec 命令,简洁高效,就是为你量身定做的。

  • CI/CD 流水线中的构建与测试环节 :在 GitHub Actions 或 GitLab CI 中,你用 docker build 构建镜像,用 docker run 启动一个临时数据库容器来跑单元测试。这里的容器是“一次性的、短暂的、隔离的”,Kubernetes 的调度、持久化、服务发现全是冗余。Docker 的轻量和快速启动,是 CI 环境的生命线。

  • 嵌入式设备或边缘计算节点(资源极度受限) :一个运行在树莓派上的家庭自动化网关,只需要启动 2-3 个容器(MQTT Broker、Home Assistant、Node-RED)。k3s 虽然轻量,但其 etcd、kube-apiserver、controller-manager 等组件仍需至少 512MB 内存和稳定的存储。而一个 dockerd 进程,256MB 内存就能稳稳运行。在这种场景下,追求“Kubernetes 兼容性”是舍本逐末。

  • 遗留系统容器化改造的过渡期 :一个单体 Java 应用,你把它打包成 Docker 镜像,用 docker run -d --restart=always 启动,配上 docker logs -f 查看日志。这已经比以前 java -jar app.jar & 启动强太多了。此时强行拆分成十几个微服务,再上 Kubernetes,只会把一个简单问题复杂化。Docker 是容器化的第一步,也是最坚实的基础。

实操心得:判断是否需要 Kubernetes,有一个朴素的“三问法”:

  1. 我的服务实例数,是否经常需要动态增减(非手动)? 如果答案是“否”,K8s 的 HPA 就是摆设。
  2. 我的服务,是否部署在 3 台以上的物理/虚拟机上? 如果所有容器都在一台机器上,K8s 的跨节点调度、故障转移能力就无从谈起。
  3. 我的团队,是否有专人(或至少一人)能理解并维护 kubectl 、YAML、Helm、Prometheus 这套生态? 如果没有,K8s 不是加速器,而是定时炸弹。宁可先用 docker-compose + systemd 做好服务管理,等团队能力跟上再平滑迁移。

4.2 什么时候 Kubernetes 成为唯一选择?

当你的业务规模和复杂度,已经让 Docker 的“单点管理”模式不堪重负时,Kubernetes 就不再是选项,而是必需品。以下是几个典型的临界点信号:

  • 服务网格(Service Mesh)成为刚需 :当你的微服务数量超过 50 个,服务间调用链路复杂,需要统一的可观测性(Tracing)、流量控制(Canary Release)、安全策略(mTLS)。Istio、Linkerd 这些服务网格,其数据平面(Envoy Proxy)必须注入到每个 Pod 中,控制平面则深度集成在 Kubernetes 的 CRD(Custom Resource Definition)之上。你无法脱离 K8s 的声明式 API 和强大的 Admission Control(准入控制)来部署和管理它们。此时,Docker 的 --network 参数,连入门门槛都够不到。

  • 多云/混合云架构落地 :你的业务既要跑在 AWS 上,也要跑在阿里云上,还要在客户私有数据中心部署。Kubernetes 的核心价值之一,就是提供了统一的、标准化的 API 抽象层。你写一套 deployment.yaml ,在 AWS EKS、阿里云 ACK、本地 k3s 上都能运行,只需更换底层的 CNI(网络)和 CSI(存储)插件。而 Docker 的 run 命令,在不同云厂商的 VPC 网络、安全组、负载均衡配置下,需要写无数份适配脚本,维护成本指数级上升。

  • Serverless 架构的底层支撑 :如今流行的 Knative、OpenFaaS、KEDA 等 FaaS(函数即服务)平台,其核心就是 Kubernetes 的事件驱动扩展。它们监听 Kafka 主题、S3 对象上传、HTTP 请求等事件,动态创建 Pod 来执行函数,执行完自动销毁。这个“按需启停、毫秒级伸缩”的能力,完全建立在 K8s 的调度器(Scheduler)和控制器(Controller)之上。Docker 本身不具备事件监听和自动扩缩的机制。

  • AI/ML 训练任务的批量调度 :一个 PyTorch 分布式训练任务,需要同时启动 8 个 GPU Pod,它们之间通过 RDMA 网络高速通信。Kubernetes 的 Device Plugin 机制可以精确地将 GPU 设备分配给 Pod,而 Job CronJob 对象可以声明式地定义任务的并行度、重试策略和超时时间。你无法用 docker run 来协调 8 个容器的启动顺序、网络拓扑和资源绑定。

4.3 从 Docker 到 Kubernetes 的平滑迁移路径

很多团队最大的误区,是把迁移当成一场“大换血”。正确的做法,是把它看作一次“渐进式升级”。我亲历过三个成功案例,总结出一条黄金路径:

阶段一:容器化先行(Docker Only)

  • 目标:所有应用(包括数据库、中间件)都打包成 Docker 镜像,通过 docker-compose 在预发环境运行。
  • 关键动作:建立公司级的 Dockerfile 最佳实践(多阶段构建、非 root 用户、固定 UID/GID)、私有镜像仓库(Harbor)、镜像扫描(Trivy)。
  • 成果:交付物标准化,环境一致性问题消失 80%,这是后续一切的基础。

阶段二:Kubernetes 试点(K8s for Stateful Apps)

  • 目标:选择一个 无状态、流量不大、但对稳定性要求极高 的核心服务(如公司内部的 SSO 认证服务),迁移到 Kubernetes。
  • 关键动作:用 Helm Chart 封装该服务的 Deployment、Service、Ingress、ConfigMap;接入 Prometheus + Grafana 做基础监控;配置 livenessProbe readinessProbe
  • 成果:团队熟悉了 kubectl 、YAML、Helm,验证了 K8s 的自愈能力,建立了第一个“成功样板”。

阶段三:平台化赋能(K8s as a Platform)

  • 目标:将 Kubernetes 抽象为一个内部 PaaS 平台,为所有业务线提供自助服务。
  • 关键动作:开发一个简单的 Web 控制台或 CLI 工具,业务方只需填写应用名、Git 仓库地址、端口号,后台自动生成 Helm Chart 并 helm install ;集成 CI/CD, git push 自动触发构建、镜像推送、K8s 部署。
  • 成果:DevOps 团队从“救火队员”转变为“平台建设者”,业务迭代速度提升 3 倍以上。

常见问题速查表(来自我踩过的坑):

问题现象 根本原因 解决方案 我的教训
Pod 一直 Pending, kubectl describe pod 显示 0/1 nodes are available: 1 node(s) had taints that the pod didn't tolerate. 节点被打上了 NoSchedule 污点(如 node-role.kubernetes.io/control-plane:NoSchedule ),而你的 Pod 没有容忍(toleration)该污点。 在 Pod 的 spec 中添加 tolerations 字段,或使用 kubectl taint nodes --all node-role.kubernetes.io/control-plane:NoSchedule- 移除污点(仅限单节点测试)。 k3s 默认会给 master 节点打污点,防止普通 Pod 调度上去。这是安全设计,不是 bug。别急着删,先学会容忍。
Service 的 ClusterIP 无法从 Pod 内部访问, curl http://my-service 超时 CoreDNS 解析失败,或 Service 的 selector 标签与 Pod 的 labels 不匹配。 kubectl get endpoints my-service 看后端列表是否为空; kubectl get pods --show-labels 确认标签; kubectl exec -it a-pod -- nslookup my-service 测试 DNS。 标签(label)是 K8s 的“胶水”,90% 的网络问题都源于标签对不上。养成习惯: kubectl get pods -l app=myapp kubectl get svc -l app=myapp 一起看。
Ingress 404, kubectl get ingress 显示 ADDRESS 为空 Ingress Controller(如 nginx-ingress)未安装,或未正确关联到你的 Ingress 资源。 kubectl get pods -n ingress-nginx 确认 Controller Pod Running;检查 Ingress 的 ingressClassName 是否与 Controller 的 ingressclass 名称一致。 Ingress 是一个“接口”,不是“实现”。你必须先部署一个具体的 Controller(如 nginx、traefik),它才会监听并处理你的 Ingress 资源。
kubectl logs 查不到日志,显示 Error from server: no such file or directory k3s 默认使用 containerd 作为运行时,其日志路径与 Docker 不同,且 kubectl logs 依赖 cri-o containerd 的日志插件。 确保 k3s 启动时启用了 --disable-agent (不适用)或检查 containerd 配置;更可靠的方式是 kubectl exec -it pod-name -- cat /proc/1/fd/1 直接读取 stdout。 不要迷信 kubectl logs 。在 k3s 环境, crictl logs (k3s 自带的 CRI 工具)往往更准确。

5. 总结:工具

更多推荐