Spring Boot 项目在 K8S 中的打包、部署与运维发布实践
面向运维工程师,从基础概念到流水线发布、再到上线保障与故障排查,逐步建立对
Spring Boot + Docker + K8S + JVM的完整认知。
一、为什么运维要掌握 Spring Boot 项目的发布链路
1. 本文目标
- 说明运维为什么不能只会
kubectl apply - 建立从源码到上线的完整交付视角
- 帮助运维理解
Spring Boot项目为什么能以jar形式运行 - 帮助运维提升
K8S流水线发布成功率
2. 适合谁看
- 正在接触 Java 项目交付的运维工程师
- 想搞懂
jar、war、Tomcat、Maven关系的人 - 想系统学习
Spring Boot在K8S中部署流程的人
3. 运维最容易踩的坑
- 只会改镜像 tag,不理解交付物类型
- 不清楚
jar和war的运行环境差异 - 不理解
JVM与容器内存限制的关系 - 不会判断到底是构建失败、镜像失败,还是
K8S启动失败
1. 什么是 Java 项目的"交付物"
-
什么是源码:
源码就是源代码。
-
什么是编译产物:
编译产物就是源码经过编译处理之后的交付结果,比如 Java 的源码经过 Maven 处理后,会生成
.class文件、jar包、war包。 -
什么是
jar
jar是Java Archive,是 Java 的一种打包格式,本质上是基于 zip 格式的压缩包,专门用于打包 Java 类文件、资源文件和元数据。里面会放置编译后的一些文件:编译后的
.class文件、依赖包、配置文件、资源文件。
jar包主要分为普通jar包和可执行jar包。普通
jar包仅包含编译后的.class文件和资源,无外部依赖,不能直接运行;而可运行jar中通常会把依赖和内嵌的 Web 容器一起打包进去,不需要外置 Tomcat,可以通过java -jar直接执行。Spring Boot 应用就属于这种一键部署方式。 -
什么是
war
war包是Web Application Archive,是 Java Web 应用的打包格式,同样基于 zip 压缩,专门用于打包 Web 应用资源(servlet、jsp、静态资源等)。与jar相比,war仅适用于 Java Web 应用,结构固定(需包含WEB-INF/、WEB-INF/classes/、WEB-INF/lib/等),必须部署到 Servlet 容器(如 Tomcat)中,由容器负责加载、初始化和调度。传统的 Java Web 应用(如 Spring MVC)常使用这种方式。
jar 包的典型架构:
text
myapp.jar
├── META-INF/
│ └── MANIFEST.MF # 包含Main-Class和Class-Path配置
├── BOOT-INF/
│ ├── classes/ # 业务代码.class文件
│ └── lib/ # 所有依赖Jar包
└── org/springframework/boot/loader/ # Spring Boot启动器
war 包典型架构:
text
myapp.war
├── WEB-INF/
│ ├── web.xml # Web应用配置文件(传统项目必需)
│ ├── classes/ # 业务代码.class文件
│ └── lib/ # Web应用依赖Jar包
├── META-INF/
│ └── MANIFEST.MF
├── index.jsp # JSP页面
├── static/ # 静态资源(CSS、JS、图片)
└── templates/ # 模板文件(如Thymeleaf、FreeMarker)
什么是 Spring Boot
-
Spring Boot解决了什么问题springboot 的核心目标主要是为了简化 spring 应用的初始化搭建和开发过程。主要解决传统 spring 开发配置繁琐、依赖管理复杂、部署麻烦、监控缺失等问题,让开发者能够更快上手,专注于业务逻辑开发。
-
为什么它常见产物是可执行
jar主要是因为 Spring Boot 构建出的
jar包能够独立运行、简化部署,符合微服务架构需求,保证服务独立部署运行,不需要外部环境依赖。适合通过 Docker 等容器化方式进行启动。运维侧也更加方便,更新回滚时仅替换文件即可。 -
内嵌
Tomcat的基本原理springboot 内嵌 tomcat 的核心是将 tomcat 作为普通的 Java 对象运行在应用中,而非独立的外部进程。
当引入
spring-boot-starter-web时,会自动传递引入 Tomcat 内嵌依赖:
tomcat-embed-core是 Tomcat 核心引擎,tomcat-embed-el是表达式语言支持,tomcat-embed-websocket是 WebSocket 支持(可选)。Spring Boot 通过自动配置机制完成 Tomcat 的初始化和启动。
4. 什么是 Maven
maven 是 Java 项目的自动化构建和依赖管理工具。
你可以把它理解成 Java 项目的管家:自动帮你下载所有 jar 包,自动帮你编译、打包、运行、测试,统一管理项目结构、版本、依赖。
一句话:不用手动找 jar、手动导包、手动配置,Maven 全部自动化搞定。
pom.xml 是 Maven 项目的核心配置文件,使用哪些依赖、如何打包、使用什么插件,都在这里定义。
Spring Boot 项目的打包发布
springboot 项目在前面已经介绍过了,通过 Java 就可以启动构建好的 jar 包。
通常会先通过 Docker 镜像进行打包,然后再到集群中部署。
下面详细介绍一下 Spring Boot 项目的打包构建流程。
Jenkins 流水线发布流程
现在使用的流水线打包配置,基本都通过 Jenkins 来实现,而 Jenkins 中的打包过程主要有以下几步:
在当前的流水线平台架构上,通过定义好的 Jenkinsfile 来指导 Jenkins 的构建流程。这里就构建、打包以及发布的过程进行梳理:
- Jenkins 拉取配置仓,将提前定义好的部署文件、
Dockerfile等文件放到工作目录。 - Jenkins 根据填写的仓库地址拉取项目源码。
- Jenkins 中进行编译检查,使用
mvn complie。 - 触发 Docker 构建。Docker 构建一般可以分为两种:先构建打包镜像产物,再构建运行镜像;或者全部放到 Docker 构建中做多阶段构建。
两种方式各有优劣。为了更好地管理流水线、减少磁盘空间占用,线上采用第二种方式,将打包和构建都放到一个 Dockerfile 中执行。
示例 Dockerfile:
dockerfile
FROM maven:3.3.9 as BUILD
#构建构建镜像且命名为build
COPY . /usr/app/
RUN cd /usr/app; mvn clean package -Dmaven.test.skip=true
## 运行打包命令
FROM your-jdk-runtime:8
#导入运行镜像
COPY --from=BUILD /usr/app/your-app/target/your-app.jar /usr/local/apps/
## 关键,将从前一个名为build的构建阶段容器的文件系统里面,把文件复制到当前这个运行阶段的镜像里面。,这就是将构建和运行分开。
ENV APP_BASE /usr/local/apps/
WORKDIR /usr/local/apps/
RUN ping -c 4 gitlab.example.com && \
yum install -y git && mkdir -p /tmp/gitfile && \
cd /tmp/gitfile && git init && \
git remote add origin -f gitlab.example.com/example-group/shell.git && \
echo springboot/start.sh >> .git/info/sparse-checkout && \
git config core.sparsecheckout true && \
git pull origin master && chmod +x /tmp/gitfile/springboot/start.sh && \
mv /tmp/gitfile/springboot/start.sh /usr/local/apps/
## 这里是做一些见检查,然后拉取一个启动脚本,启动脚本中定义的一些环境变量的相关信息。
EXPOSE 8080
## springboot的主业务端口
EXPOSE 10090
## 管理端口
CMD ["./start.sh", "your-app.jar"]
#通过脚本去启动jar包
从上面的 Dockerfile 中可以看到,Jenkins 没有在宿主机上直接执行 mvn 打包命令。Jenkins 负责触发 docker build,而实际的打包构建是在 docker build 阶段执行的 mvn clean package,也就是 jar 包是在容器构建中生成的。
可以看出,在打包构建阶段引用的是 maven:3.3.9,然后执行的打包命令是 mvn clean package -Dmaven.test.skip=true。
打包完成之后,再把 jar 拷贝到运行镜像目录中,构建出最终运行镜像。
- 在运行镜像构建完成之后,Jenkins 会生成一个带时间戳、
commit id、随机串的 tag,然后推送到 Harbor,再进入部署阶段。 - 部署阶段,Jenkins 通过选择指定的 K8S 环境,调用
deployment.yaml,替换 YAML 中的镜像名为构建完成的运行镜像,然后执行发布操作。
流水线环节中容易失败的地方
Maven依赖下载失败(私服Nexus/Artifactory不可用、认证过期、仓库地址变更、网络超时)- 源码拉取失败(
Git凭证失效、分支/tag 不存在、子模块或大仓超时) Maven编译/测试失败(代码冲突、本地与流水线pom不一致、跳测参数与质量门禁不匹配)- 构建环境
JDK/Maven版本与本地或Dockerfile基镜像不一致 Dockerfile多阶段构建失败(路径写错、COPY --from阶段名错误、构建阶段内存不足)- 镜像构建失败(基础镜像拉取失败、
RUN命令非 0 退出、磁盘空间满) - 镜像推送失败(
Harbor登录过期、项目配额满、网络或 TLS 问题) K8S拉镜像失败(imagePullSecrets缺失、tag 未推上去、镜像名与部署 YAML 不一致)Pod启动失败(CrashLoopBackOff、配置/密钥缺失、JVM堆大于容器内存限制)- 探针失败(
readiness/liveness路径或端口与真实监听不一致、初始延迟过短、依赖未就绪) - 发布阶段失败(
YAML语法错误、资源配额不足、RBAC无权限、Deployment与HPA/PDB冲突)
运维如何提升发布成功率
- 固化构建基线 :流水线与
Dockerfile使用固定版本的JDK、Maven、基础镜像;重大升级走单独变更,避免「同一套流水线突然换版本」。 - 依赖与制品可复现 :私服高可用、凭证轮换有流程;必要时对关键依赖做缓存层或构建节点本地
.m2缓存策略,减少外网抖动影响。 - 镜像与部署联动 :
tag规则统一(时间戳 +commit id);部署前校验镜像在仓库中可拉取;imagePullPolicy与回滚策略和团队约定一致。 - 资源与 JVM 对齐 :为容器设置合理
requests/limits,JVM-Xmx等明显小于容器内存上限,避免OOMKilled;大构建任务单独调高构建 Pod/节点的内存与超时。 - 探针与启动顺序 :与研发确认健康检查 URL、端口、依赖就绪时间;适当调大
initialDelaySeconds,区分liveness与readiness语义,避免误杀仍在启动的进程。 - 配置与密钥 :
ConfigMap/Secret变更纳入发布 checklist;避免「只改镜像不改配置」导致启动即失败;密钥轮换后同步更新K8S与流水线凭据。 - 可观测与快速回滚 :发布前后看构建日志、事件
kubectl describe、Pod日志;保留上一版可用镜像 tag,出问题优先rollout undo或改回旧 tag。 - 分环境与灰度:测试/预发与生产隔离;生产尽量金丝雀或分批发布,降低单次失败影响面。
七、运维必须掌握的 JVM 基础与参数设置
1. 为什么运维要懂 JVM
- Java 服务性能和稳定性直接受
JVM影响 - 容器内存限制与
JVM堆配置强相关 - 发布成功不代表运行稳定
java 服务的所有代码都运行在 JVM 上,JVM 的行为直接决定了服务的性能和稳定性。所以 JVM 的配置很重要,关联业务代码是否能够稳定运行。
运维侧必须掌握的 JVM 技能:
- 能够看懂 GC 日志,识别 GC 频繁、GC 停顿过长。
- 会用
jstack抓取线程栈,定位死锁、CPU 高的线程。 - 会用
jmap抓取堆 dump,分析内存泄漏。 - 会配置基本的 JVM 参数(堆大小、GC 收集器)。
容器内存限制与 JVM 堆配置强相关。
因为容器的规格限制与 JVM 的限制相关,JVM 配置不能大于容器规格限制。JVM 默认运行在容器里面,而集群部署对于容器规格是有限制的。
例如在集群中通过 resources.limits.memory=2G 限制了容器占用内存的大小,如果 JVM 占用大于 2G,那么容器会被识别为超出限制并直接重启。这时候会触发 OOM,Pod 会被强制重启。
而在 Pod 日志中,仅能看到退出原因为 OOMKilled。
在 Java 11+ 版本中,自带容器感知能力,默认使用容器内存的 25% 作为堆大小。
所以在 JVM 示例中,通常需要显式指定 JVM 大小。
例如:
yaml
env:
- name: JAVA_OPTS
value: "-XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=75.0"
resources:
limits:
memory: "2Gi"
常见问题是服务部署发布完成后,运行一段时间 Pod 又会被重启,服务偶尔出现异常中断。
JVM 的问题大多是累积形式:1. 内存泄漏;2. GC 问题;3. 线程问题;4. 大对象问题等。
所以在发布后,不能只关注 Pod 是否 Running,还需要关注 Pod 的内存使用情况、GC 次数等指标。
介绍一下 GC:
GC 是垃圾回收机制,是 JVM 自带的自动内存管理机制。
你可以把 JVM 堆内存想象成一个仓库:
- 你的 Java 代码运行时,会不断创建新对象(往仓库里放东西)
- 有些对象用完就没用了(变成垃圾)
- GC 就是仓库的清洁工,自动把没用的垃圾清走,腾出空间
为什么 GC 会搞崩你的服务:
因为 GC 在工作时,会暂停所有业务线程。这个暂停就是 STW,是很多 Java 服务卡顿的根源。
GC 的类型有 Young GC 和 Full GC。其中 Young GC 频繁问题不大,只要不耗时太长;Full GC 是更危险的信号,只要 Full GC 超过 1 次 / 分钟,或者单次超过 1 秒,服务通常就会出问题。
而在容器 Pod 中,查看 GC 日志和 GC 指标,才能定位问题。
方式 1:进入 Pod 直接查看 GC 日志(前提是 GC 日志开启了打印和收集)。
常见的位置:
bash
# 最常见:和 app.jar 同目录
ls -l /app/gc*.log
# 有些项目会放在 logs 目录
ls -l /app/logs/gc*.log
# 找不到就全局搜
find / -name "gc*.log" 2>/dev/null
示例:
bash
# Full GC 日志(重点看这行)
2024-05-20T10:30:00.123+08:00: [Full GC (System.gc()) 1500M->800M(2048M), 2.5s]
从这条记录中可以看到,发生了 Full GC,发生前使用了 1500M,GC 后使用了 800M。
这次 GC 的总耗时时间是 2.5s。
抓取堆栈 dump 和线程栈:
当发现 Full GC 频繁、内存一直上涨时,就要怀疑是内存泄漏,此时要抓取堆 dump(内存快照)来进行分析。
抓取方式:
bash
# 进入 Pod
kubectl exec -it <pod-name> -- /bin/bash
# 找到 Java 进程 ID(一般是 1,因为容器里只有一个进程)
jps
# 抓取堆 dump(会生成一个 hprof 文件)
jmap -dump:format=b,file=/app/heapdump.hprof 1
# 从 Pod 复制到本地
kubectl cp <pod-name>:/app/heapdump.hprof ./heapdump.hprof
使用工具分析 dump 文件:
Eclipse MAT:最常用的内存分析工具JProfiler:功能更强大的商业工具
Pod 内存使用率高不等于一定有问题。
举个例子:
- 你给 Pod 限制了 2G 内存
- JVM 堆配置了 1.5G
- 运行一段时间后,Pod 内存使用率到了 80%(1.6G)
这完全正常,因为 JVM 会把内存用满,然后触发 GC 回收。只要 GC 能回收,内存使用率就会降下来。
真正有问题的是:
- GC 后内存使用率还是 80% 以上
- Full GC 越来越频繁
- 内存使用率一直涨,直到 OOM
运维排查 GC 问题标准流程:
- 发现问题:Grafana 告警
Full GC频繁、接口超时或 Pod 内存使用率高。 - 初步判断:
kubectl logs <pod-name> | grep "Full GC",看 Full GC 次数和耗时。 - 深入分析:进入 Pod 看完整 GC 日志,看 GC 前后内存变化。
- 抓取证据:如果怀疑内存泄漏,抓取堆 dump。
- 临时解决:重启 Pod(能暂时缓解,但不能根治)。
- 根治问题:分析 dump 文件,找到泄漏点,让开发修复。