运维知识
悠悠
2026年6月21日

从 Docker Compose 到 Kubernetes:我走过的那些坑

我还记得那天下午,当老板问"我们能把线上应该能跑在 K8s 上吧"时,我才意识到我们不能再靠 Docker Compose 撑下去了。

当时我们用 Compose 运行了 3 年的微服务:一个 Node.js API、两个 Python 后台任务、一个 Redis 缓存、PostgreSQL 数据库——都在一个 docker-compose.yml 文件里定义,在单台虚拟机上跑。每次发布新版本就是:停容器、拉新镜像、docker-compose up -d。工作是能正常工作,但问题开始浮现了。

一个晚上,那台虚拟机的硬盘满了。所有容器崩了,我们直到用户打电话才知道。自动扩容?没有。服务发现?靠写死的 IP。滚动更新?做不了,停一个容器就有几秒的请求失败。当时我就明白,是时候升到 Kubernetes 了。

第一个坑:别被"一键转换"骗了

我最初的想法很天真——用 Kompose 工具自动转换。

# 多么诱人的命令
kompose convert -f docker-compose.yml -o k8s/

Kompose 确实能把你的 Compose 文件转成 Kubernetes YAML。我当时用了这个工具,结果生成了一堆 yaml 文件扔进去——然后一切都崩了。

问题出在细节上。Kompose 把 depends_on 转成了 initContainer,但它不知道 PostgreSQL 实际上需要 30 秒才能启动。API 容器一启动就连不上数据库,立刻被 crash loop 了。更糟的是,Kompose 没有给任何 pod 设置资源限制(requests/limits),所以 scheduler 没法做合理的资源分配。

我的建议是: Kompose 只能当做一个参考。你还是得手动写 YAML,理解每一行在干什么。

第二个坑:资源限制这个"可选项"

在 Docker Compose 里,我们从没想过资源限制是什么。容器想用多少内存就用多少。

移到 Kubernetes 后,我设置了这样的资源定义:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-server
  template:
    metadata:
      labels:
        app: api-server
    spec:
      containers:
      - name: api-server
        image: myregistry/api-server:v1.2.3
        ports:
        - containerPort: 3000
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"

太保守了。一上生产,API 在突发流量下直接被 OOMKill 了。Kubernetes 发现 pod 用了超过 256MB 内存,就杀掉它重启。服务抖动得厉害。

反过来,我又把 limits 设得很松(512Mi、1000m),结果一个有内存泄漏的服务逐渐吞掉了整个节点的资源,其他 pod 被无情地驱逐。

现在我的做法是:先在本地或测试环境压测一遍,看真实的内存和 CPU 占用,然后 requests 设为实际用量的 1.2 倍左右,limits 设为 1.5-2 倍。这样既给了应用突发的空间,又能保护集群。

对了,这里还有个坑:Kubernetes 有三个 QoS class——Guaranteed、Burstable、BestEffort。如果你设置了 requests 和 limits(且相等),pod 会被标记为 Guaranteed,在资源紧张时最后被驱逐。如果只设了 requests,就是 Burstable,被驱逐的优先级更高。理解这个很重要,尤其是你在跑有状态服务时。

第三个坑:网络和存储不是"自动的"

在 Docker Compose 里,容器之间通过服务名通信,自动有个 bridge network。迁到 K8s 后,我以为 DNS 名字可以一样用——结果踩坑了。

Kubernetes 里,service 的 DNS 名字是 service-name.namespace.svc.cluster.local。我的 Node.js API 之前通过 postgres://db:5432 连接数据库(db 是 Compose 里的服务名)。换到 K8s 后,我试了各种办法,最后才意识到应该用 postgres://postgres-service.default.svc.cluster.local:5432

更复杂的是存储。Compose 里,我们用 volumes 来持久化 PostgreSQL 的数据:

services:
  db:
    image: postgres:14
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

这在 K8s 里变复杂了。单纯的 emptyDir 只能在 pod 重启时保留数据,node 重启就没了。我需要 PersistentVolume 和 PersistentVolumeClaim。如果用的是云服务(AWS、阿里云),还得配置存储类(StorageClass)。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: standard
  resources:
    requests:
      storage: 20Gi
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres-service
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:14
        ports:
        - containerPort: 5432
          name: postgres
        env:
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: password
        volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
  - metadata:
      name: postgres-storage
    spec:
      accessModes:
        - ReadWriteOnce
      storageClassName: standard
      resources:
        requests:
          storage: 20Gi

对了,我还有个坑没提——数据库密码。在 Compose 里,我们就直接在 docker-compose.yml 里写 POSTGRES_PASSWORD=mypassword。上了 K8s,这太不安全了。必须用 Secret。我专门花了半天时间学怎么用 Secret 和密钥管理。

第四个坑:健康检查从"可有可无"到"必不可少"

