把 Spring Boot 应用搬到 K8s 上,网上教程一大堆,但大多停留在”跑起来”的层面。实际落地时,有一批问题是教程不会告诉你的。下面记录我实际遇到的 5 个坑,每个都有具体的解法。
坑 1:健康检查配置不当导致频繁重启
K8s 的 livenessProbe 和 readinessProbe 很多人直接用默认配置,结果应用启动慢一点就被 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
preStop 的 sleep 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 工作机制不了解导致的。