从 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 下手动拉旧镜像方便多了。
平滑过渡的方案
我最后采用的迁移步骤是这样的:
- 先在测试环境试水。用现有的 Compose 文件启动,然后手写 K8s YAML,部署到测试集群。
- 逐个服务迁移。我们先迁了无状态的 API 服务,跑了一周没问题,再迁后台任务,最后才迁数据库。
- 灰度发布。线上同时跑 Compose 和 K8s,用负载均衡器分流。验证 K8s 里的服务正常后再全量切换。
- 保留回滚方案。K8s 的 Deployment 能自动回滚,但我还是在虚拟机上保留了 Compose 配置,以防万一。
这样做虽然慢,但稳。我们最后花了三个月从 Compose 完全迁到 K8s,期间没有任何生产事故。
最后的收获
现在回头看,Compose 和 K8s 就像自行车和汽车的区别。自行车简单轻快,你可以很快上手。但当你需要跑长距离、经常爬坡、要拉货的时候,汽车就是更好的选择,虽然学习曲线陡一点。
最关键的是要理解两者的差异,不要期待一键转换。花时间理解 K8s 的概念——Pod、Service、Deployment、PVC——然后一个一个解决迁移中遇到的坑。这样出来的系统才是稳定可靠的。
如果你现在也在用 Compose,我的建议是:别等着有一天被逼迫升级。趁着系统还不太复杂,主动做这个迁移。一开始会累一点,但长期收益绝对值得。
公众号:耕云躬行录
个人博客:躬行笔记