
去年我把一个内部服务迁上 K8s,照着教程写了 Deployment,把 replicas 从 1 改成 3,觉得这就算高可用了。三份实例,挂一份还有两份,天塌不下来。
上线第二周,一次滚动发布,服务整整不可用了四分钟。三个副本全在 CrashLoopBackOff 里排队重启,用户那边看到的是 502。我盯着 kubectl get pods 想不通:我明明有三个副本。
后来我才明白,K8s 给你的是"能同时跑三个容器"的能力,不是"你的应用高可用"的事实。这两件事之间,隔着整个应用架构。
我那个服务的问题出在一个没人在意的单点上:三个副本共用一个后台定时任务,任务启动时会去抢同一把数据库锁,抢不到的那个进程直接退出。平时看不出问题,发布时三个副本同时启动、同时抢锁、两个退出——replicas: 3 实际只有一个健康。
K8s 的副本控制器只保证"有三个 Pod 在跑",它不知道你的进程为什么退出、依赖谁、能不能并行。它管的是编排,不管你的应用是不是真的能横着扩。
一个能横着扩的应用,至少要满足:无本地状态、无单点依赖、启动幂等、可被随时杀掉。这四条没有一条写在 YAML 里,全在你的代码里。
迁移时我抄了一份"标准"配置:
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10/healthz 里我顺手让它查了数据库连接。看着很负责任,其实是灾难的开始。
有一次数据库主库切换,抖动了两分钟。所有副本的 /healthz 都返回失败,livenessProbe 判定"应用死了",把三个容器全部重启。重启中的容器连不上数据库,起来又失败,又被杀——数据库抖动两分钟,被我放大成了服务全挂十分钟。
正确的分工是:liveness 只回答"我这个进程还要不要活着",readiness 才回答"我现在能不能接流量"。
livenessProbe:
httpGet: { path: /healthz, port: 8080 } # 只检查进程自身
readinessProbe:
httpGet: { path: /readyz, port: 8080 } # 检查依赖,失败就从 Service 摘掉区别在于失败后的动作:liveness 失败会杀容器,readiness 失败只是摘流量。依赖抖动时你想要的是后者——别杀我,先别给我派活,等依赖好了我自己回来。把这两者混为一谈,是把"降级"升级成了"雪崩"。
resources 那一段,很多人(包括当时的我)嫌麻烦就删了。删掉的后果不是"资源随便用",是调度器失去依据。
K8s 调度一个 Pod 时看的是 requests:这个 Pod 声明要多少 CPU、多少内存,调度器据此找一台"放得下"的节点。不写 requests,调度器默认你要 0,于是它把所有 Pod 都塞进同一台机器,那台机器上的应用互相抢 CPU,谁慢谁快全看运气。
resources:
requests: { cpu: 200m, memory: 256Mi } # 调度依据,也是 QoS 依据
limits: { memory: 512Mi } # 防止单 Pod 吃爆节点还有一层:requests 和 limits 的比值决定了 Pod 的 QoS 等级。内存 requests == limits 的 Pod 是 Guaranteed,节点内存紧张时最后被杀;什么都没写的是 BestEffort,第一个被 OOM 清退。你写的不是数字,是节点资源紧张时谁先死的顺序。
迁完之后我复盘,发现所有事故都指向同一件事:把"应用的哪部分该由 K8s 管"想错了。
SIGTERM 之后还在处理请求的进程,会在滚动发布时给用户返回连接重置。这三条分开之后,replicas 才真正开始有意义。因为你终于保证了三份实例之间没有共享的可变状态、没有互相争抢的单点、启动不依赖"上一次运行的残留"。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。