ElIauk

Spring Boot 部署到 K8s 与探针

探针配错,比不配更危险 把 Spring Boot 丢进 K8s,最容易出事的地方不是镜像,也不是资源限制,而是健康探针。探针配得太敏感,应用刚启动就被判死、反复重启;配得太宽,挂了半天流量还在往上打;把数据库检查塞进 liveness,数据库抖一下,所有 Pod 一起重启,故障被放大一圈。 先记住

评论
Spring Boot 部署到 K8s 与探针

探针配错,比不配更危险

把 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 的具体提示。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者