版本比较
标识
- 该行被添加。
- 该行被删除。
- 格式已经改变。
前言
我们用 我用 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 里需要有对应的设置,如下所示:
| 代码块 | ||
|---|---|---|
| ||
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。
- 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 |
环境变量和配置管理
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 文件启动,然后手写 K8s YAML,部署到测试集群。
- 逐个服务迁移。我们先迁了无状态的 API 服务,跑了一周没问题,再迁后台任务,最后才迁数据库。
- 灰度发布。线上同时跑 Compose 和 K8s,用负载均衡器分流。验证 K8s 里的服务正常后再全量切换。
- 保留回滚方案。K8s 的 Deployment 能自动回滚,但我还是在虚拟机上保留了 Compose 配置,以防万一。
| 目录 |
|---|