后端服务如何做健康检查与优雅下线

chinaz
chinaz 正式会员超兽战士
发布于 2026-10-07 15:57 ·1 浏览 ·0 回复

结论:后端健康检查必须拆成「存活检查(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。核心不是配置多复杂,而是每一步的等待时间都要大于上一跳的路由收敛时间。

版权声明:本文来自 GJ站长论坛《后端服务如何做健康检查与优雅下线》
原文链接:https://www.gj0.com/thread-384.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。

全部回复 0

还没有回复,来抢沙发~