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、私钥及未脱敏业务数据。

更多推荐