版本比较

标识

  • 该行被添加。
  • 该行被删除。
  • 格式已经改变。

前言

我们用 我用 Compose 运行了 3 年的微服务:一个 Node.js API、两个 Python 后台任务、一个 Redis 缓存、PostgreSQL 数据库——都在一个 docker-compose.yml 文件里定义,在单台虚拟机上跑。每次发布新版本就是:停容器、拉新镜像、docker-compose up -d。工作是能正常工作,但问题开始浮现了。一个晚上,那台虚拟机的硬盘满了。所有容器崩了,我们直到用户打电话才知道。自动扩容?没有。服务发现?靠写死的 IP。滚动更新?做不了,停一个容器就有几秒的请求失败。当时我就明白,是时候升到 Kubernetes 了。d。

工作是能正常工作,但问题开始浮现了。一个晚上,那台虚拟机的硬盘满了。所有容器崩了,直到业务打电话才知道。自动扩容?没有。服务发现?靠写死的 IP。滚动更新?做不了,停一个容器就有几秒的请求失败。是时候升到 Kubernetes 了。

别被“一键转换”欺骗

最初的想法是直接用 Kompose 工具自动转换

代码块
kompose convert -f docker-compose.yml -o k8s/

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

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

信息

Kompose 只能当一个参考,实际场景需要自己熟悉并理解每一行在干什么

资源限制这个“可选项”

在 Docker Compose 里我们很少考虑资源限制,但是在 K8S 里需要有对应的设置,如下所示:

代码块
languageyaml
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 了。K8s 发现 pod 用了超过 256M 的内存就杀掉重启,服务抖动的厉害。

反过来,我又把 limits 设置得很宽松(512M,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 的数据:


代码块
languageyaml
services:
  db:
    image: postgres:14
    volumes:
      - db-data:/var/lib/postgresql/data

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



代码块
languageyaml
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。

健康检查从“可有可无”到“必不可少”

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

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

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

代码块
languageyaml
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 里定义了一大堆变量:

代码块
languageyaml
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:

代码块
languageyaml
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 里引用:

代码块
languageyaml
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,保证服务不中断。但这需要你正确配置:

代码块
languageyaml
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。后来加了这个配置才解决。

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

代码块
languageyaml
kubectl rollout undo deployment/api-server

平滑过渡方案

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



目录