把 Spring Boot 应用搬到 K8s 上,网上教程一大堆,但大多停留在”跑起来”的层面。实际落地时,有一批问题是教程不会告诉你的。下面记录我实际遇到的 5 个坑,每个都有具体的解法。

坑 1:健康检查配置不当导致频繁重启

K8s 的 livenessProbereadinessProbe 很多人直接用默认配置,结果应用启动慢一点就被 K8s 判定为不健康,然后反复重启。

问题根源: Spring Boot 应用启动时要加载 Spring 容器、连接数据库、预热缓存,时间可能超过默认的探测超时。

正确配置:

livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  initialDelaySeconds: 60   # 给足启动时间
  periodSeconds: 15
  failureThreshold: 3
  timeoutSeconds: 5

readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  failureThreshold: 3
  timeoutSeconds: 5

同时在 application.yml 中开启 Actuator 的分组健康检查:

management:
  health:
    livenessstate:
      enabled: true
    readinessstate:
      enabled: true
  endpoint:
    health:
      probes:
        enabled: true

liveness 只检查应用是否存活(JVM 是否正常),readiness 检查是否准备好接收流量(数据库连接是否正常等)。两个分开配置,避免误杀。


坑 2:ConfigMap 更新了但应用没生效

把配置放到 ConfigMap,更新后发现应用完全没变化。原因是:K8s 更新 ConfigMap 不会自动重启 Pod,挂载为文件的 ConfigMap 会延迟同步(通常 1-2 分钟),但环境变量方式注入的永远不会更新。

三种方案:

方案实时性复杂度
更新 ConfigMap 后手动 rollout restart手动触发
用 Spring Cloud Config Server实时推送
Reloader(第三方控制器)自动检测

对于大多数场景,最简单的方案就够用:

kubectl rollout restart deployment/your-app -n your-namespace

把这个命令加到 CI/CD 流水线里,ConfigMap 更新后自动触发重启。


坑 3:JVM 内存设置与容器内存不匹配

不设置 JVM 堆内存上限时,JVM 会尝试使用容器的全部内存,然后被 OOM Killer 干掉。

正确做法: 在容器的环境变量里显式设置 JVM 参数:

env:
  - name: JAVA_OPTS
    value: "-Xms512m -Xmx1g -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
resources:
  requests:
    memory: "1Gi"
    cpu: "500m"
  limits:
    memory: "1.5Gi"
    cpu: "1000m"

关键参数说明:

  • -XX:+UseContainerSupport:让 JVM 感知容器内存限制(JDK 8u191+ 默认开启)
  • -XX:MaxRAMPercentage=75.0:堆最多用容器内存的 75%,留空间给 Metaspace、线程栈、堆外内存
  • memory limit 设为 Xmx 的 1.5 倍,给非堆内存留余量

坑 4:应用收到 SIGTERM 后直接中断请求

K8s 缩容或滚动更新时,会向 Pod 发送 SIGTERM 信号,默认情况下 Spring Boot 应用收到信号后立刻退出,正在处理的请求直接中断。

开启优雅停机:

# application.yml
server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

同时在 K8s Deployment 里配置 terminationGracePeriodSeconds,要比 Spring 的超时时间稍大:

spec:
  template:
    spec:
      terminationGracePeriodSeconds: 40  # 比 Spring 的 30s 稍大
      containers:
        - name: your-app
          lifecycle:
            preStop:
              exec:
                command: ["sh", "-c", "sleep 5"]  # 等待 K8s 更新 Endpoints

preStopsleep 5 是关键:K8s 更新 Endpoints(从 Service 摘掉这个 Pod)需要时间,如果立刻开始停机,新请求可能还会路由过来。


坑 5:日志在容器里消失了

容器重启后,之前的日志就没了。如果用 logback 把日志写到容器内的文件,重启一次等于日志清空。

正确方案:输出到 stdout,让 K8s 日志系统收集

修改 logback-spring.xml,去掉文件 appender,只保留 Console:

<configuration>
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <!-- JSON 格式,方便日志系统解析 -->
      <pattern>{"time":"%date{ISO8601}","level":"%level","logger":"%logger{36}","msg":"%message"}%n</pattern>
    </encoder>
  </appender>

  <root level="INFO">
    <appender-ref ref="CONSOLE" />
  </root>
</configuration>

输出到 stdout 后,用 kubectl logs 可以实时查看,EFK/Loki 等日志系统也可以直接收集。


总结

问题解法关键点
频繁重启配置足够的 initialDelaySeconds,分开 liveness/readiness
ConfigMap 不生效更新后触发 rollout restart
OOM 被杀显式设置 -Xmx,用 MaxRAMPercentage
请求中断开启 server.shutdown: graceful,加 preStop sleep
日志消失只输出到 stdout,不写文件

这 5 个问题我都在生产环境实际遇到过,修复后系统稳定性明显提升。K8s 本身不复杂,大多数坑是对 K8s 工作机制不了解导致的。