Compose 下,容器挂了就挂了,我们靠监控和告警来发现。K8s 不一样,它有 liveness probe 和 readiness probe。

  • Readiness probe:检查容器是否准备好接收流量。失败的话,pod 从 service endpoints 里被移除,但不会重启。
  • Liveness probe:检查容器是否还活着。失败的话,K8s 会重启这个 pod。

我一开始没设置这两个,结果 API 服务有时候启动完成但还在初始化数据库连接池,请求打过来直接失败。后来我加了健康检查:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  template:
    spec:
      containers:
      - name: api-server
        image: myregistry/api-server:v1.2.3
        ports:
        - containerPort: 3000
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 15
          periodSeconds: 20
          timeoutSeconds: 5
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /ready
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5
          timeoutSeconds: 3
          failureThreshold: 2

这样 K8s 会等 15 秒让容器完全启动,然后定期检查。这个改动之后,我们的服务稳定性明显提升。

第五个坑:环境变量和配置管理

Compose 用 .env 文件,K8s 用 ConfigMap 和 Secret。听起来很简单,但实际迁移时坑很多。

我之前在 .env 里定义了一大堆变量:

NODE_ENV=production
DB_HOST=db
DB_PORT=5432
DB_USER=postgres
LOG_LEVEL=info
REDIS_URL=redis://redis:6379

迁到 K8s,我把敏感信息(DB_USER、DB_PASSWORD)放进 Secret,其他的放进 ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: api-config
data:
  NODE_ENV: production
  DB_HOST: postgres-service.default.svc.cluster.local
  DB_PORT: "5432"
  LOG_LEVEL: info
  REDIS_URL: redis://redis-service.default.svc.cluster.local:6379
---
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
stringData:
  DB_USER: postgres
  DB_PASSWORD: your-secret-password

然后在 deployment 里引用:

env:
- name: NODE_ENV
  valueFrom:
    configMapKeyRef:
      name: api-config
      key: NODE_ENV
- name: DB_USER
  valueFrom:
    secretKeyRef:
      name: db-credentials
      key: DB_USER
- name: DB_PASSWORD
  valueFrom:
    secretKeyRef:
      name: db-credentials
      key: DB_PASSWORD

这样管理起来更清晰,也更安全。

第六个坑:发布流程和回滚

Compose 下,发新版本就是:docker pull、docker-compose up -d。简单粗暴,但会有一瞬间的 downtime。

K8s 的 Deployment 支持滚动更新(rolling update),默认配置下会一个一个替换 pod,保证服务不中断。但这需要你正确配置:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    # ...

maxSurge: 1 表示最多可以有 1 个额外的 pod(总共 4 个)。maxUnavailable: 0 表示不能有 pod 不可用。这样更新过程中,始终有 3 个 pod 在处理请求。

但我一开始没配这个,导致发布时有 pod 直接被杀掉替换,用户请求出现了短暂的 503。后来加了这个配置才解决。

回滚也很方便。如果新版本有问题,一条命令就能回到上个版本:

kubectl rollout undo deployment/api-server

这比 Compose 下手动拉旧镜像方便多了。

平滑过渡的方案

我最后采用的迁移步骤是这样的:

  1. 先在测试环境试水。用现有的 Compose 文件启动,然后手写 K8s YAML,部署到测试集群。
  2. 逐个服务迁移。我们先迁了无状态的 API 服务,跑了一周没问题,再迁后台任务,最后才迁数据库。
  3. 灰度发布。线上同时跑 Compose 和 K8s,用负载均衡器分流。验证 K8s 里的服务正常后再全量切换。
  4. 保留回滚方案。K8s 的 Deployment 能自动回滚,但我还是在虚拟机上保留了 Compose 配置,以防万一。

这样做虽然慢,但稳。我们最后花了三个月从 Compose 完全迁到 K8s,期间没有任何生产事故。

最后的收获

现在回头看,Compose 和 K8s 就像自行车和汽车的区别。自行车简单轻快,你可以很快上手。但当你需要跑长距离、经常爬坡、要拉货的时候,汽车就是更好的选择,虽然学习曲线陡一点。

最关键的是要理解两者的差异,不要期待一键转换。花时间理解 K8s 的概念——Pod、Service、Deployment、PVC——然后一个一个解决迁移中遇到的坑。这样出来的系统才是稳定可靠的。

如果你现在也在用 Compose,我的建议是:别等着有一天被逼迫升级。趁着系统还不太复杂,主动做这个迁移。一开始会累一点,但长期收益绝对值得。


公众号:耕云躬行录

个人博客:躬行笔记

文章目录

博主介绍

热爱技术的云计算运维工程师,Python全栈工程师,分享开发经验与生活感悟。
欢迎关注我的微信公众号@运维躬行录,领取海量学习资料

微信二维码