我用 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/ |
Compose 确实能把 Compose 文件转换为 Kubernetes YAML。我当时也用了这个工具,结果生成了一堆 yaml 文件扔进去,然后一切都崩溃了。
问题出在细节上。Kompose 把 depends_on 转成了 initContainer,但它不知道 PostgreSQL 实际上需要 30 秒才能启动。API 容器一启动就连不上数据库,立刻被 cash loop 了。更糟糕的是 Compose 没有给任何 pod 设置资源限制(request/limits),所以 scheduler 没法做合理的资源分配。
Kompose 只能当一个参考,实际场景需要自己熟悉并理解每一行在干什么 |
在 Docker Compose 里我们很少考虑资源限制,但是在 K8S 里需要有对应的设置,如下所示:
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 的数据:
services:
db:
image: postgres:14
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data: |
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 里写 |
Compose 下,容器挂了就挂了,我们靠监控和告警来发现。K8s 不一样,它有 liveness probe 和 readiness probe。
我一开始没设置这两个,结果 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 |
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 |