Spring Boot 项目在 K8S 中的打包、部署与运维发布实践

Spring Boot 项目在 K8S 中的打包、部署与运维发布实践

面向运维工程师,从基础概念到流水线发布、再到上线保障与故障排查,逐步建立对 Spring Boot + Docker + K8S + JVM 的完整认知。


一、为什么运维要掌握 Spring Boot 项目的发布链路

1. 本文目标

  • 说明运维为什么不能只会 kubectl apply
  • 建立从源码到上线的完整交付视角
  • 帮助运维理解 Spring Boot 项目为什么能以 jar 形式运行
  • 帮助运维提升 K8S 流水线发布成功率

2. 适合谁看

  • 正在接触 Java 项目交付的运维工程师
  • 想搞懂 jarwarTomcatMaven 关系的人
  • 想系统学习 Spring BootK8S 中部署流程的人

3. 运维最容易踩的坑

  • 只会改镜像 tag,不理解交付物类型
  • 不清楚 jarwar 的运行环境差异
  • 不理解 JVM 与容器内存限制的关系
  • 不会判断到底是构建失败、镜像失败,还是 K8S 启动失败

1. 什么是 Java 项目的"交付物"

  • 什么是源码:

    源码就是源代码。

  • 什么是编译产物:

    编译产物就是源码经过编译处理之后的交付结果,比如 Java 的源码经过 Maven 处理后,会生成 .class 文件、jar 包、war 包。

  • 什么是 jar
    jarJava 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 应用资源(servletjsp、静态资源等)。与 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 的构建流程。这里就构建、打包以及发布的过程进行梳理:

  1. Jenkins 拉取配置仓,将提前定义好的部署文件、Dockerfile 等文件放到工作目录。
  2. Jenkins 根据填写的仓库地址拉取项目源码。
  3. Jenkins 中进行编译检查,使用 mvn complie
  4. 触发 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 拷贝到运行镜像目录中,构建出最终运行镜像。

  1. 在运行镜像构建完成之后,Jenkins 会生成一个带时间戳、commit id、随机串的 tag,然后推送到 Harbor,再进入部署阶段。
  2. 部署阶段,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 无权限、DeploymentHPA/PDB 冲突)

运维如何提升发布成功率

  • 固化构建基线 :流水线与 Dockerfile 使用固定版本的 JDKMaven、基础镜像;重大升级走单独变更,避免「同一套流水线突然换版本」。
  • 依赖与制品可复现 :私服高可用、凭证轮换有流程;必要时对关键依赖做缓存层或构建节点本地 .m2 缓存策略,减少外网抖动影响。
  • 镜像与部署联动tag 规则统一(时间戳 + commit id);部署前校验镜像在仓库中可拉取;imagePullPolicy 与回滚策略和团队约定一致。
  • 资源与 JVM 对齐 :为容器设置合理 requests/limitsJVM -Xmx 等明显小于容器内存上限,避免 OOMKilled;大构建任务单独调高构建 Pod/节点的内存与超时。
  • 探针与启动顺序 :与研发确认健康检查 URL、端口、依赖就绪时间;适当调大 initialDelaySeconds,区分 livenessreadiness 语义,避免误杀仍在启动的进程。
  • 配置与密钥ConfigMap/Secret 变更纳入发布 checklist;避免「只改镜像不改配置」导致启动即失败;密钥轮换后同步更新 K8S 与流水线凭据。
  • 可观测与快速回滚 :发布前后看构建日志、事件 kubectl describePod 日志;保留上一版可用镜像 tag,出问题优先 rollout undo 或改回旧 tag。
  • 分环境与灰度:测试/预发与生产隔离;生产尽量金丝雀或分批发布,降低单次失败影响面。

七、运维必须掌握的 JVM 基础与参数设置

1. 为什么运维要懂 JVM

  • Java 服务性能和稳定性直接受 JVM 影响
  • 容器内存限制与 JVM 堆配置强相关
  • 发布成功不代表运行稳定

java 服务的所有代码都运行在 JVM 上,JVM 的行为直接决定了服务的性能和稳定性。所以 JVM 的配置很重要,关联业务代码是否能够稳定运行。

运维侧必须掌握的 JVM 技能:

  1. 能够看懂 GC 日志,识别 GC 频繁、GC 停顿过长。
  2. 会用 jstack 抓取线程栈,定位死锁、CPU 高的线程。
  3. 会用 jmap 抓取堆 dump,分析内存泄漏。
  4. 会配置基本的 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 GCFull 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 问题标准流程:

  1. 发现问题:Grafana 告警 Full GC 频繁、接口超时或 Pod 内存使用率高。
  2. 初步判断:kubectl logs <pod-name> | grep "Full GC",看 Full GC 次数和耗时。
  3. 深入分析:进入 Pod 看完整 GC 日志,看 GC 前后内存变化。
  4. 抓取证据:如果怀疑内存泄漏,抓取堆 dump。
  5. 临时解决:重启 Pod(能暂时缓解,但不能根治)。
  6. 根治问题:分析 dump 文件,找到泄漏点,让开发修复。
相关推荐
a8a3021 小时前
Laravel8.x新特性全解析
java·spring boot·后端
Elastic 中国社区官方博客1 小时前
在 Elastic 中使用 MCP 自动化用户旅程以进行合成监控
大数据·运维·人工智能·elasticsearch·搜索引擎·自动化·可用性测试
长安链开源社区1 小时前
学者观察 | 基于区块链的隐私计算技术——北京理工大学教授祝烈煌
运维·区块链
白露与泡影2 小时前
Spring Boot 完整流程
java·spring boot·后端
learning-striving2 小时前
Ubuntu26.04下载安装教程
运维·服务器·vmware·虚拟机
码上行动 662 小时前
用U盘制作系统盘以及如何装系统
运维
invicinble2 小时前
关于搭建运维监控系统(Prometheus+Grafana)
运维·grafana·prometheus
__beginner__2 小时前
CentOS 磁盘占用异常排查与处理手册(df 高、du/ncdu 低)
linux·运维·centos
2501_927283582 小时前
荣联汇智立体仓库:为智慧工厂搭建高效“骨骼”与“中枢”
大数据·运维·人工智能·重构·自动化·制造