Jenkins 后续构建原理讲解
概述
本文是对上文的一些原理补充,上文连接:https://blog.csdn.net/weixin_52748928/article/details/162829807?spm=1011.2124.3001.6209
jenkins 有一个关键的问题,就是提高构建速度,以下很多步骤大多是用来解决这一问题的。
因为上文中jenkins是在docker中部署的,并在构建时需要执行docker 命令,因此就有了将宿主机的 Docker CLI 挂载进容器这个步骤。
由于每次都要构建maven下载依赖,我们把maven 容器的下载路经挂载到宿主机上,这样第二次下载时只下载增量文件,用于提高速度。
由于每次打镜像后镜像比较大,上传时间比较长。为了解决这个问题,一是增加网络带宽,二是减小镜像大小。本文针对减小镜像大小做了一些操作。使用了分层 JAR + Dockerfile 分层 COPY的方案。该方案具具体实现如下:
在pom 中开启分层,并在dockerfile 中复制分层
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.5.0</version>
<!-- 启用分层功能-->
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
# 【核心优化】按依赖顺序分层复制文件,充分利用 Docker 的层缓存机制。
# 第 1 层:复制第三方依赖(除非修改 pom.xml,否则此层命中缓存,无需重建)
COPY --from=builder /app/dependencies/ ./
# 第 2 层:复制 Spring Boot 加载器类(除非升级 Spring Boot 版本,否则命中缓存)
COPY --from=builder /app/spring-boot-loader/ ./
# 第 3 层:复制 SNAPSHOT 依赖(变更频率中等)
COPY --from=builder /app/snapshot-dependencies/ ./
# 第 4 层:复制业务代码和资源(每次代码提交都会变化,仅这一小层需要重新构建和推送)
COPY --from=builder /app/application/ ./
原理详解
为什么需要 Docker Socket 挂载?
宿主机
┌─────────────────────────────────────────────────┐
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Docker 守护进程 │◄───│ /var/run/docker.sock│ │
│ │ (dockerd) │ └──────────┬───────────┘ │
│ └──────┬───────┘ │ │
│ │ 挂载 │
│ ┌──────▼───────────────────────▼──────────┐ │
│ │ Jenkins 容器 │ │
│ │ docker build / docker push 等命令 │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
Docker 采用 C/S 架构:
- dockerd(守护进程):真正执行容器管理、镜像构建等操作
- docker CLI(客户端):解析命令行参数并通过 REST API 与 dockerd 通信
默认情况下,dockerd 监听 /var/run/docker.sock(Unix Socket)。将宿主机的 Docker Socket 挂载到 Jenkins 容器内,本质上是让容器内的 docker CLI 能直接与宿主机的 dockerd 通信。
好处: Jenkins 容器内执行的 docker build / docker push 实际上是在宿主机上执行的,容器本身不需要运行 dockerd。
注意: 这被称为 "Docker in Docker" 的 socket 方案(DooD),与完整的 DinD(在容器内运行完整 dockerd)不同,socket 方案更轻量、更安全。
BuildKit 与传统 Docker Build 的区别
传统 Build(DOCKER_BUILDKIT=0):
┌──────────────┐ 顺序执行步骤 ┌──────────────┐
│ Dockerfile │ ───────────────► │ 镜像输出 │
│ Step 1-11 │ │ │
└──────────────┘ └──────────────┘
BuildKit 模式(DOCKER_BUILDKIT=1):
┌──────────────┐ 并发执行 ┌──────────────┐
│ Dockerfile │ ──► Step 1 ──► │ 镜像输出 │
│ Step 1-11 │ ──► Step 2 ──► │ │
│ │ ──► Step 3 ──► │ │
└──────────────┘ └──────────────┘
BuildKit 是 Docker 的下一代构建引擎:
- 并行构建: 无依赖的步骤可以并发执行
- 缓存优化: 更好的层缓存机制
- 安全性: 支持 seccomp、AppArmor 等安全配置
- 多平台: 支持构建多架构镜像(arm64/amd64 等)
本项目的选择: 显式禁用 BuildKit(DOCKER_BUILDKIT=0),因为服务器上没有安装 docker-buildx 组件。对于简单的 Spring Boot 镜像构建,传统引擎完全够用。
Maven 缓存挂载链(两层,非三层)
宿主机 Maven 编译容器
/opt/maven-cache/.m2 ────────────────────────────► /root/.m2
持久化 依赖下载目录
原理:
- 宿主机层: /opt/maven-cache/.m2 是真实的物理存储目录,所有依赖最终保存在这里
- Maven 编译容器层: Maven 容器是由 Jenkins 通过
docker run -v /opt/maven-cache/.m2:/root/.m2启动的,将宿主机目录直接挂载到 Maven 容器内
-
- 关键:Jenkins 容器通过挂载
/var/run/docker.sock来调用宿主机的 Docker daemon,所以-v中的路径是由宿主机文件系统解析的,不会经过 Jenkins 容器
- 关键:Jenkins 容器通过挂载
为什么这样设计?
- Jenkins 容器与 Maven 编译容器是两个独立的容器
- Maven 编译容器是临时创建的(--rm),每次构建结束后自动删除
- 如果不做持久化,Maven 容器删除后所有已下载的依赖也会消失
- 通过两层挂载,Maven 容器的 /root/.m2 最终指向宿主机 /opt/maven-cache/.m2,依赖被持久保存
dockerignore 的设计考量
**/target/ # Maven 编译输出(构建镜像时不需要)
.git/ # Git 元数据
*.md # 文档
Jenkinsfile # CI 配置
Dockerfile # Docker 构建配置
为什么排除 /target/?**
- docker build 会将 Dockerfile 所在目录(及子目录)作为构建上下文发送给 dockerd
- target/ 目录包含编译后的 class 文件和 JAR,体积大(一个 Spring Boot fat jar 约 50-100MB)
- 排除后可以显著减少上下文大小,加速 Docker build 的 "Sending build context" 阶段
为何需要 build-output/ 避开排除?
- 因为 */target/ 被排除了,所以 Dockerfile 不能直接 COPY MODULE/target/.jar
- 解决方案:在 Jenkins 的 Maven build 阶段,先将 JAR 复制到 build-output/(不在排除规则内)
- Dockerfile 再从 build-output/ 复制
为什么一次构建打两个镜像标签?
docker build -t registry/rag:abc123-33 -t registry/rag:latest .
|
标签 |
用途 |
特点 |
|
rag:abc123-33 |
固定版本标签 |
git短SHA-构建序号,每次构建唯一,可回滚 |
|
rag:latest |
当前最新标签 |
每次构建覆盖,部署时省去改版本号 |
两个标签指向完全相同的镜像 ID(相同的文件系统层),所以不占用额外空间。这是标准的 CI/CD 做法。
分层 JAR 节省时间的原理
核心就一句话:不变的数据只上传一次,后续构建只上传变化的部分。
Docker 镜像的层结构
没有分层时,一个 Spring Boot JAR 在 Docker 镜像里就是一个整体块:
Spring Boot 镜像(~180MB)
├── 基础 JDK 层(~70MB) ← 拉取,固定不变
└── fat JAR 层(~100MB) ← 每次代码变动都得重新推送整个 JAR
用了分层 JAR + Dockerfile 分层 COPY 后:
Spring Boot 镜像(~180MB)
├── 基础 JDK 层(~70MB) ← 拉取,固定不变
├── dependencies/(~50MB) ← 第三方依赖,pom.xml 不变就不变
├── spring-boot-loader/(~1MB) ← Spring Boot 加载器,几乎不变
├── snapshot-dependencies/(~5MB)← SNAPSHOT 依赖,偶尔变
└── application/(~2MB) ← 你自己的代码,**每次构建唯一变化的部分**
Docker 推送的工作原理
Docker 镜像由一组**内容寻址的层(content-addressable layers)**组成,每层都有一个基于内容的 SHA256 digest:
layer digest = SHA256(layer content)
推送时,Docker 向仓库逐个查询:
"我这里有层 sha256:xxxx,仓库你有吗?"
如果仓库返回 "已有"(Layer already exists),Docker 直接跳过上传,只传一个引用。这就是在构建时你看到的 Layer already exists 的那些行。这里的仓库指的是远程仓库。
前后对比
|
无分层(fat JAR) |
有分层(layered JAR) |
|
|
首次构建 |
推 ~100MB |
推 ~58MB(依赖 + loader + snapshot + application) |
|
后续构建 |
推 ~100MB(整个新 JAR) |
推 ~2MB(仅 application 层) |
|
构建时间 |
13.5 min |
2.2 min |
所以分层架构的关键优势是:dependencies/ 层作为最重的部分,只要 pom.xml 不变化,它的 digest 就完全相同,后续构建只需上传几 MB 的 application 层。 网络传输瓶颈从 "每次都推 100MB" 变成了 "每次都推 2MB",十几分钟就变成了两分钟。
更多推荐




所有评论(0)