Spring Boot 明明正常,为什么 Kubernetes 每隔几分钟重启一次?
Spring Boot 明明正常,为什么 Kubernetes 每隔几分钟重启一次?
应用日志没有 OOM,也没有主动退出,接口偶尔只是慢了几秒。
但 Pod 的重启次数不断上涨,最终所有实例轮流重启。
根因不是 Spring Boot 崩了,而是一条写错的存活检查在反复“杀死”正常进程。
说明:本文基于常见容器场景复现,日志、名称和参数均为示例。
一、应用没报错,Pod 却一直重启
先看 Pod:
kubectl get pod -n prod
输出:
NAME READY STATUS RESTARTS AGE
order-service-7f8b9c-x1 1/1 Running 6 2h
order-service-7f8b9c-x2 1/1 Running 4 2h
order-service-7f8b9c-x3 1/1 Running 5 2h
应用现在是 Running,但两个小时已经重启了多次。
查看当前日志,没有明显异常:
kubectl logs -n prod order-service-7f8b9c-x1
只看当前日志很容易得出错误结论,因为容器重启后,看到的已经是新进程。
应该继续查看上一个容器的日志:
kubectl logs -n prod order-service-7f8b9c-x1 --previous
再查看 Pod 事件:
kubectl describe pod -n prod order-service-7f8b9c-x1
发现了关键线索:
Warning Unhealthy Liveness probe failed: HTTP probe failed with statuscode: 503
Normal Killing Container failed liveness probe, will be restarted
不是应用自己退出,而是 livenessProbe 连续失败后,Kubernetes 主动重启了容器。
二、问题出在一条“看起来很完善”的健康检查
原来的探针配置:
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
/actuator/health 又聚合了:
应用自身状态
MySQL
Redis
消息队列
一个远程配置接口
当数据库短暂抖动或远程接口超过 2 秒时,健康接口返回 DOWN 或超时。
三次失败后,Kubernetes 得出结论:
这个进程已经无法工作,需要重启
但实际情况是:
Java 进程仍然正常
部分不依赖数据库的接口仍可使用
数据库只是短暂变慢
重启应用并不能修复数据库
于是故障被放大:
数据库短暂抖动
-> 所有 Pod 的存活检查陆续失败
-> Kubernetes 开始重启 Pod
-> 剩余实例流量增加
-> 剩余实例也变慢
-> 更多探针失败
-> 形成连续重启
这就是典型的健康检查雪崩。
三、Liveness、Readiness、Startup 到底怎么分?
Liveness:进程还能不能自我恢复
存活检查回答的是:
这个进程是否已经进入无法自行恢复的状态,需要被重启?
适合判断:
应用内部状态已经损坏
关键线程永久退出
死锁或内部组件不可恢复
不适合直接绑定:
MySQL 是否可用
Redis 是否可用
外部 HTTP 接口是否可用
消息队列是否短暂延迟
因为重启 Spring Boot 修不好外部系统。
Readiness:当前是否应该接收流量
就绪检查回答的是:
这个实例现在能否处理业务请求?
失败时,实例可以暂时退出流量,但进程不一定需要重启。
外部依赖要不要加入 Readiness 也要谨慎。如果所有实例共享同一个数据库,数据库一抖就让全部实例同时退出流量,仍可能扩大故障。
Startup:是否还在启动阶段
启动检查用于保护启动较慢的应用。
如果应用需要初始化缓存、加载规则或执行迁移,启动时间可能超过普通存活检查的容忍范围。配置 startupProbe 后,在它成功之前,Kubernetes 不会执行 Liveness 和 Readiness。
它可以避免:
应用还没启动完成
-> Liveness 已经开始检查
-> 连续失败
-> 应用被杀掉重启
-> 永远启动不起来
四、Spring Boot 可以使用独立探针端点
加入 Actuator 后,可以使用:
/actuator/health/liveness
/actuator/health/readiness
配置示例:
management:
endpoint:
health:
probes:
enabled: true
show-details: never
Kubernetes 示例:
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 5
failureThreshold: 24
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
参数只是示例,需要按真实启动时间、接口波动和故障恢复目标调整。
还有一个容易忽略的点:如果 Actuator 使用独立管理端口,管理端口正常不代表业务端口一定能接收请求。可以评估把探针附加路径暴露在主服务端口上:
management:
endpoint:
health:
probes:
add-additional-paths: true
这样可使用主端口上的 /livez 和 /readyz,避免管理端口活着、业务 Web 线程却已经完全阻塞的假健康。
五、排查 Pod 反复重启,我先看这 6 项
1. 重启次数和状态
kubectl get pod -n <namespace> -o wide
2. Pod 事件
kubectl describe pod -n <namespace> <pod-name>
重点找:
Unhealthy
Killing
OOMKilled
BackOff
Evicted
3. 上一个容器的日志
kubectl logs -n <namespace> <pod-name> --previous
4. 上一次退出原因
kubectl get pod -n <namespace> <pod-name> \
-o jsonpath='{.status.containerStatuses[0].lastState}'
5. 探针实际配置
kubectl get pod -n <namespace> <pod-name> -o yaml
确认路径、端口、超时、周期、失败阈值和启动等待。
6. 探针失败时的外部依赖
把失败时间和下面指标对齐:
数据库 RT
Redis RT
应用 GC
CPU 限流
业务线程池
网络丢包
不要看到 RESTARTS 增加就直接给内存扩容。
六、修复健康检查时,避免两个极端
第一个极端是检查太深:
一次 Liveness 调用五个外部系统
任何一个抖动都重启应用
第二个极端是检查太浅:
只要端口能建立连接就返回 200
业务线程全部堵死仍然显示健康
更合理的思路是:
Startup 保护启动阶段
Liveness 判断应用内部是否不可恢复
Readiness 判断是否适合继续接流量
业务监控负责判断外部依赖和核心交易是否正常
探针不是完整监控系统。
它的返回值会直接触发“摘流量”或“重启”这种控制动作,因此必须比普通监控告警更克制。
七、总结
这次问题的故障链路是:
Liveness 绑定了数据库和远程接口
-> 外部依赖短暂变慢
-> 存活检查连续失败
-> Kubernetes 重启正常应用
-> 剩余实例流量增加
-> 更多实例检查失败
最后记住:
Liveness 失败意味着“重启我可能有用”,而不是“我依赖的任何东西暂时不可用”。
如果你正在排查 Pod 反复重启、健康检查失败或 Spring Boot 容器部署问题,可以关注我,协助帮忙看一眼脱敏后的 Pod 事件、探针配置和退出日志,协助判断方向。请勿发送集群凭证、Token、私钥及未脱敏业务数据。
更多推荐

所有评论(0)