探针配错,比不配更危险
把 Spring Boot 丢进 K8s,最容易出事的地方不是镜像,也不是资源限制,而是健康探针。探针配得太敏感,应用刚启动就被判死、反复重启;配得太宽,挂了半天流量还在往上打;把数据库检查塞进 liveness,数据库抖一下,所有 Pod 一起重启,故障被放大一圈。
先记住三条职责:liveness 决定“要不要重启”,readiness 决定“要不要接流量”,startup 决定“启动期给多久”。三者混用,就是事故的开始。
三种探针的边界
| 探针 | 失败后果 | 该检查什么 | 不该检查什么 |
|---|---|---|---|
| startupProbe | 重启容器 | 应用是否完成启动 | 下游依赖 |
| livenessProbe | 重启容器 | 进程是否假死、死锁 | 数据库、Redis、第三方接口 |
| readinessProbe | 从 Service 摘除 | 能否处理请求、依赖是否就绪 | 与处理请求无关的组件 |
liveness 的原则是“只有重启能解决问题时才失败”。数据库连不上,重启应用没用,反而会让所有副本同时不可用。readiness 可以看数据库,但要接受“数据库短暂不可用时全部副本被摘流量”的后果;如果业务能降级读缓存,就不要把数据库放进 readiness。
Spring Boot 侧与 K8s 侧怎么配
Spring Boot 3 在 K8s 环境会自动开启探针端点,显式写出来更清楚:
management:
endpoint:
health:
probes:
enabled: true
group:
liveness:
include: livenessState
readiness:
include: readinessState
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
对应两个端点:/actuator/health/liveness 和 /actuator/health/readiness。注意默认的 /actuator/health 聚合了所有健康指示器,任何一个组件 DOWN 就返回 503,不适合直接拿来做 liveness。
K8s 侧的关键片段:
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 5
failureThreshold: 30
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 5
failureThreshold: 2
startupProbe 给了 150 秒的启动窗口,在它成功之前 liveness 不会生效,所以不需要再给 liveness 加很长的 initialDelaySeconds。这个组合对 JVM 冷启动、类加载、连接池初始化都更友好。
优雅停机:滚动更新不丢请求
只配探针还不够。K8s 删除 Pod 时,会同时做两件事:把 Pod 从 Endpoints 摘掉、给容器发 SIGTERM。这两件事是并行的,而 iptables/Ingress 的规则更新有延迟,所以会出现“流量还在进来,应用已经开始关闭”的窗口。
preStop 的 sleep 10 就是用来填这个窗口的:先让调用方感知到实例下线,再真正停服。Spring Boot 侧的 server.shutdown=graceful 会停止接收新请求,等待在途请求完成,最多等 timeout-per-shutdown-phase。
三者的大小关系要理清:terminationGracePeriodSeconds 必须大于 preStop 时间加上优雅停机超时,否则 K8s 会在应用还没处理完时发 SIGKILL。60 秒对应 10 秒 preStop 加 30 秒停机,留了 20 秒余量,是比较稳的配置。
JVM 参数也要配合容器内存:
-XX:MaxRAMPercentage=75.0
-XX:+ExitOnOutOfMemoryError
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/dump
堆占容器内存的 75% 左右,剩下留给元空间、线程栈、直接内存和系统开销。ExitOnOutOfMemoryError 让进程在 OOM 后主动退出,交给 K8s 重启,比半死不活地撑着更好。
常见坑与排查
坑一:liveness 指向 /actuator/health。 数据库慢查询导致 health 超时,三个副本同时被重启,流量全断。liveness 只指 /liveness。
坑二:探针端口写错。 如果配了 management.server.port,探针要指管理端口;很多人复制了业务端口的配置,探针一直失败。
坑三:安全框架拦了 actuator。 Spring Security 默认拦截所有路径,探针返回 401,Pod 永远不 Ready。要把 /actuator/health/** 放行,且只对内网开放。
坑四:readiness 里加了太多依赖。 加了下游服务、消息队列、配置中心,任何一个抖动都会让 Pod 被摘流量,滚动更新直接卡住。
坑五:没有 startupProbe。 应用启动要 60 秒,liveness 的 initialDelaySeconds 只给了 30 秒,于是启动即重启,循环到永远。
坑六:OOMKilled 被当成探针失败。 退出码 137、describe 里显示 OOMKilled,说明是内存超限,不是健康检查问题。调小堆或者调大 limit,别去改探针参数。
排查时先 kubectl describe pod 看 Events 里的 Liveness probe failed、Readiness probe failed、OOMKilled,再 kubectl logs --previous 看应用退出前的日志,然后进容器 curl 探针地址确认返回码。滚动更新卡住时,先看新 Pod 的 readiness,再看 kubectl rollout status 的具体提示。
评论