Docker到Kubernetes迁移:容器、编排与Pod核心概念解析
1. 项目概述:这不是一次工具切换,而是一场基础设施思维的重构
“从 Docker 到 Kubernetes:概念对照与迁移”——这八个字背后,藏着过去五年里我亲手带过的17个团队踩过的坑、重装过的23台开发机、删掉又重建的400多个YAML文件,以及凌晨三点盯着kubectl get pods命令输出时那杯早已凉透的咖啡。它不是教你怎么敲几行命令,而是帮你把脑子里那个“Docker 就是 run 一个镜像”的旧地图,换成一张标着 Service、Ingress、StatefulSet 和 Operator 的新作战图。核心关键词就三个:容器、编排、Pod——它们像三把钥匙,分别打开隔离、调度和抽象的大门。如果你还在用 docker run -p 8080:8080 启动一个 Spring Boot 应用,然后靠 docker ps | grep java 查进程,再手动 docker exec -it 进去改配置,那你就是这张旧地图的典型用户;而当你开始思考“这个 Java 应用该跑几个副本?”、“数据库连接池怎么随 Pod 数量自动伸缩?”、“服务A调用服务B,IP变了怎么办?”,你就已经站在 Kubernetes 的入口了。这篇文章不讲虚的,只讲我每天在生产环境里真实操作的东西:怎么把一个用 Docker Compose 跑得好好的本地项目,一砖一瓦地搬上 K8s 集群,过程中哪些概念必须掰开揉碎,哪些命令必须背下来,哪些报错信息一看就知道是哪个环节出了问题。它适合两类人:一类是刚学完 Docker 基础、想往云原生深挖的开发者;另一类是运维老手,正被老板催着“把现有服务容器化并上 K8s”。别怕,我们从最基础的对照表开始,就像学开车先认仪表盘一样。
2. 核心概念对照:为什么不能直接把 docker-compose.yml 拖进 kubectl?
2.1 容器(Container):从“单兵作战”到“班组编制”
Docker 里的容器,本质是一个进程的封装。你执行 docker run nginx:alpine,系统就拉起一个 Nginx 进程,配上自己的 rootfs、网络命名空间和 cgroups 限制,它就是一个独立的、可运行的最小单元。这很像一个士兵:有武器(镜像)、有制服(namespace)、有活动范围(cgroup limit),但没有指挥链,没有战友协同,打完一仗就撤。Kubernetes 里的容器,从来不是单独存在的。它必须被包裹在一个叫 Pod 的结构里。Pod 是 K8s 最小的调度单位,不是容器。一个 Pod 可以包含一个或多个紧密耦合的容器,它们共享同一个网络命名空间(所以 localhost 互通)、同一个 IPC 命名空间、同一个存储卷(Volume)。举个实在的例子:你的 Java Web 应用容器,旁边通常会配一个 sidecar 容器,比如一个日志收集器(Filebeat)或者一个服务网格代理(Istio 的 Envoy)。这两个容器在物理上是两个进程,但在 K8s 眼里,它们属于同一个 Pod,共享 IP 和端口,生命周期也绑定在一起——Pod 启动,两个容器一起启动;Pod 终止,两个容器一起终止。这就像一个战斗班组:Java 应用是主战兵,Filebeat 是通信兵,他们共用一辆装甲车(Pod),车上有统一的电台频率(网络命名空间)和弹药箱(Volume)。你永远无法在 K8s 里直接创建一个“裸容器”,就像你不能在军队里只招一个兵而不给他编入班排连。所以,第一步迁移,就是把 docker-compose.yml 里每一个 service,都理解为未来要放进一个 Pod 里的一个容器。如果一个 service 依赖另一个 service 的端口,比如 app 依赖 redis,在 Docker Compose 里你写 redis:6379,在 K8s 里,你得先确保 redis 服务被暴露成一个 Service 对象,然后 app 容器通过 redis.default.svc.cluster.local:6379 来访问——这是服务发现,不是 IP 直连。
2.2 编排(Orchestration):从“手工点菜”到“智能厨房调度”
Docker Compose 的 docker-compose up 命令,本质上是一份“点菜清单”。你告诉 Docker:“我要一份 Nginx、一份 Redis、一份 MySQL,按这个顺序上,Nginx 启动后等 5 秒再启 Redis”。它执行的是线性、确定性的流程,所有动作都在一台宿主机上完成。而 Kubernetes 的编排,是“智能厨房调度”。你提交的不是菜单,而是一份“最终状态声明”(Declarative State)。你写一个 YAML 文件,声明:“我需要 3 个 app Pod,每个都带 2G 内存;我需要一个 redis Service,类型是 ClusterIP;我需要一个 nginx Ingress,把 example.com/app 的流量路由到 app Service”。K8s 的控制平面(Control Plane)——包括 API Server、Scheduler、Controller Manager——会持续监听这个声明,并不断对比集群当前的实际状态(Actual State)和你声明的目标状态(Desired State)。如果某个 app Pod 因为宿主机宕机而消失了,Controller Manager 会立刻发现“哦,少了一个”,然后通知 Scheduler 在另一台健康的节点上,重新调度一个一模一样的 Pod。这个过程完全自动,无需人工干预。这就是“自愈能力”。Docker Compose 没有这种能力。你 docker-compose down 之后,所有容器就没了,除非你再 up 一次。K8s 的编排还意味着资源感知。Scheduler 不会随便找个节点塞一个 Pod,它会看这个节点还有多少 CPU、内存空闲,会不会超过 ResourceQuota 限制,甚至考虑亲和性(Affinity)——比如“把数据库 Pod 和它的备份 Pod 分开部署在不同机架上,避免单点故障”。这种基于策略的、动态的、分布式的调度,是 Docker Compose 这种单机编排工具根本无法企及的。所以,迁移的核心,不是把 docker-compose.yml 的语法翻译成 YAML,而是把“我要运行这些服务”的思维,升级为“我要维持这些服务的稳定、弹性、可观测的最终状态”。
2.3 Pod:Kubernetes 的原子单位与一切的起点
Pod 是 Kubernetes 里绕不开的基石,也是初学者最容易误解的概念。很多人以为 Pod 就是“一个容器”,这是大错特错。Pod 是一个逻辑上的“运行时环境”,它本身不运行代码,它只是为里面的容器提供了一个共享的上下文。你可以把它想象成一个“气泡”:气泡里可以有一个容器,也可以有两个、三个。气泡提供了网络、存储、生命周期管理。为什么设计成这样?因为现实世界的应用很少是单体的。一个微服务应用,除了主业务进程,往往还需要:
- 一个日志轮转器(logrotate),防止磁盘被日志撑爆;
- 一个配置热更新器(confd),监听配置中心变化并重载 Nginx;
- 一个健康检查探针(health check sidecar),专门负责向主进程发
/health请求并上报给 K8s。
把这些功能都塞进主容器的镜像里?不行。那会让镜像臃肿、职责混乱、升级困难。K8s 的解法是:主容器 + Sidecar 容器,同处一个 Pod。它们共享 localhost,所以主进程监听 :8080,Sidecar 可以直接 curl http://localhost:8080/health;它们共享 Volume,所以主进程写日志到 /var/log/app.log,Sidecar 可以直接读取这个路径进行收集。这才是 Pod 的真正价值:它让“一个应用”这个概念,在容器时代有了更精细、更符合实际的表达。因此,在迁移时,你必须对每一个 docker-compose.yml 中的 service 进行审视:它是否真的只需要一个容器?还是它天然就需要一个搭档?比如,一个用 docker-compose 启动的 nginx 服务,如果它需要反向代理后端 Java 服务,那么在 K8s 里,你很可能需要一个 nginx Pod(里面只有 nginx 容器)和一个 app Pod(里面是 Java 容器),再通过 Service 把它们连起来。而如果你的 nginx 还需要做 JWT 验证,那可能就得在 nginx Pod 里加一个 auth-service sidecar。Pod 是一切的起点,所有其他对象(Deployment、Service、Ingress)都是围绕 Pod 构建的抽象。不理解 Pod,就永远在 K8s 的门外徘徊。
3. 迁移实操:四步走,把本地 Docker 项目搬上 K8s 集群
3.1 第一步:环境准备与工具链搭建——别让环境拖垮你的第一次尝试
在动手写 YAML 之前,你必须确保手头有一套能用的、稳定的 K8s 环境。别一上来就冲生产集群,那是给自己挖坑。我的建议是:本地用 Kind(Kubernetes in Docker),测试用 Minikube,预发/生产用云厂商托管集群(如 EKS/GKE/AKS)。Kind 是目前最轻量、最接近真实集群的本地方案。它用 Docker 容器来模拟 K8s 的各个组件(control plane 和 worker node),启动快、资源占用小、网络模型和真实集群一致。安装 Kind 只需一条命令:curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64 && chmod +x ./kind && sudo mv ./kind /usr/local/bin/。然后创建一个单节点集群:kind create cluster --name my-cluster。这条命令会在后台拉起一个名为 kind-control-plane 的容器,它既是 master 也是 worker。验证:kubectl cluster-info 和 kubectl get nodes。此时,你的 kubectl 已经指向了这个本地集群。接下来,你需要一个可靠的镜像仓库。Docker Hub 有速率限制,且公有镜像安全性堪忧。我强烈推荐使用 Harbor 或者云厂商的私有仓库(如阿里云 ACR、腾讯云 TCR)。对于本地开发,你可以用 registry:2 镜像快速搭一个:docker run -d -p 5000:5000 --restart=always --name registry registry:2。然后把你的应用镜像推送到 localhost:5000/my-app:1.0。最后,必备的 CLI 工具链:
kubectl:K8s 的瑞士军刀,必须熟练;kubectx/kubens:快速切换上下文(context)和命名空间(namespace),避免kubectl --context=prod --namespace=default get pods这种长命令;stern:实时 tail 多个 Pod 的日志,比kubectl logs -f强十倍;k9s:终端里的 K8s UI,可视化查看资源状态,排查问题神器。
提示:在
kind集群里,localhost:5000是指宿主机的localhost,而不是kind容器内部的localhost。所以你在kind里拉取镜像时,要写localhost:5000/my-app:1.0,K8s 会自动解析到宿主机的 5000 端口。这是很多新手卡住的第一关。
3.2 第二步:从 docker-compose.yml 到基础 YAML —— 手动拆解,拒绝一键转换
网上有很多 docker-compose-to-k8s 的在线转换工具,我劝你千万别用。它们生成的 YAML 往往是“能跑”,但离“能用”、“能维护”差十万八千里。真正的迁移,必须手动拆解,理解每一行的意义。我们以一个极简的 docker-compose.yml 为例:
现在,我们把它一步步变成 K8s 的 YAML。首先,redis 服务。在 K8s 里,它对应一个 Deployment(定义如何运行 Pod)和一个 Service(定义如何访问 Pod)。
注意几个关键点:
selector和template.metadata.labels必须严格匹配,这是 Service 找到 Pod 的唯一方式;command和args是分开的,command替换镜像的ENTRYPOINT,args替换CMD,这是为了清晰和可覆盖;resources是必须设置的!没有 limits 的 Pod 会吃光节点资源,没有 requests 的 Pod 会被 Scheduler 认为“不需要资源”,导致调度失败。这里的250m表示 0.25 个 CPU 核心,64Mi是 64 兆字节内存,这是根据 Redis 官方文档推荐的最小值设定的。
接着是 app 服务。它需要构建镜像,所以我们先 docker build -t localhost:5000/my-app:1.0 ./app,再 docker push localhost:5000/my-app:1.0。然后写它的 Deployment:
这里的关键是 DB_HOST 的值。在 Docker Compose 里,它是 redis,因为 Compose 会自动创建一个 Docker 网络,所有服务名就是 DNS 名。在 K8s 里,DNS 规则是 <service-name>.<namespace>.svc.cluster.local。redis 服务在 default 命名空间,所以全名是 redis.default.svc.cluster.local。livenessProbe 和 readinessProbe 是 K8s 的生命线。liveness 是“你还活着吗?”,死了就重启;readiness 是“你准备好接客了吗?”,没准备好就不把流量导给你。这两个探针的配置,直接决定了你的服务是否稳定。最后是 web 服务。它需要对外暴露,所以除了 Deployment,还需要一个 Service(ClusterIP)和一个 Ingress(七层路由):
Ingress 是 K8s 的标准七层网关抽象。它本身不干活,需要一个 Ingress Controller(如 nginx-ingress、traefik)来实现。在 kind 集群里,你可以用 kind load docker-image nginx-ingress-controller 加载镜像,然后部署官方的 ingress-nginx。host: example.com 是域名,你需要在本地 /etc/hosts 里加上 127.0.0.1 example.com,才能用浏览器访问。
3.3 第三步:部署、验证与调试——kubectl 是你的眼睛和手
所有 YAML 文件写好后,就是部署时刻。不要一股脑 kubectl apply -f .,那会出大问题。我的习惯是分组、分步、带 -v 6(详细日志):
部署后,验证的黄金三步法:
- 看 Pod 状态:
kubectl get pods。理想状态是Running。如果卡在Pending,说明调度失败,kubectl describe pod <pod-name>看 Events,通常是资源不足或镜像拉取失败;如果卡在ContainerCreating,kubectl describe看 Events,常见原因是ImagePullBackOff(镜像名错了或仓库不可达);如果CrashLoopBackOff,说明容器启动就崩溃,kubectl logs <pod-name>看日志。 - 看 Service 的 Endpoints:
kubectl get endpoints redis。如果输出是<none>,说明 Service 的selector没匹配到任何 Pod 的 label,回去检查deployment.yaml里的selector和template.metadata.labels。 - 看 Ingress 的地址:
kubectl get ingress。ADDRESS列应该有 IP。如果没有,说明Ingress Controller没装好或没运行。kubectl get pods -n ingress-nginx看 controller 的 Pod 是否 Running。
调试时,kubectl exec 是你的手术刀。比如,你想确认 app Pod 能否连通 redis:
注意:
kubectl exec进入的是 Pod 的第一个容器。如果 Pod 有多个容器,要用-c <container-name>指定。-- sh后面的sh是进入 shell,有些镜像用bash,记得先kubectl exec <pod> -- which bash确认。
3.4 第四步:进阶优化与生产就绪——别让“能跑”变成“不敢动”
当你的服务在 kind 里跑通了,恭喜你迈过了第一道坎。但离生产就绪,还有很长的路。以下是我在真实项目中总结的、必须做的五件事:
- 命名空间(Namespace)隔离:别把所有东西都扔在
default命名空间。按环境(dev/staging/prod)或按业务域(user-service/payment-service)划分。kubectl create namespace prod,然后在所有 YAML 的metadata.namespace字段里填上prod。这不仅是组织,更是安全边界。 - ConfigMap 与 Secret 管理配置:把
DB_HOST、DB_PASSWORD这些硬编码从 Deployment 里抽出来。kubectl create configmap app-config --from-literal=DB_HOST=redis.prod.svc.cluster.local,kubectl create secret generic app-secret --from-literal=DB_PASSWORD=xxx。然后在 Deployment 的envFrom字段里引用。Secret 的值是 base64 编码的,但kubectl会自动处理。 - 持久化存储(PersistentVolume):Redis 的
--appendonly yes会把数据写到/data目录。默认 Pod 的文件系统是临时的,重启就丢。你需要一个PersistentVolumeClaim(PVC),指向一个PersistentVolume(PV)。在kind里,可以用hostPath类型的 PV;在云上,用云盘(EBS/EVS)。 - Horizontal Pod Autoscaler(HPA):让
appDeployment 根据 CPU 使用率自动扩缩容。kubectl autoscale deployment app --cpu-percent=50 --min=1 --max=10。K8s 会监控app的平均 CPU 使用率,超过 50% 就加 Pod,低于 50% 就减。 - Helm Chart 封装:当你的 YAML 文件超过 10 个,手动
apply就是灾难。用 Helm 把它们打包成一个 Chart。helm create my-app会生成标准目录结构,把所有 YAML 放进templates/,用 Go 模板语法({{ .Values.replicaCount }})参数化。helm install my-app ./my-app一键部署。这是团队协作和 CI/CD 的基石。
4. 常见问题与排查技巧实录:那些让我熬夜的报错,现在都给你列出来了
4.1 “Virtualization support not detected” —— Docker Desktop 启动失败的终极解法
这个错误信息,几乎出现在每一篇 Windows 用户的 Docker Desktop 教程评论区里。它不是 K8s 的问题,而是 Docker Desktop 自身的依赖没满足。根本原因只有一个:Windows 的 WSL2(Windows Subsystem for Linux 2)没有启用,或者 BIOS 里的虚拟化(VT-x/AMD-V)被关闭了。解决方案分三步:
- BIOS 设置:重启电脑,狂按
F2/Del进 BIOS,找到Advanced->CPU Configuration,把Intel Virtualization Technology(或SVM Mode)设为Enabled。保存退出。 - 启用 WSL2:以管理员身份打开 PowerShell,依次执行:重启电脑。然后下载 WSL2 Linux kernel update package 并安装。最后,POWERSHELLdism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestartdism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
wsl --set-default-version 2。 - Docker Desktop 设置:打开 Docker Desktop 设置(Settings)-> General,勾选
Use the WSL 2 based engine;再点 Resources -> WSL Integration,把你的发行版(如 Ubuntu)勾上。重启 Docker Desktop。
实操心得:我见过太多人卡在这一步,反复重装 Docker Desktop 十几次。记住,99% 的情况,问题不在 Docker,而在 WSL2 和 BIOS。先搞定这两样,Docker Desktop 自然就活了。
4.2 “Unable to connect to the server: EOF” —— kubectl 连不上集群的排查链
当你 kubectl get nodes 报这个错,说明 kubectl 的配置(kubeconfig)指向了一个不存在或已失效的集群。排查链必须按顺序:
- Step 1:检查 kubeconfig:
kubectl config view。看current-context是什么,再看contexts里对应的cluster名,最后看clusters里这个cluster的server地址。kind创建的集群,server通常是https://127.0.0.1:32768这样的地址。 - Step 2:检查端口是否监听:
netstat -tuln | grep 32768(Linux/macOS)或netstat -ano | findstr :32768(Windows)。如果没输出,说明kind集群根本没起来。kind delete cluster,再kind create cluster。 - Step 3:检查证书:
kubectl config view --raw --minify --flatten会输出当前生效的完整配置。把certificate-authority-data的值(一长串 base64)复制出来,echo "<base64>" | base64 -d > ca.crt,然后openssl x509 -in ca.crt -text -noout。看Not Before和Not After,如果证书过期了(kind集群的证书默认一年),就必须重建集群。 - Step 4:检查网络代理:如果你公司用了全局代理,
kubectl也会走代理,而代理服务器无法访问127.0.0.1。export NO_PROXY="127.0.0.1,localhost"(Linux/macOS)或set NO_PROXY=127.0.0.1,localhost(Windows)。
4.3 “CrashLoopBackOff” —— 容器启动即崩溃的万能诊断法
这是 K8s 里最让人抓狂的状态。容器启动,运行几秒,崩溃,K8s 重启,再崩溃……循环。诊断法如下:
- 先看最近一次的日志:
kubectl logs <pod-name> --previous。--previous是关键,它看的是上一次崩溃前的日志,而不是重启后的新日志。 - 如果日志为空,看事件:
kubectl describe pod <pod-name>,重点看Events部分。常见事件:Failed to pull image "xxx":镜像名拼错,或仓库权限不够;Back-off restarting failed container:容器进程退出码非 0,通常是应用启动失败;OOMKilled:内存超限被系统杀死,limits.memory设太小了。
- 如果还找不到,进容器看现场:
kubectl exec -it <pod-name> -- sh。如果进不去,说明容器根本没起来。那就用kubectl debug(K8s 1.20+)创建一个临时调试容器:kubectl debug -it <pod-name> --image=busybox:1.35。这个busybox容器会共享目标 Pod 的所有命名空间(network、pid、ipc),你可以用ps aux看进程,用netstat -tuln看端口,用df -h看磁盘。
4.4 “No endpoints for service” —— Service 找不到后端 Pod 的血泪教训
kubectl get endpoints <service-name> 输出 <none>,这是新手必经之路。原因无非三点:
- Selector 不匹配:
kubectl get deployment <deployment-name> -o wide,看LABELS列;kubectl get pods --show-labels,看 Pod 的 labels。两者必须完全一致。matchLabels是精确匹配,matchExpressions是表达式匹配,新手用matchLabels就够了。 - Pod 没 Running:
kubectl get pods,如果 Pod 是Pending、ContainerCreating或Error,Service 就不会把它们加进来。先解决 Pod 的问题。 - 命名空间不一致:
kubectl get service -n <ns>和kubectl get pods -n <ns>,必须在同一个命名空间下操作。kubectl get service默认是default,而你的 Pod 可能在dev命名空间。
实操心得:我曾经因为一个空格,debug 了 3 小时。
selector:下面的app: web,我多打了一个空格,变成了app: web(两个空格)。YAML 解析器认为这是两个 key,app的值是空字符串,自然匹配不到任何 Pod。所以,永远用kubectl apply --dry-run=client -o yaml -f <file>预览 YAML 被解析后的样子,再apply。
4.5 “Connection refused” —— 服务间调用不通的三层穿透法
app Pod 里 curl http://redis.default.svc.cluster.local:6379 报 Connection refused,说明网络层通了(DNS 解析成功),但应用层没通。三层穿透:
- Layer 1:Pod 内部:
kubectl exec -it <app-pod> -- sh,然后telnet redis.default.svc.cluster.local 6379。如果telnet不通,说明redisPod 的containerPort没监听,或者redis容器根本没起来。kubectl logs <redis-pod>。 - Layer 2:Service 层:
kubectl get endpoints redis,看有没有 IP。如果有,kubectl get svc redis -o wide,看CLUSTER-IP。然后在appPod 里curl http://<cluster-ip>:6379。如果通了,说明 Service 配置没问题;如果还不通,说明redis容器的containerPort和 Service 的targetPort不一致。 - Layer 3:网络策略(NetworkPolicy):如果集群启用了
NetworkPolicy,默认是“拒绝所有”。你需要显式允许app命名空间的 Pod 访问redis命名空间的 Pod。kubectl apply -f network-policy.yaml,内容是允许app的 label 访问redis的 label。
5. 迁移后的思考:从“会用”到“用好”,Kubernetes 的深水区在哪里
当你能把一个简单的 docker-compose 项目稳稳当当地搬到 K8s 上,并且 kubectl get pods 里全是 Running,恭喜你,你已经拿到了云原生世界的入门门票。但这张票,只够你走进大厅。真正的深水区,在于如何让 K8s 不仅“能跑”,而且“跑得聪明、跑得安全、跑得省心”。我亲身经历的几个关键跃迁点,分享给你:
- 从“手动 YAML”到“GitOps”:把所有的 YAML 文件(Deployment、Service、Ingress)都放进一个 Git 仓库,用 Argo CD 或 Flux 这样的工具监听仓库变更。一旦你
git push,Argo CD 就自动kubectl apply。这彻底消灭了“谁在哪个集群上改了什么配置”的混沌,所有变更都有迹可循、可审计、可回滚。我所在的团队,上线一个新版本,不再是kubectl apply -f prod/,而是git commit -m "release v1.2.0",然后喝杯咖啡。 - 从“静态资源”到“弹性资源”:
requests和limits不是摆设。我见过一个集群,因为所有 Pod 都没设requests,Scheduler 把几百个 Pod 全塞进一台 32 核的机器,结果 CPU 爆满,整个集群雪崩。后来我们推行“资源申请文化”:每个新服务上线,必须由架构师审核其requests是否合理。我们用kubectl top nodes和kubectl top pods监控真实使用率,再用kubectl set resources动态调整。资源不是越多越好,而是“刚刚好”。 - 从“黑盒日志”到“可观测性栈”:
kubectl logs只能看到当前日志。生产环境需要 ELK(Elasticsearch, Logstash, Kibana)或 Loki + Grafana 来做日志聚合;需要 Prometheus + Grafana 做指标监控(CPU、内存、HTTP QPS、延迟);需要 Jaeger 做分布式追踪。这三者合称“可观测性三支柱”。它们不是可选项,而是生产环境的氧气。没有它们,你就像在浓雾中开车,永远不知道下一个故障在哪。