ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从Docker Compose到Kubernetes:单机编排与集群调度的迁移实践

从Docker Compose到Kubernetes:单机编排与集群调度的迁移实践 1. Compose 跑得好好的为什么要折腾 k8s1.1 一个真实的“被迫升级”场景我最早接触 docker compose 的时候心里想的是“终于不用在一台服务器上手动敲一串 docker run 了”。那时候公司项目还不大一台 4核8G 的机器nginx php-fpm mysql redis 全部用 compose 编排一条 docker compose up -d 全起来日志、网络、重启策略都安排得明明白白。说实话对中小项目来说docker compose 的体验是真好它把“多容器协作”这件原本很琐碎的事情压缩成了一两个命令。但后来事情变了。某个活动周期流量上来之后单机部署的瓶颈就暴露了一台机器扛不住想要横向扩容compose 基本帮不上忙。你当然可以在另一台服务器上再起一套 compose但负载均衡、服务发现、滚动更新、故障自愈这些事全都得自己搞定。此时你会看到团队里开始有人提 k8s也有人在争论“docker compose 升级到 k8s”是不是过度设计。这篇文章我不打算写成 k8s 教程也不打算劝你无脑迁移。我想从一个实际使用者的角度把这两个东西的底层逻辑、适用边界、迁移路径和踩坑过程讲清楚。无论你是在纠结要不要学 k8s还是正在准备把一套 compose 项目搬到 k8s这篇文章应该都能给你一些实在的参考。1.2 先搞清楚 docker compose 是干什么的热词里有“docker compose 是干什么的”这个问题其实问得挺核心。简单来说docker compose 是一个容器编排工具但它编排的范围是“单台主机”。你在 docker-compose.yml 里定义几个 service每个 service 用什么镜像、暴露哪些端口、挂载哪些卷、依赖哪个服务然后 compose 帮你在同一台机器上把这些容器拉起来并且管理它们的网络、生命周期和日志。它解决的核心痛点是“多容器应用的管理”。比如一个 LNMP 项目有 nginx、php-fpm、mysql、redis 四个容器手工 docker run 四次会非常痛苦尤其是网络配置、环境变量、重启策略这些细节手工敲容易漏。compose 用声明式配置解决这个问题而且 docker compose up 之后如果容器挂了restart: unless-stopped 这类策略也能帮你拉起。对单机开发、测试、小型生产部署来说这套方案已经足够。但你要注意compose 的“编排”是扁平化的它没有调度器没有节点概念没有自动伸缩没有跨主机的服务发现。它更像一个“单机管家”而 k8s 则是一个“集群操作系统”。1.3 k8s 到底解决的是什么问题提到 k8s很多人第一反应是“复杂”“要命的 YAML”“运维门槛高”。这些印象不算错但容易让人忽略它的本质k8s 是一套分布式系统的编排和调度平台。它把多台服务器抽象成一个资源池然后通过声明式 API 让你描述“我想要的最终状态”比如“我要 3 个 nginx 副本”k8s 负责调度、编排、自愈、伸缩、滚动更新。为什么会有“k8s经典版”“k8s的lnmp架构实验”这类热搜因为大多数人的学习路径是从单机 of things 开始的接触 k8s 之后第一反应是“我要怎么把我熟悉的 compose 项目搬到 k8s 上”。而这正是我接下来要展开的部分。如果你只有一台机器跑 k8s 不是不行但收益很低。一台机器上compose 能做的k8s 都能做但绕远路。反过来也一样如果你有五六台机器想统一管理所有中间件和应用想实现灰度发布、自动扩容、故障转移这时候 compose 就力不从心了。2. 两者本质单机编排与集群调度2.1 Compose 的模型本机多容器协作docker compose 的工作模型建立在 docker 引擎本身之上。每个 service 本质上还是一个容器compose 做的事情是帮你把这些容器纳入一个共享网络并按依赖关系决定启动顺序。举个例子。一个典型的 docker-compose.yml 可能长这样version: 3 services: nginx: image: nginx:1.26 ports: - 80:80 volumes: - ./html:/usr/share/nginx/html php: build: ./php expose: - 9000 mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 volumes: - db_data:/var/lib/mysql volumes: db_data:这里 nginx 和 php 之间通过 compose 创建的 bridge 网络通信mysql 的数据存在命名卷里nginx 把宿主机的 80 端口映射到容器内。这个模型很简单所有容器都在同一台机器上网络、存储、进程都是共享宿主机内核的。compose 的优点是简单、直观、可读性高。缺点是它没有“跨主机”的抽象。一旦你有两台以上机器compose 就不知道该怎么把容器分布在不同的节点上也不知道怎么处理节点故障更不知道如何做负载均衡。你只能在外面套一层像 nginx、keepalived、consul 这样的“外挂”来弥补。2.2 Kubernetes 的模型声明式集群调度k8s 的模型复杂很多但核心思想其实不复杂你告诉它“我要什么”它不断尝试让现实向期望状态收敛。你想跑一个 nginx你创建一个 Deployment里面写 replicas: 3它会在集群里找三台合适的工作节点各自跑一个副本。你想让外部访问这些副本你创建一个 Service它会给你一个稳定的虚拟 IP并做负载均衡。你想在发布新版本时不停机你改一下镜像版本它帮你做滚动更新。某个节点宕机了它会把上面的 Pod 调度到其他节点重新创建。这种模型的底层架构有几个核心组件API Server、etcd、Scheduler、Controller Manager以及每个节点上的 kubelet 和 kube-proxy。API Server 是所有操作的入口etcd 存储所有状态Scheduler 负责把 Pod 分配到合适的节点Controller Manager 负责各种控制循环。听起来复杂但你日常使用 k8s 时直接面对的主要是 kubectl 和 YAML 配置。这里有一个非常关键的思维转变从“命令式”到“声明式”。compose 的 docker compose up 是你告诉 docker 去启动东西k8s 的 kubectl apply -f 是你告诉集群“这个 YAML 描述了我想要的最终状态请你去实现并持续维护”。compose 也不是没有声明式的东西但它的“声明”范围比 k8s 小得多。2.3 概念映射表把 compose 翻译成 k8s很多人觉得 k8s 难学是因为概念太多。但如果从 compose 的视角去看其实很多概念是可以一一对应的。我整理了一张对照表方便你从熟悉的概念出发理解 k8sDocker Compose 概念Kubernetes 对应概念说明serviceDeployment ServiceDeployment 管副本和维护状态Service 提供稳定访问入口containerPod 内的容器Pod 是最小调度单元可包含 1 个或多个容器networkbridgePod 网络 Service Ingress默认同 Pod 内 localhost跨 Pod 用 Service 抽象volume命名卷/绑定挂载PersistentVolumeClaimPVC emptyDir持久化数据用 PVC临时数据用 emptyDirenvironment / env_fileConfigMap / Secret普通配置放 ConfigMap敏感信息放 Secretdepends_oninitContainer 探针startup / readiness控制启动顺序的方式完全不同restart: unless-stoppedPod 的 restartPolicy 控制器兜底k8s 中推荐用 Deployment 控制器保证自愈portsServiceNodePort / LoadBalancer Ingress不建议直接在宿主机映射端口扩展服务docker compose up --scaleHPA水平自动伸缩k8s 支持基于 CPU/内存/自定义指标的自动伸缩这张表是我做迁移时自己总结的后来发现不少团队也是这么教新人的。你可以把它当成一本地图而不是死记硬背的概念列表。真正需要理解的是每个映射背后的意图比如 depends_on 在 compose 里只是控制启动顺序但在 k8s 里光是顺序不够还需要健康检查来判断“启动完成后是否可用”。3. 从 Compose 到 k8s5 步迁移路径3.1 第一步把 compose 项目容器化合规我见过很多人把 docker-compose.yml 直接扔给 k8s想找个自动转换工具一步到位。工具确实有比如 compose-spec 转换成 k8s 的插件但实际用下来效果一般因为 compose 和 k8s 的模型差异太大了。我的建议是先手工做一次“容器化合规检查”再动手迁移。具体检查四点镜像是否已经构建并推送到镜像仓库。compose 里如果用的是 buildk8s 里默认是没有构建能力的你需要先构建镜像推到私有仓库或 Docker Hub然后在 k8s 的 deployment YAML 里用 image 指定完整地址。环境变量是否可配置。compose 的 environment 可以硬编码但到 k8s 里最好用 ConfigMap 管理避免频繁改 YAML。启动顺序是否有强依赖。如果某个服务必须等另一个服务完全就绪才能启动你需要考虑 k8s 的 initContainer 或者探针。注意initContainer 只能保证“启动前检查”如果对方是一个外部服务不如直接让应用支持重试。数据持久化在哪里。compose 里无论是绑定挂载还是命名卷迁移到 k8s 都要换成 PVC这涉及存储类的选择尤其是有状态服务mysql、redis时要特别小心。这个阶段不用写任何 k8s 配置先把 compose 项目的规范性和可移植性提上来。如果 compose 本身就依赖宿主机文件路径、特定内核模块或者宿主机端口迁移时大概率会卡住。3.2 第二步镜像仓库与版本管理k8s 调度到哪台机器是随机的Pod 重建后会在新节点启动所以镜像必须在集群所有节点都能拉取的位置。自建 Harbor 是最常见的方案开发环境用 Docker Hub 或者阿里云 ACR 也可以。这里有个细节常被忽略镜像 tag 一定要带版本。很多人习惯用 latest这在单机 compose 上没问题但在 k8s 里会非常坑。因为 Deployment 滚动更新的判断依据是镜像 tag 或 digest 变化如果你一直用 latest那么你重新构建镜像、推送同一个 tag 到仓库之后kubectl rollout restart 才能触发重新拉取不然节点上的 kubelet 可能直接用本地的缓存镜像。正确的做法是每次构建都打一个新 tag或者用 commit SHA 做 tag。另外一个坑是私有仓库认证。k8s 节点拉取私有镜像需要配置 imagePullSecret你可以把它挂在 Deployment 的 spec.template.spec.imagePullSecrets 里。很多新手迁移时镜像一直拉取失败排查半天发现是没配 secret。3.3 第三步配置剥离——ConfigMap 与 Secretcompose 里最常见的配置方式有两种直接在 environment 里写或者用 env_file 引用文件。到 k8s 里我强烈建议把配置统一剥离到 ConfigMap 和 Secret 中。普通配置放 ConfigMap 的例子apiVersion: v1 kind: ConfigMap metadata: name: app-config data: APP_ENV: production DB_HOST: mysql-service DB_PORT: 3306敏感信息放 SecretapiVersion: v1 kind: Secret metadata: name: app-secret type: Opaque stringData: DB_PASSWORD: root123然后在 Deployment 里引用env: - name: APP_ENV valueFrom: configMapKeyRef: name: app-config key: APP_ENV - name: DB_PASSWORD valueFrom: secretKeyRef: name: app-secret key: DB_PASSWORD为什么推荐这么做因为配置和镜像分离之后你改配置不需要重新构建镜像只需要 kubectl apply 更新 ConfigMap然后滚动重启 Pod 即可。在团队协作时也不容易把密码提交到 Git。有个坑要提醒ConfigMap 更新后已经运行的 Pod 里的环境变量不会自动变化。你需要修改 Deployment 的 annotations比如加一个版本号触发滚动更新或者直接 kubectl rollout restart deployment 名称。如果你希望配置变化自动生效可以考虑用 ConfigMap 挂载文件的方式kubelet 会定期同步但依然有延迟业务方需要自己监听文件变化。3.4 第四步存储从 volume 到 PVC如果你在 compose 里用了命名卷迁移到 k8s 时对应的是 PVC。PVC 的挑战在于存储类的选择不同云厂商的存储类能力不一样常见的有本地存储local性能好但 Pod 漂移后数据不跟着走适合临时数据。网络存储NFS、Ceph、云盘Pod 漂移后依然能挂载适合有状态服务。我迁移 LNMP 里的 mysql 时用的是 NFS 存储类。配置大致是这样apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mysql-pvc spec: accessModes: - ReadWriteOnce storageClassName: nfs resources: requests: storage: 30Gi然后在 Deployment 里挂载volumes: - name: mysql-data persistentVolumeClaim: claimName: mysql-pvc这里要注意 accessModes 和实际存储类是否匹配。NFS 一般支持 ReadWriteMany云盘可能只支持 ReadWriteOnce。如果多个副本同时读写同一个 PVC用 ReadWriteOnce 类型的存储会直接失败。还有一点mysql、redis 这类有状态服务迁移时千万不要直接把 compose 里的数据目录原样挂载过去。如果数据库版本或配置差异较大轻则启动失败重则数据文件损坏。稳妥的做法是先在新环境起一个空实例再把数据导入进去。3.5 第五步启动顺序、探针与滚动更新compose 里的 depends_on 看起来控制了启动顺序但它实际上只控制容器创建的先后顺序并不等待依赖服务就绪。比如说 mysql 还没初始化完成php 可能就先启动了连接数据库失败后如果应用有重试逻辑还好没有重试逻辑就直接崩溃。k8s 里没有 depends_on 这种写法你需要用两种方式配合initContainer在主容器启动前执行一些检查或初始化操作。比较常见的做法是在 initContainer 里运行一条命令去检查依赖服务是否就绪比如用 nc 或 wget 探测端口。探针Probek8s 有三种探针livenessProbe 判断容器是否存活readinessProbe 判断是否可以接收流量startupProbe 保护启动较慢的应用。一个简单的 readinessProbe 示例readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 10 periodSeconds: 5配置探针之后k8s 才会把 Pod 标记为 Ready然后接入 Service 的负载均衡。这和 compose 的 depends_on 相比是本质上的升级compose 只能保证“启动了”k8s 能保证“可用了”。滚动更新也值得提前设计好。默认情况下Deployment 更新镜像时会逐个替换 Pod如果你的应用只有一个副本更新时会有短暂中断。如果要求零停机需要设置 minReadySeconds、maxSurge 和 maxUnavailable。compose v2 也有 rolling 更新但比较复杂实际用到的人很少。4. 典型案例在 k8s 上把 LNMP 架构跑起来4.1 LNMP 容器化的注意点热搜词里出现了“k8s的lnmp架构实验”“k8s部署lnmp”说明不少人在拿 LNMP 当 k8s 练手项目。LNMP 确实是一个很好的案例因为它涉及 Web 服务、动态语言、数据库三种不同类型的工作负载。LNMP 在 k8s 里的部署思路大概是nginx无状态 Web 层Deployment 副本数可伸缩前端静态文件可以放镜像里或挂 PVC。php-fpm无状态应用层Deployment 副本数可伸缩nginx 通过 Service 访问 php-fpm。mysql有状态数据库层用 StatefulSet 或者 Deployment PVC 部署通常只跑一个副本数据持久化到 PVC。redis有状态缓存层可以用 Deployment PVC或者直接上 redis Operator。如果只是单机实验Deployment 就够了。nginx 与 php-fpm 的通信在 compose 里很简单因为它们在同一网络里。在 k8s 里nginx 容器要访问 php-fpm Pod需要给 php-fpm 创建一个 Service。比如apiVersion: v1 kind: Service metadata: name: php-fpm-service spec: selector: app: php-fpm ports: - port: 9000 targetPort: 9000然后 nginx 配置里的 fastcgi_pass 指向 php-fpm-service:9000。这里有个小坑compose 里 service 名可以直接作为 DNS 名称比如 fastcgi_pass php:9000而 k8s 里 Service 名称同样可以作为 DNS 名称所以其实写法是类似的只是要注意命名空间隔离和同名冲突。4.2 网络规划Service、Ingress 与多网卡场景LNMP 对外提供 Web 服务在 k8s 里通常有两种方案NodePort 或者 Ingress。如果你只是实验NodePort 最简单apiVersion: v1 kind: Service metadata: name: nginx-service spec: type: NodePort selector: app: nginx ports: - port: 80 targetPort: 80 nodePort: 30080这样集群任何节点的 30080 端口都能访问到 nginx。但生产环境一般用 Ingress Controller比如 nginx-ingress 或 traefik。Ingress 的好处是可以在七层做路由、TLS、限流等而且不需要直接暴露 Service 端口。顺便说一下热搜里提到的“k8s multus 网络 vlan 配置”。Multus 是一个多网络插件允许 Pod 同时接入多个网络接口可以在一个虚拟网络上跑管理面流量另一个物理网络跑数据面流量。日常单集群项目基本用不到但如果你在做 SDN、边缘计算或者对网络性能有特殊要求的场景可以了解一下。它配置的核心是 NetworkAttachmentDefinition 资源需要配合可用的 CNI如 macvlan、vlan和默认网络插件如 Calico、Flannel协同工作。这个扩展我不建议新手第一时间玩先把基础 Service/Ingress 网络搞明白更重要。4.3 LNMP 迁移时踩过的几个坑我在把一套 compose 版 LNMP 迁到 k8s 的时候踩过不少值得记录的坑。第一个坑是 php-fpm 启动后nginx 报 502。排查后发现是 php-fpm 的 listen 配置。compose 里容器内启动 php-fpm 默认监听 9000 端口但在某些镜像里php-fpm 的配置是监听 Unix socket 路径而不是 TCP。如果是 socketnginx 的 fastcgi_pass 需要改成 socket 文件路径并额外挂 socket 目录。用 k8s 时不同 Pod 之间没法共享 socket 文件所以必须改成 TCP 监听并在 php-fpm 的配置里指定 listen 0.0.0.0:9000。第二个坑是 mysql 初始化。compose 里可以用 environment 设置 MYSQL_ROOT_PASSWORDk8s 环境也可以但要小心 ConfigMap 里的密码包含特殊字符。比如密码里有 $ 或者 \YAML 解析时会被转义导致初始化密码和业务配置里的密码不一致。建议用 Secret 的 stringData 字段避免手动 base64 转码时的格式问题。第三个坑是镜像拉取超时。集群里如果有节点在中国大陆直接拉 Docker Hub 镜像经常超时。解决办法是在镜像仓库层面做缓存/代理或者把镜像同步到云厂商的镜像仓库然后用完全限定名。这个话题跑题了不展开但要提醒你提前处理。5. 常见问题与排坑实录5.1 是不是所有 compose 项目都值得迁移到 k8s这是每次聊到迁移时最容易被问起的问题。我的看法是不是。如果你同时满足以下条件迁移的收益其实不大项目规模小单台服务器足以支撑业务。团队没有专职运维没有 k8s 使用经验。流量变化平缓不需要自动扩缩容。数据存储强依赖本地磁盘且没有现成网络存储。这种情况下docker compose 足够稳定强行上 k8s 只会增加复杂度。迁移到 k8s 是有代价的YAML 数量变多、学习成本上升、排查问题所需的技能栈变广、集群本身也需要维护apiserver、etcd、网络插件、证书过期等。“新版 docker 自带 compose”这个特性本来就是在降低单机用户的编排成本说明 Docker 官方也在认可 compose 在轻量场景里的价值。那什么情况下必须考虑 k8s两条明确信号一是需要横向扩容二是需要故障自愈。当你的应用需要“机器挂了自动换一台继续跑”或者“流量高峰自动增加副本”compose 是做不了的这时候上 k8s 就是刚需。5.2 安装部署与版本选择的常见误区热搜里有“k8s安装部署”“rancher装k8s”“k8s经典版”这些词说明很多人卡在装环境这一步。k8s 的安装确实容易劝退新手尤其是裸金属环境证书、etcd、网络插件、kubelet 配置每一步都可能出问题。我给你的建议是分阶段纯学习阶段用 kindKubernetes in Docker或者 k3d一条命令起一个集群省去了几乎所有安装环节。适合先熟悉概念和 YAML 写法。单机生产实验用 k3s轻量、内存占用小也是 Rancher 团队出的产品配 Rancher 的可视化管理界面非常方便。正规多节点生产用 kubeadm 或者云厂商托管集群比如 ACK、TKE、EKS。托管集群省心很多etcd 和 master 节点都不需要自己维护。关于版本我建议优先选择当前稳定版本不要追“经典版”这种过时概念。k8s 的迭代速度很快社区一般维护最近三个小版本老版本会有安全漏洞和兼容性问题。安装工具链版本如 kubeadm、kubelet、kubectl要与集群版本匹配这个在安装文档里写得很清楚但很多人会栽在这上面。Rancher 作为一个可视化管理平台适合团队多人操作它本身不替代 k8s而是给你一个 UI 来管理多个集群和部署应用。如果你团队里有人畏惧命令行Rancher 能显著降低使用门槛。5.3 高频问题速查表最后我整理了一个高频问题表都是我在实际操作和帮同事排障时遇到过的经典问题直接收藏可查问题现象常见原因排查与解决Pod 一直 Pending节点资源不足 / 调度约束不满足kubectl describe pod 查看事件检查 requests 配置与节点可用资源ImagePullBackOff镜像地址错误 / 私有仓库未认证检查镜像 tag 是否存在配置 imagePullSecretCrashLoopBackOff应用启动失败 / 探针配置过严查看日志 kubectl logs 上一个容器调整探针参数503 Service UnavailableService 选择器无匹配 Pod / 后端未 Readykubectl get endpoints检查 selector 是否匹配Nginx 502 Bad Gateway后端服务端口不匹配 / php-fpm listen 配置错误进入 nginx 容器 curl 测试后端地址ConfigMap 修改后不生效环境变量注入不会自动更新触发滚动更新 restart deploymentPVC 一直 Pending存储类不存在或不可用kubectl get storageclass检查 storageClassNameNodePort 无法访问安全组/防火墙未放行检查节点安全组和防火墙规则kubectl get svc 确认端口跨节点 Pod 无法通信网络插件问题检查 CNI 组件状态查看 kube-system 命名空间日志证书过期导致 kubectl 报错集群证书有效期问题查看 kubeadm 证书期限定期 renew5.4 我的建议小步慢走别搞一刀切迁移这件事最忌讳的就是把所有服务一次性搬到 k8s。我见过一些团队周会上定了“下个月全部上 k8s”的目标结果三个月后还在和 YAML 搏斗线上事故不断。更稳妥的方式是挑一个无状态服务先迁移比如把 nginx 前端迁到 k8s后面依然连 compose 环境的 php-fpm。之所以能这么干是因为 Service 的抽象天然屏蔽了后端地址的细节只要你把 compose 服务的地址改成 k8s Service 的 DNS 或者 NodePort 地址即可。等跑顺了再逐步迁移 php-fpm、redis、mysql。这样做的另一个好处是团队能渐进式学习。先让一两个主力把 k8s 跑熟再带着其他人一起踩坑。如果一开始就全员扑上去大家的挫败感会非常强。6. 写在最后两个工具的定位从来不是替代关系我接触 These two 工具多年最深的感受是它们不是同一层的东西硬要比出高下其实是浪费时间。docker compose 解决的是“一台机器上怎么把多个容器组织起来”k8s 解决的是“一个集群里怎么把各种应用调度的井井有条”。两者的关系更像自行车和汽车都能代步但适用场景和驾驶门槛完全不同。如果你问我个人会怎么选我的原则很简单单机环境、开发测试、小规模生产优先 compose多节点、高可用、自动伸缩、复杂发布流程直接 k8s。如果团队资源和精力有限compose 完全够用的阶段强行上 k8s只会给自己找麻烦。反过来如果业务已经明显在增长早点接触 k8s 其实是给未来省时间因为真正的迁移成本会随着业务复杂度和数据量的增长指数上升。最后分享一个比较反常识的经验把 compose 项目迁到 k8s 之后你再回头看 compose往往能把 compose 写得更好。因为你理解了 ConfigMap 该怎么拆、探针该怎么配、存储边界在哪这些认知反过来会让你的 compose 项目更加健壮。我在实际迁移完 LNMP 之后就把原来 compose 里依赖启动顺序的写法全部改成了应用层重试加健康检查整体稳定性提升非常明显。工具永远在变但“把应用状态描述清楚、让系统自己去维护”的这种声明式思维是这两个工具共同传递给你的最有价值的东西。学会了它将来不管再出现什么新编排系统你上手都会快很多。
返回列表