后端服务如何做健康检查与优雅下线
结论:后端健康检查必须拆成「存活检查(liveness)+ 就绪检查(readiness)」两套,存活检查只判断进程本身,就绪检查才判断能不能接流量;优雅下线则是固定三步——先摘流量、再等在途请求跑完、最后才退出进程。K8s 环境下这三步靠 preStop 钩子和 terminationGracePeriodSeconds 打时间差来实现。
存活检查和就绪检查有什么区别?为什么不能只写一个 /health?
结论:liveness 失败触发「重启容器」,readiness 失败只触发「从 Service Endpoint 摘除、不重启」,两者职责必须分开。
最常见的线上事故是:把数据库连接检测写进 liveness。数据库抖动 30 秒,所有 Pod 的 liveness 同时失败,K8s 把整个集群的实例全部重启,故障从「依赖慢」放大成「全站不可用」。
正确做法是分成两个接口:
/healthz(liveness):只返回进程自身状态,比如事件循环是否阻塞、本地磁盘是否可写,不查任何外部依赖,永远返回 200,除非进程真的死了。/ready(readiness):检查 DB 连接池、Redis、关键下游,任何一个不通就返回 503。
K8s 配置参考值:liveness 用 initialDelaySeconds: 10、periodSeconds: 10、failureThreshold: 3;readiness 用 periodSeconds: 5、failureThreshold: 2,让它更快摘流量。启动耗时超过 30 秒的 Java 服务加一个 startupProbe,failureThreshold: 30、periodSeconds: 5,避免启动期被 liveness 误杀。
K8s 里怎么做到优雅下线?
结论:Pod 被删除时,K8s 会同时做两件事——从 Endpoint 摘除、向容器发 SIGTERM,这两条链路不同步,所以必须用 preStop 人为加一段等待。
标准配置长这样:
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 8"]
terminationGracePeriodSeconds: 30
执行顺序是:收到删除请求 → preStop 执行 sleep 8(这 8 秒里 kube-proxy、Ingress、客户端连接池完成路由更新)→ 容器收到 SIGTERM → 应用停止接收新请求并排空 → 进程退出。
terminationGracePeriodSeconds: 30 必须大于「preStop 等待时间 + 应用排空时间」,比如 preStop 8 秒 + 排空 20 秒 = 28 秒 < 30 秒才安全。如果排空超时,K8s 会直接发 SIGKILL 强杀,前面所有努力归零。
应用排空多久合适?取值参考 P99.9 的请求耗时乘以 1.5,普通 HTTP 服务通常 10~15 秒,有大批量导入任务的接口可能要 60 秒。
应用代码里怎么实现「停止接收新请求 + 等待在途请求」?
结论:不同语言都有现成的排空能力,关键是让应用先把自己的 readiness 置为 503,再等几秒,最后才关监听。
Spring Boot 3.x 只需两行配置:
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=25s
Go 标准库写法:用 signal.Notify 监听 SIGTERM,收到后先调用一个开关让 /ready 返回 503,等 5 秒,再执行 srv.Shutdown(ctx),ctx 超时设 25 秒。
WebSocket、gRPC streaming 这类长连接不受 Shutdown 管理,必须自己维护连接列表,逐个发送 close frame 或 GOAWAY,否则客户端会挂到 TCP 超时。
网关和注册中心还要做什么?
结论:应用层排空只能保证「自己这一跳」,跨进程的流量摘除要靠注册中心和网关配合。
- 注册中心(Nacos、Eureka):下线顺序是「先反注册,等 10 秒,再停进程」,Eureka 默认心跳间隔 30 秒、失效剔除最长 90 秒,等待时间短了客户端本地缓存还在把流量打过来。
- Nginx:
upstream里配max_fails=3 fail_timeout=10s,proxy_next_upstream只对 GET、HEAD 等幂等请求重试,避免重复下单。 - 客户端:配幂等重试 + 指数退避(初始 50ms,最大 2s),这是最后一道兜底。
最容易踩的坑有哪些?
结论:多数 502 都来自「摘流量和停进程同时发生」,而不是代码问题。
三个高频坑:一是没配 preStop,SIGTERM 和 Endpoint 更新同时发生,窗口期内必然打到已关闭的端口;二是 exec 写成 ["sleep", "8"] 在某些精简镜像里没有这个二进制,必须用 /bin/sh -c;三是灰度发布时一次性摘掉全部旧 Pod,正确做法是先摘 1 台,观察 1~2 分钟确认错误率没上涨,再逐台推进。
把存活/就绪分离、preStop 打时间差、应用层排空、注册中心反注册这四件事按顺序串起来,滚动更新期间的 5xx 就能压到 0。核心不是配置多复杂,而是每一步的等待时间都要大于上一跳的路由收敛时间。
原文链接:https://www.gj0.com/thread-384.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。