帮客户搞定 Spring Boot 生产部署:15 天踩坑与架构决策实录
前不久帮一家做供应链管理的客户重构了他们的后端系统,核心任务是把原本跑在虚拟机上的 Spring Boot 单体应用,迁移到 Kubernetes 集群里,还要保证高并发下的稳定性。客户那边用的是 Java 21 和最新的 Spring Boot 版本,毕竟到了 2026 年,这俩已经是标配了。项目规模不算大,但痛点很明确:老系统启动慢,每次扩容要等好几分钟,而且监控数据散落在各个日志文件里,出了问题根本定位不到根因。说实话,我当时觉得把 Dockerfile 写好、Jenkins 流水线配通就完事了,结果真正上手才发现,云原生部署里的坑比想象中多得多。
容器化与构建:从 JAR 到 Native Image 的挣扎
客户最开始想沿用传统的 JAR 包部署,但我坚持建议他们试试 GraalVM 原生镜像。你知道的,Spring Boot 3.5+ 对 AOT(提前编译)的支持已经非常成熟了,启动速度能压到毫秒级。对于他们那种需要快速弹性缩放的场景,原生镜像简直是救命稻草。
不过,试了一圈发现,迁移过程并不顺畅。我们在 CI/CD 流水线里用了 Kaniko 来构建镜像,因为它不需要 Docker Daemon,安全性高,适合在 K8s 内部署。但问题出在反射处理上。我们的业务代码里用了很多第三方库的动态代理,GraalVM 在编译时需要提前知道这些类的元数据。
当时我觉得只要加几个reflection-config.json文件就行,结果构建一直报错,说找不到某些类。排查了半天,发现是因为 Spring AI 的集成引入了一些新的依赖链,这些依赖在原生环境下行为变了。有意思的是,我们最终没有去手动维护那些配置文件,而是转向了更自动化的方案——引入了 Spring Boot 的原生代理生成插件。
```dockerfile
基于 Distroless 镜像,移除所有 shell 和包管理器
FROM gcr.io/distroless/java21-debian12:nonroot
将原生可执行文件复制进去
COPY target/my-app /home/app/my-app
设置运行用户
USER nonroot
暴露端口
EXPOSE 8080
启动命令
ENTRYPOINT ["/home/app/my-app"]
```
这里做了一个关键的架构决策:放弃 Alpine 镜像。虽然它体积小,但 musl libc 和 glibc 的差异在原生编译后容易引发隐蔽的系统调用错误。我们最终选择了 Distroless,虽然体积稍大一点,但胜在稳定,攻击面也最小。
可观测性与监控:OpenTelemetry 的统一之路
系统跑起来后,另一个头疼的问题是监控。以前我们用 Micrometer 对接 Prometheus,现在得全面转向 OpenTelemetry (OTel)。客户希望实现 Tracing、Metrics 和 Logs 的统一,这样在排查问题时,能从链路追踪直接关联到具体的日志行。
刚开始配置 OTel Agent 时,我发现 Trace 数据丢失严重。我以为是网络问题,调大了超时时间也没用。后来冷静下来想了想,可能是采样策略的问题。在生产环境全量采样肯定扛不住存储压力,但太稀疏又抓不到关键异常。
最终我们定了一个混合采样策略:对于错误请求和慢查询(P99 > 1s),强制采样;其他请求按 10% 随机采样。这样既保证了关键数据的完整性,又控制了成本。
```java
// 自定义 Sampler 配置示例
public class ProductionSampler implements Sampler {
@Override
public SamplingResult decide(SamplingContext context) {
// 如果是错误或者慢请求,永远采样
if (context.getAttributes().containsKey("error") ||
context.getDuration().toMillis() > 1000) {
return SamplingResult.recordAndSample();
}
// 否则 10% 概率采样
return Math.random() < 0.1 ? SamplingResult.recordAndSample() : SamplingResult.drop();
}
}
```
日志方面,我们也做了调整。不再输出纯文本,而是全部改为 JSON 格式。这样 Loki 解析起来非常快,而且可以直接通过 Trace ID 关联到 OTel 的数据。说实话,一开始团队挺抵触改日志格式的,因为本地调试看着费劲,但一旦上线,这种结构化日志的价值就体现出来了,查问题效率提升了不止一倍。
性能调优与动态配置
最后聊聊性能。Spring Boot 3.5+ 默认启用了 ZGC,这对低延迟应用非常友好。但在高并发场景下,我们发现 Full GC 还是偶尔会发生。排查发现,是因为一些旧的缓存实现没有及时释放引用。我们把这些缓存替换成了基于 Caffeine 的本地缓存,并设置了合理的过期策略。
配置管理上,客户之前用的是硬编码在 application.yml 里的方式,改个参数要重新打包镜像。这次我们接入了 Nacos 作为配置中心,实现了配置的热更新。不过,这里有个坑:有些核心配置(比如数据库连接池大小)修改后,Spring Boot 不会自动刷新底层连接池。我们不得不写了一个自定义的 RefreshScope 处理器,监听特定配置的变化,然后手动触发连接池的重置。
当时我觉得这样处理有点重,但为了确保生产环境的安全,没有连接池的中断比任何优雅的重启都重要。
这一套流程走下来,客户的系统启动时间从原来的 45 秒降到了 3 秒,内存占用减少了 40%,最重要的是,现在出了 bug,运维能在 5 分钟内定位到问题根源。虽然过程挺折腾,但看到效果,还是觉得值得。
本文基于实际项目经验整理,如有技术问题欢迎私信交流。