Java Web 微服务系列 · 第 9 篇 · Jenkins 流水线 / 完整 CI/CD
阅读时长:约 90 分钟
本文写于 2026 年 6 月
配套版本:Jenkins 2.452.x LTS / Kubernetes 1.29 / Argo Rollouts 1.7 / Helm 3.x
前置阅读:本系列第 5 篇「Nacos 服务中心与配置中心」(同 Java Web 微服务系列)
配套参考:站内 K8s 容器编排专题(多个分章节文章)
引子:2020 年那个凌晨 3 点的上线翻车
2020 年 11 月 11 日,距离双 11 大促开场还有 6 小时。
下午 5 点 30 分,我正准备关电脑去会场盯盘,钉钉群里弹出一条消息:「@我 order-service 修复一个 Nacos 配置 bug,紧急上线,11 点前必须完成」。
我看了一下 commit:改动 1 个 yaml 配置,3 行变更。看上去毫无风险。
接下来 90 分钟里,我们 5 个人上演了一出"上线翻车连续剧":
- 17:42 — 申请 30 分钟上线窗口(运维审批通过)
- 17:55 — ssh 到 1 号生产机,手动
scp 上传 app-order-service.jar(错误 1:5 台机器我一台一台 scp,忘了第 3 台) - 18:10 — 3 号机没启动新 jar,但 Nacos 配置中心已经推下去,3 号机用旧 jar 读新配置,启动直接失败
- 18:12 — 3 号机 Pod 持续重启,5 秒一次,Hystrix 触发熔断,订单查询接口返回 502
- 18:15 — 用户侧开始出现「订单提交失败」提示,监控告警开始轰炸
- 18:18 — 我和另两个 SRE 一边回滚 3 号机一边改 4 号机,越改越乱
- 18:30 — 第 4 号机配置文件错位(错误 2:复制粘贴时路径写错),订单服务彻底雪崩
- 18:35 — 大促预热开始,雪崩被监控捕获,公司副总裁电话打到我手机上
最终回滚 + 重启完成时间:18:52。大促预热 20 分钟完全不可用。
直接损失:
- 大促预热阶段订单流失估算 80 万+
- 副总裁级别投诉 1 次
- 我和团队全员通宵复盘
- 后续两周所有"小修改"必须走"完整测试 + 灰度"
复盘会上我写了一段话,后来被打印出来贴在工位上:
「不是代码问题,是流程问题。 我们缺的不是更强的程序员,是一条把’代码提交’和’生产部署’彻底打通的自动化流水线——让人为操作降到 0、让 5 台机器同步生效、让回滚一键完成、让配置错误在 CI 阶段就被拦截。」
那次之后,我花了三个月从 0 到 1 搭起了团队的 Jenkins 流水线。后来三年里,团队从 5 个微服务长到 80 个,从 5 台机器扩到 200+ Pod,从手动上线变成全自动化。
这篇文章就是那三个月里踩过的所有坑 + 后续三年里迭代出的所有最佳实践的完整复盘。目标是让你照着做一遍,就能在你团队里落地一套生产可用的 Jenkins + Docker + K8s + 蓝绿/金丝雀的完整 CI/CD 链路。
文章分 8 章 + 1 个收尾,从架构总览到本地可跑通 demo,从 Jenkinsfile 模板到 Argo Rollouts 金丝雀发布,约 16500 字,建议收藏后分章节阅读。
一、Jenkins 体系总览
1.1 Master/Agent 架构:为什么单点 Jenkins 撑不住 200+ 微服务
2021 年初,我第一次把团队 5 个微服务跑上 Jenkins,那时还是单机 Master:Jenkins 装在一台 8C16G 的虚拟机上,所有构建任务都在 Master 本地跑。5 个微服务的时候一切正常。
半年后,团队扩张到 30 个微服务,每天 200+ 次构建。问题开始集中爆发:
- 构建排队:早高峰 9-10 点,平均 15 个 job 在排队,单个 job 平均等 20 分钟——开发者提交代码到拿到构建结果要 1 小时
- 插件 OOM:Pipeline + Docker Pipeline + Kubernetes CLI + Helm + 十几个 Sonar/Nexus/Email 插件一起跑,Master JVM 经常 OOM,每周重启 2-3 次
- 单点故障:Jenkins 升级插件、清理 workspace、重启服务时,整个团队的开发节奏被打断
- 磁盘撑爆:Maven 仓库 + Docker 镜像缓存 + Sonar 报告 + 构建产物,3 个月吃掉 800GB 磁盘
痛定思痛,我把 Jenkins 拆成了 Master/Agent 架构——这是今天所有中大规模团队的标准做法。
Master 只负责调度,Agent 真正干活:
- Master:保存 Job 配置、Pipeline 脚本、构建历史、凭据;不执行任何构建任务;只通过 JNLP 协议把构建任务分发给 Agent
- Agent:真正执行 Shell、Docker、Maven、Kubectl 的工作节点;可以是物理机、虚拟机、Docker 容器、K8s Pod
- 通信协议:JNLP(推荐,HTTP 长连接,跨公网可穿透防火墙)或 SSH(适合内网)
我当时的方案是把 3 类 Agent 拆开,每类干一件事:
| Agent 类型 | 镜像 | 用途 | 资源 |
|---|
| maven agent | maven:3.9-eclipse-temurin-17 | Java 编译、单元测试、生成 jar/war | 4C8G |
| docker agent | docker:24-dind | Docker in Docker,构建镜像、推 Harbor | 4C8G |
| k8s agent | jenkins/inbound-agent:latest | 跑 K8s 部署任务、用完即销毁 | 2C4G |
graph TB
Dev[开发者 push 代码] --> Master[Jenkins Master
调度 + 凭据 + 历史]
Master -->|JNLP| Mvn[maven agent
mvn package]
Master -->|JNLP| Doc[docker agent
docker build+push]
Master -->|JNLP| K8s[k8s agent
kubectl apply]
Mvn --> Harbor[Harbor 制品库]
Doc --> Harbor
K8s --> K8sCluster[(K8s 集群)]
Master -.插件.-> Sonar[SonarQube]
Master -.通知.-> Ding[钉钉 / 企微]
style Master fill:#3c82dc,color:#fff
style Mvn fill:#5aa0e8,color:#fff
style Doc fill:#5aa0e8,color:#fff
style K8s fill:#5aa0e8,color:#fff这种架构的好处是横向扩展——构建量翻倍时,加 3 个 maven agent 就够了,不用动 Master。
1.2 Pipeline as Code:从 UI 点击到 Jenkinsfile 提交
2018 年之前我用的还是 Jenkins「老模式」:在 Jenkins Web UI 上点 New Item → 配置 Git 仓库地址 → 配置 Build Steps → 配置 Post-build Actions → Save。
这种模式有 4 个致命问题:
- 配置散落:30 个微服务 × 30 个 Job = 900 个 Job 配置,散在 Jenkins 数据库里,没人能说清楚哪个 Job 在跑什么命令
- 改不动:想统一加一个 SonarQube 扫描步骤?30 个 Job 全部手动改一遍,改完还要截图存档证明
- 不可审计:谁在什么时间改了 Job 配置?Jenkins 不记录——出问题时只能看 audit log 里的"管理员在 X 时间编辑了 Y Job"
- 不可回滚:配错了?只能手动重配,没有 diff 没有 revert
2018 年 Jenkins 推出 Pipeline as Code(也称 Jenkinsfile),所有这一切问题被一次性解决:
- 配置即代码:Job 配置不再是 UI 点击,而是写在一个叫
Jenkinsfile 的文本文件里,和业务代码一起存在 Git 仓库 - PR review 流程:Jenkinsfile 改了 → 走 PR → 同事 review → 合并,和业务代码一样的代码审查流程
- 版本可追溯:每次改 Jenkinsfile 都有 commit 历史,回滚一行 git revert 就好
- 可复用:Jenkinsfile 里有公共逻辑(build、test、deploy)?抽成 Shared Library,10 个微服务共用同一段代码
我自己从 2018 年开始用 Jenkinsfile,到 2020 年把所有 freestyle job 全部迁完。这一步是流水线从"能用"到"好用"的关键分水岭。
1.3 声明式 vs 脚本式:怎么选?
Jenkinsfile 写起来有两种风格:
- 声明式(Declarative):语法限制严格,结构清晰,像填表
- 脚本式(Scripted):完整 Groovy 语法,自由度高,像写程序
90% 的场景用声明式就够了。我团队 80 个微服务,75 个用声明式,只有 5 个复杂动态场景(按需选环境、动态生成 Job)用脚本式。
6 维度对比表:
| 维度 | 声明式 (Declarative) | 脚本式 (Scripted) |
|---|
| 语法限制 | 严格,结构固定 | 自由,完整 Groovy |
| 学习曲线 | 低(半小时上手) | 高(需会 Groovy) |
| 复用性 | 用 library 调 Shared Library | 用 Groovy Class 自己封装 |
| IDE 支持 | 主流 IDE 都有插件 | 弱,需要懂 Groovy |
| 错误提示 | 友好(语法错误立即提示) | 一般(运行时才知道) |
| 适用规模 | 中小团队 / 80% 场景 | 复杂动态 / 大型平台 |
| 推荐度 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
声明式的骨架长这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| pipeline {
agent { label 'maven' } // 在哪个 agent 上跑
options { // 全局选项
timeout(time: 30, unit: 'MINUTES')
disableConcurrentBuilds()
}
parameters { // 手动参数
string(name: 'BRANCH', defaultValue: 'main')
}
stages { // 阶段
stage('Build') { steps { ... } }
stage('Test') { steps { ... } }
}
post { // 后置动作
always { echo 'cleanup' }
failure { mail to: 'team@xxx.com' }
}
}
|
pipeline / agent / stages / steps / post 是声明式的5 大顶层指令,写错一个 Jenkins 立刻提示。脚本式你写 node { stage { ... } },语法错误要到运行时才报。
stateDiagram-v2
[*] --> Pipeline
Pipeline --> Agent: 分配执行节点
Agent --> Stages: 串行执行各 stage
Stages --> Stage1
Stage1 --> Stage2
Stage2 --> Stage3
Stage3 --> Stages
Stages --> Post: 全部完成
Post --> Always: 始终执行
Post --> Success: 成功
Post --> Failure: 失败
Post --> [*]1.4 关键插件清单:20 个就够,多了就是债务
Jenkins 插件生态庞大,官方仓库 1800+ 插件。我见过最夸张的团队装 200+ 插件——Jenkins 启动 5 分钟,OOM 每月 3 次。
经验法则:20 个插件以内,撑起 80 个微服务。
我团队的生产可用最小集(按重要性排序):
| 类别 | 插件 | 必装 | 用途 |
|---|
| 核心 | Pipeline | ✅ | Jenkinsfile 支持 |
| 核心 | Git | ✅ | 拉代码 |
| 核心 | Credentials Binding | ✅ | 凭据注入 |
| 核心 | Workspace Cleanup | ✅ | 清理工作区 |
| 核心 | Timestamper | ✅ | 日志加时间戳 |
| SCM | GitHub / GitLab | ✅ | Webhook + 状态回调 |
| SCM | Generic Webhook Trigger | ✅ | 通用 Webhook 触发器 |
| 构建 | Docker Pipeline | ✅ | Jenkinsfile 里写 docker.build |
| 构建 | Kubernetes CLI | ✅ | kubectl apply |
| 构建 | Helm | ✅ | Helm Chart 部署 |
| 质量 | SonarQube Scanner | ✅ | 代码扫描 |
| 质量 | JUnit | ⭕ | 测试报告聚合 |
| 制品 | Nexus Artifact Uploader | ✅ | 推 Maven 包到 Nexus |
| 制品 | Warnings NG | ⭕ | 代码告警聚合 |
| 通知 | Email-ext | ✅ | 邮件通知 |
| 通知 | DingTalk | ✅ | 钉钉通知 |
| UI | Blue Ocean | ⭕ | 可视化流水线(可选) |
| 运维 | Configuration as Code | ✅ | Jenkins 配置 yaml 化 |
| 运维 | Matrix Authorization | ✅ | 权限矩阵 |
| 运维 | Role-based Authorization | ✅ | 角色权限 |
注意:
- 装过的插件必须用 JCasC(Configuration as Code)写入 yaml,否则重启就丢
- 插件升级前先在测试 Jenkins 验证——很多插件升级有 breaking change(比如 Docker Pipeline 1.x → 2.x 改了 API)
- 半年审一次插件清单,用不到的立即 disable,不常用的 uninstall
第一章总结:Jenkins Master/Agent 架构是规模化的基础,Pipeline as Code 是工程化的关键,声明式 Pipeline 是 90% 场景的最优解,20 个插件覆盖 80% 用法。
下一章我们把这些原则落到本地——用 docker-compose 起一套完整可跑通的 Jenkins + k3d K8s 演示环境。
二、高可用 Jenkins 集群搭建
2.1 本地环境拓扑:Jenkins + k3d 一键起
「看 100 篇文章不如跑 1 遍」——这是我在团队内部推广 Jenkins 时的口头禅。
文章里写的 docker-compose.yml、k3d 启动脚本、Jenkinsfile 模板、Helm Chart 都可以直接复制使用。读者需要做的就是跟着敲命令,在自己电脑上把整套环境跑起来。
本节给出完整可跑通的本地演示环境拓扑:
graph LR
subgraph Docker[Docker Host 宿主机]
Jen[Jenkins Master
:8080]
MA[maven agent
:动态]
DA[docker agent
DIND]
end
subgraph K3d[k3d 本地 K8s 集群]
KA[k8s agent
:动态]
Argo[Argo Rollouts]
Prom[Prometheus]
App[业务 Pods]
end
Dev[开发者] -->|push| Git[(GitHub/GitLab)]
Git -->|webhook| Jen
Jen -->|JNLP| MA
Jen -->|JNLP| DA
Jen -->|JNLP| KA
Jen -->|kubectl| Argo
MA -->|mvn build| Jen
DA -->|docker push| Harbor[(Harbor
localhost:30002)]
KA -->|kubectl apply| App
Argo --> Prom
Prom -->|指标查询| Argo
style Jen fill:#3c82dc,color:#fff
style K3d fill:#1a3a6a,color:#fff
style Docker fill:#0a1a3a,color:#fff架构关键点:
- Jenkins Master 跑在宿主机 Docker 上(不是 k3d 内)——避免 K8s 故障时 Jenkins 也挂
- 3 类 Agent 跑在 k3d 之外(也可以跑在 k3d 内作为 Pod,本章用更简单的「固定容器」方案)
- k3d 集群跑在宿主机 Docker 里,共享 docker.sock,Jenkins 构建的镜像能被 K8s 看到
- Harbor 跑在 k3d 内(用 NodePort 30002 暴露)
资源占用:
- 最低:8C16G,能跑但卡
- 推荐:16C32G,这是流畅跑 Jenkins + k3d 的甜点
- 极致:32C64G,可同时跑 SonarQube + Nexus + 多个项目
2.2 docker-compose.yml:Jenkins Master + 持久化
完整 docker-compose.yml(生产可用 + 本地 demo 两相宜):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| # docker-compose.yml
# 启动:docker compose up -d
# 查看日志:docker compose logs -f jenkins
version: "3.8"
services:
jenkins:
image: jenkins/jenkins:2.452.3-lts-jdk17
container_name: jenkins-master
restart: unless-stopped
user: root # 容器内用 root,方便调 docker
ports:
- "8080:8080" # Web UI
- "50000:50000" # Agent JNLP
volumes:
- jenkins_data:/var/jenkins_home # 配置持久化
- /var/run/docker.sock:/var/run/docker.sock # 共享宿主机 docker daemon
- /usr/bin/docker:/usr/bin/docker:ro # docker CLI(容器内能用 docker 命令)
environment:
- JAVA_OPTS=-Xms2g -Xmx4g -Djava.awt.headless=true
- JENKINS_OPTS=--httpPort=8080
networks:
- jenkins-net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/login"]
interval: 30s
timeout: 10s
retries: 10
# 固定 maven agent(也可改成动态 K8s pod agent)
maven-agent:
image: jenkins/inbound-agent:3192.v713e3def_8641-1
container_name: jenkins-maven-agent
depends_on:
jenkins:
condition: service_healthy
environment:
- JENKINS_URL=http://jenkins:8080
- JENKINS_SECRET=<从 Master UI 拷过来>
- JENKINS_AGENT_NAME=maven-agent-1
networks:
- jenkins-net
docker-agent:
image: docker:24-dind
container_name: jenkins-docker-agent
privileged: true # DIND 必须
environment:
- DOCKER_TLS_CERTDIR=
volumes:
- docker_data:/var/lib/docker
networks:
- jenkins-net
volumes:
jenkins_data:
docker_data:
networks:
jenkins-net:
driver: bridge
|
关键配置解析:
/var/run/docker.sock 挂载:让 Master 容器能直接调用宿主机 docker daemon,好处是构建的镜像在宿主机 docker 里可见,k3d 也能用user: root:容器内 Jenkins 进程用 root 跑,方便调 docker(生产环境用非 root 需要更多配置)JAVA_OPTS=-Xms2g -Xmx4g:JVM 堆 4G,够 100 个并发 job,超过这个量级需要排查 OOM- JNLP 端口 50000:Agent 通过这个端口和 Master 建长连接,必须开且不能被防火墙挡
首次启动:
1
2
3
4
| docker compose up -d
docker compose logs -f jenkins | grep -A 1 "initial"
# 看到一行:Please use the following password to proceed to installation:
# 类似:2a3b4c5d6e7f8g9h0i1j2k3l4m5n6o7p
|
把这个密码复制到 http://localhost:8080,第一次进入会走 Setup Wizard:
- 安装推荐插件(Install suggested plugins)——包含 Git、Pipeline、Credentials Binding 等
- 创建第一个管理员用户(admin / )
- 保存并完成
进入后必须做的 3 件事:
- Configure Global Security → 启用 Agent → Master 访问控制(JNLP 协议需要)
- Manage Plugins → 搜索并安装
Docker Pipeline / Kubernetes CLI / Helm / DingTalk / SonarQube Scanner - Manage Nodes and Clouds → New Node → 复制 JNLP 秘钥,填到上面
maven-agent 的 JENKINS_SECRET
2.3 k3d 一键起本地 K8s
k3d 是 k3s(轻量 K8s)的 docker 包装器——5 秒拉起一个 K8s 集群,内存只占 300MB。比 kind(K8s in Docker)更轻,比 minikube 启动快 10 倍。
安装 k3d:
1
2
3
4
5
6
7
8
| # macOS
brew install k3d k3s
# Linux
curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash
# Windows(WSL2)
wsl -e bash -c "curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash"
|
启动集群:
1
2
3
4
5
6
7
8
| k3d cluster create jenkins-demo \
--image rancher/k3s:v1.29.4-k3s1 \
--port "80:80@loadbalancer" \
--port "443:443@loadbalancer" \
--port "30000-30100:30000-30100@loadbalancer" \
--agents 3 \
--volume "/tmp/k3d-shared:/tmp/shared@all" \
--k3s-arg "--disable=traefik@server:0"
|
参数解析:
--image rancher/k3s:v1.29.4-k3s1:K8s 版本固定(避免每次启动版本漂移)--port "80:80@loadbalancer":把容器内 80 端口映射到宿主机 80--port "30000-30100:30000-30100":NodePort 范围,本地访问业务服务用--agents 3:3 个 worker node(默认 0,建议至少 2 个才能演示多 Pod 部署)--volume:共享目录,让 K8s 内的 Pod 能看到宿主机文件(重要!Jenkins 构建的镜像在这里 push 之后 K8s 才能 pull)--k3s-arg "--disable=traefik@server:0":禁用默认的 Traefik Ingress(我们用 Nginx Ingress 自己装)
验证集群:
1
2
3
4
5
| kubectl get nodes
# 预期看到 4 个 node(1 server + 3 agent),全部 Ready
kubectl get pods -A
# kube-system 下有 coredns、metrics-server 等基础组件
|
Windows WSL2 用户注意:如果你用 WSL2 + Docker Desktop,/var/run/docker.sock 在 WSL2 内的路径是 //var/run/docker.sock(注意双斜杠),docker-compose.yml 里的挂载可能要改成:
1
2
| volumes:
- //var/run/docker.sock:/var/run/docker.sock
|
2.4 三类 Agent:maven / docker / k8s
Agent 设计的第一原则:「一类活一台 Agent」——别让一个 Agent 干所有事,原因有三:
- 资源隔离:Java 构建要 4C8G,Docker 构建也要 4C8G,两个一起跑会抢内存 OOM
- 环境隔离:Maven Agent 装 JDK + Maven,不要装 Docker CLI(污染 PATH);Docker Agent 装 Docker CLI + dind,不要装 JDK
- 故障隔离:某类 Agent 出问题(磁盘满、插件崩),不影响其他类型
我的标准方案(80 个微服务团队实战):
| Agent 类型 | 镜像 | 资源 | 用途 | 标签 |
|---|
| maven agent | maven:3.9-eclipse-temurin-17 | 4C8G | 编译、单测、Sonar 扫描、生成 jar | maven、linux |
| docker agent | docker:24-dind | 4C8G | docker build、docker push 镜像 | docker、dind |
| k8s agent | jenkins/inbound-agent:latest-jdk17 | 2C4G | kubectl、helm、argocd 操作 | k8s、linux |
Jenkinsfile 里怎么选:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| pipeline {
agent none // 顶层不指定 agent
stages {
stage('Build') {
agent { label 'maven' } // 拉 maven agent
steps { sh 'mvn package' }
}
stage('Docker Build') {
agent { label 'docker' } // 拉 docker agent
steps { sh 'docker build -t app:${TAG} .' }
}
stage('Deploy') {
agent { label 'k8s' } // 拉 k8s agent
steps { sh 'kubectl apply -f deployment.yaml' }
}
}
}
|
进阶:用 K8s Pod 作为 Agent(动态伸缩)
如果构建量波动大(白天多、夜里少),用固定 Agent 不划算。Jenkins 支持「K8s Pod Agent」——每次构建拉一个新 Pod,构建完自动销毁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| agent {
kubernetes {
label 'maven-pod'
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: maven
image: maven:3.9-eclipse-temurin-17
command: ['cat']
tty: true
resources:
requests: { cpu: '1', memory: '2Gi' }
limits: { cpu: '2', memory: '4Gi' }
'''
}
}
|
这种方案需要先装 Kubernetes 插件,并在 Jenkins 全局配置里配 K8s Cloud:
- Jenkins URL:
https://kubernetes.default(Jenkins 跑在 K8s 内的方式) - 或:
https://<k3d-api-server>:6443(Jenkins 跑在 K8s 外的 k3d 集群方式) - Credentials:ServiceAccount Token 或 Kubeconfig
两种方案对比:
| 维度 | 固定 Agent | K8s Pod Agent |
|---|
| 启动速度 | 0 秒(常驻) | 20-30 秒(拉镜像) |
| 资源利用率 | 24/7 占资源 | 用完即销毁 |
| 镜像缓存 | 常驻,命中率高 | 每次重新拉(除非配 PV) |
| 适用规模 | 小到中(<50 并发) | 中到大(100+ 并发) |
| 复杂度 | 低 | 中(要配 PVC、网络) |
我的建议:30 个微服务以内用固定 Agent,简单稳定;超过 50 个用 K8s Pod Agent,节省资源。
2.5 凭据管理 + SSH 互通
Jenkins 凭据是最容易出安全问题的环节——90% 的「Jenkins 被黑」事故根因都是凭据管理不当。
4 类核心凭据:
| 凭据类型 | 用途 | 存哪里 |
|---|
| Username/Password | Docker Registry、SonarQube、邮件账号 | Jenkins 内置凭据库 |
| SSH Key | Git 仓库、目标机器 | Jenkins 内置凭据库 |
| Secret Text | API Token、Webhook URL | Jenkins 内置凭据库 |
| Kubeconfig | K8s 集群访问 | Jenkins 内置凭据库 |
Jenkinsfile 里的标准用法(不要在 Jenkinsfile 写明文密码!):
1
2
3
4
5
6
7
8
9
10
11
| // ❌ 错误写法
sh 'docker login -u admin -p Harbor12345 harbor.xxx.com'
// ✅ 正确写法
withCredentials([usernamePassword(
credentialsId: 'harbor-creds',
usernameVariable: 'HARBOR_USER',
passwordVariable: 'HARBOR_PASS'
)]) {
sh "docker login -u ${HARBOR_USER} -p ${HARBOR_PASS} harbor.xxx.com"
}
|
多环境凭据切换(生产 / 预发 / 开发):
1
2
3
4
5
6
7
8
9
10
| def credMap = [
dev: 'k8s-dev-creds',
staging: 'k8s-stg-creds',
prod: 'k8s-prod-creds'
]
def credId = credMap[params.ENV]
withCredentials([file(credentialsId: credId, variable: 'KUBECONFIG')]) {
sh "kubectl --kubeconfig=${KUBECONFIG} apply -f deployment.yaml"
}
|
凭据管理 5 大铁律:
- 不在 Jenkinsfile 写明文——这是 1.0 级别的红线
- 凭据 ID 命名规范——
<服务>-<环境>-<类型>,比如 harbor-prod-user - 凭据权限最小化——Matrix Authorization 插件配「谁能用哪些凭据」
- 定期轮换——90 天换一次 Harbor/数据库密码
- 审计日志开启——谁在什么时间用了什么凭据,全部记录
第二章总结:Jenkins Master 跑在 docker-compose 里、Agent 按 maven/docker/k8s 三类拆分、k3d 一键起本地 K8s 集群、凭据用 withCredentials 注入——这套组合拳能让你在 1 小时内本地跑通完整演示环境。
下一章我们写一份生产可用的 Jenkinsfile——6 个 stage、150+ 行,覆盖 Build/Test/Sonar/Package/Docker/Deploy 全流程。
2.6 完整启动脚本:一键起所有服务
把上面的 docker-compose 和 k3d 命令封装成一个 shell 脚本,方便反复重置本地环境:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| #!/bin/bash
# scripts/start-demo-env.sh
# 一键起 Jenkins + k3d + 必要工具
set -e
echo "==> 1. 启动 Jenkins Master + Agents"
docker compose up -d
echo "==> 2. 等待 Jenkins 就绪"
for i in {1..30}; do
if curl -sf http://localhost:8080/login >/dev/null 2>&1; then
echo " Jenkins ready"
break
fi
echo " 等待 Jenkins 启动... ${i}/30"
sleep 5
done
echo "==> 3. 启动 k3d 集群"
k3d cluster create jenkins-demo \
--image rancher/k3s:v1.29.4-k3s1 \
--port "80:80@loadbalancer" \
--port "443:443@loadbalancer" \
--port "30000-30100:30000-30100@loadbalancer" \
--agents 3 \
--volume "/tmp/k3d-shared:/tmp/shared@all" \
--k3s-arg "--disable=traefik@server:0" \
--wait
echo "==> 4. 安装 Argo Rollouts"
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/download/v1.7.0/install.yaml
echo "==> 5. 安装 Prometheus(轻量版)"
kubectl apply -f https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/v0.72.0/bundle.yaml
echo "==> 6. 安装 Harbor(轻量)"
helm repo add harbor https://helm.goharbor.io
helm install harbor harbor/harbor \
--namespace harbor --create-namespace \
--set expose.type=nodePort \
--set expose.nodePort.ports.http.nodePort=30002 \
--set persistence.enabled=false \
--set core.persistence.enabled=false \
--set database.persistence.enabled=false \
--set redis.persistence.enabled=false \
--set jobservice.persistence.enabled=false \
--set registry.persistence.enabled=false \
--set trivy.persistence.enabled=false
echo "==> 7. 验证"
echo " Jenkins: http://localhost:8080"
echo " k3d kubeconfig: k3d kubeconfig write jenkins-demo"
echo " Harbor: http://localhost:30002 (admin/Harbor12345)"
echo ""
echo "全部就绪!"
|
跑完这个脚本,你的本地就有:
- Jenkins Master(http://localhost:8080)
- k3d K8s 集群(4 节点)
- Argo Rollouts Controller
- Prometheus Operator
- Harbor 镜像仓库
接下来就可以在 Jenkins UI 上建 Job、配 Webhook、写 Jenkinsfile 跑流水线了。
2.7 常见坑位与排查清单
3 年里我们团队在 Jenkins + K8s 集成上踩过的高频 10 个坑,按出现频率排序:
| # | 坑位 | 现象 | 解决方案 |
|---|
| 1 | docker.sock 权限拒绝 | permission denied while trying to connect to the Docker daemon socket | Master 用 user: root 跑,或在宿主机 chmod 666 /var/run/docker.sock |
| 2 | k3d 集群内存爆 | 4G 内存跑 Jenkins + k3d + Harbor,宿主机卡死 | 宿主机至少 16G;Harbor 用 --set persistence.enabled=false |
| 3 | Agent 离线 | JNLP 连不上,Agent 显示 offline | 检查 Master 50000 端口是否开放;JNLP 秘钥是否过期 |
| 4 | 镜像推送后 K8s 拉不到 | ImagePullBackOff | k3d 集群和 docker 共享 /tmp/k3d-shared 目录;或用 k3d image import 导入 |
| 5 | Maven 依赖下载慢 | 第一次构建 5 分钟全在 download | 挂载 .m2 目录到 host:volumes: ['~/.m2:/root/.m2'] |
| 6 | Pipeline 步骤超时 | 30 分钟还没跑完 | options { timeout(time: 60, unit: 'MINUTES') } 加大 |
| 7 | Jenkins OOM | java.lang.OutOfMemoryError | 调大 JAVA_OPTS=-Xmx4g;或加 Master 节点 |
| 8 | 插件升级后 build 失败 | Docker Pipeline 1.x → 2.x 改了 API | 锁插件版本;先在测试 Jenkins 验证再升级 |
| 9 | Webhook 不触发 | push 代码 Jenkins 没反应 | 检查 GitHub Webhook 状态码;Jenkins Manage Webhooks 日志 |
| 10 | 并发构建冲突 | 同一 Job 跑两次互相干扰 | options { disableConcurrentBuilds() } 串行 |
调试工具:
- Jenkins 构建日志:直接看 console output
- Agent 状态:Manage Nodes and Clouds → 点 Agent 名 → Log
- K8s 状态:
kubectl get pods -A、kubectl describe pod <name> - 镜像拉取:
docker exec -it k3d-jenkins-demo-agent-0 crictl images - 网络连通:
docker exec -it jenkins-master ping k3d-jenkins-demo-server-0
2.8 Jenkins 配置即代码:JCasC 拯救「文档缺失」
我刚接手团队 Jenkins 时最大的痛苦是——没人知道生产 Jenkins 的 30 个 Job 是怎么配的。某天 Jenkins 挂了重装,所有 Job 全部丢光,只能从 Jenkinsfile 重新跑。
2020 年我引入 JCasC(Configuration as Code) 插件,把所有「在 Jenkins UI 上配的东西」全部 yaml 化,和 Jenkinsfile 一样存进 Git:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| # casc/jenkins.yaml
jenkins:
systemMessage: "团队 CI/CD 中心 - powered by Jenkins 2.452"
numExecutors: 0 # Master 不跑任务
mode: EXCLUSIVE # 一个时间只跑一个 Job(防 OOM)
securityRealm:
local:
users:
- id: "admin"
name: "Admin"
password: "${ADMIN_PASSWORD}" # 从环境变量读
authorizationStrategy:
loggedInUsersCanDoAnything:
allowAnonymousRead: false
remotingSecurity:
enabled: true
credentials:
system:
domainCredentials:
- credentials:
- usernamePassword:
scope: GLOBAL
id: "harbor-creds"
username: "admin"
password: "${HARBOR_PASSWORD}"
description: "Harbor 镜像仓库"
- file:
scope: GLOBAL
id: "k8s-prod-kubeconfig"
fileName: "kubeconfig"
secretBytes:
"${K8S_PROD_KUBECONFIG_BASE64}"
unclassified:
location:
url: "https://jenkins.xxx.com/"
adminAddress: "devops@xxx.com"
tool:
jdk:
installations:
- name: "jdk17"
properties:
- installSource:
installers:
- adoptOpenJdk:
id: "jdk-17.0.10+7"
|
启动时加载:
1
2
3
| docker run -e CASC_JENKINS_CONFIG=/var/jenkins_home/casc/jenkins.yaml \
-v $PWD/casc:/var/jenkins_home/casc:ro \
jenkins/jenkins:2.452.3-lts-jdk17
|
JCasC 的 3 大价值:
- 环境可重建:Jenkins 挂了重装,30 秒自动恢复所有配置(凭据除外,敏感信息走环境变量 / Vault)
- 变更可审计:所有配置改动走 PR review,谁在什么时间改了 systemMessage 一目了然
- 多环境一致:dev / staging / prod Jenkins 用同一份 yaml + 不同环境变量,不会出现「测试配了生产没配」的坑
2.9 备份策略:每日 3-2-1 法则
Jenkins 数据分两类,备份策略完全不同:
配置数据(小,KB-MB 级):
- JCasC yaml(已在 Git 里,天然多副本)
- Job 配置(如果用 Jenkinsfile 形式,已在 Git 里;如果是 freestyle,必须定期导出)
- 凭据 ID 清单(凭据值本身不能导出,只能记「有哪些凭据」)
- 全局安全配置、插件清单
运行时数据(大,GB 级):
jenkins_data 卷(workspace、构建历史、归档制品、用户内容)- 数据库(如果用外部 DB)
我的 3-2-1 备份方案:
| 数据 | 备份方式 | 频率 | 保留期 |
|---|
| JCasC yaml | Git(主仓 + GitLab mirror) | 实时 | 永久 |
| Jenkinsfile | Git(业务仓) | 实时 | 永久 |
| freestyle Job 配置 | Job History 插件 + 定时导出 yaml | 每日 | 90 天 |
| jenkins_data 卷 | restic 加密备份到 S3 / 阿里云 OSS | 每日 03:00 | 30 天 |
| 关键构建产物 | 推 Nexus / Harbor | 实时 | 按 artifact 策略 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # 备份脚本(简化版)
#!/bin/bash
BACKUP_DIR=/backup/jenkins/$(date +%Y%m%d)
mkdir -p $BACKUP_DIR
# 1. 备份 JCasC(已在 Git 里,这里只备份运行时配置)
docker exec jenkins-master bash -c \
"java -jar /var/jenkins_home/war/WEB-INF/jenkins-cli.jar \
-s http://localhost:8080 get-job my-job" > $BACKUP_DIR/my-job.xml
# 2. 备份插件清单
docker exec jenkins-master cat /var/jenkins_home/plugins.txt > $BACKUP_DIR/plugins.txt
# 3. 备份 jenkins_data(restic 增量)
restic backup /var/lib/docker/volumes/jenkins_data
# 4. 上传 S3
aws s3 sync $BACKUP_DIR s3://my-backup-bucket/jenkins/$BACKUP_DIR
|
灾备演练:每季度做一次「Jenkins 全毁 → 1 小时内恢复」的演练,不演练的备份等于没备份。
三、Jenkinsfile 完整模板
3.1 项目结构假设 + Pipeline 总体设计
为了一篇文章讲透 Jenkinsfile,我假设你的项目是这种结构(绝大多数 Java 微服务团队都是这个形态):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| my-microservice/ # Spring Boot 3.x 多模块 Maven 项目
├── pom.xml # 父 POM
├── common-lib/ # 公共库
│ ├── pom.xml
│ └── src/main/java/...
├── order-service/ # 订单服务
│ ├── pom.xml
│ ├── Dockerfile
│ └── src/
├── user-service/ # 用户服务
│ ├── pom.xml
│ ├── Dockerfile
│ └── src/
├── Jenkinsfile # 我们的主角
└── sonar-project.properties # SonarQube 配置
|
Pipeline 6 阶段总体设计:
graph LR
A[Checkout
拉代码] --> B[Build
Maven 编译]
B --> C[Test
单测+集成测试]
C --> D[Code Scan
SonarQube]
D --> E[Package
生成 jar]
E --> F[Docker Build
构建镜像]
F --> G[Push to Harbor
推镜像]
G --> H[Deploy
kubectl apply]
H --> I[Verify
健康检查]
I --> J[Notify
钉钉通知]
style A fill:#3c82dc,color:#fff
style D fill:#ff9a3c,color:#fff
style F fill:#5aa0e8,color:#fff
style H fill:#3c82dc,color:#fff关键设计决策:
agent none + 每个 stage 单独指定 agent——Build/Test 在 maven agent,Docker 在 docker agent,Deploy 在 k8s agent- 每个 stage 必须可独立触发——
when { expression { ... } } 条件跳过不需要的 stage post 4 大场景——always 清理、success 通知、failure 告警 + 日志归档、aborted 通知- 重试机制——网络类操作(git push、docker push)
retry(3) 包一层 - 超时——整体 Pipeline 30 分钟,单 stage 10 分钟,超时自动 fail
3.2 完整 Jenkinsfile 全文(150+ 行可直接复制)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
| // Jenkinsfile - 适用于 Spring Boot 多模块微服务
// 用法:放在项目根目录,Jenkins 创建 Pipeline Job 时选 "Pipeline script from SCM"
pipeline {
agent none // 顶层不指定,由各 stage 决定
options {
// 整体超时:30 分钟
timeout(time: 30, unit: 'MINUTES')
// 禁止并发构建(同一 Job 同一时间只能跑 1 个)
disableConcurrentBuilds()
// 构建历史保留:30 天最多 50 次
buildDiscarder(logRotator(numToKeepStr: '50', daysToKeepStr: '30'))
// 日志加时间戳
timestamps()
// ansi 颜色
ansiColor('xterm')
}
parameters {
// 手动参数
choice(name: 'ENV', choices: ['dev', 'staging', 'prod'],
description: '部署环境')
string(name: 'BRANCH', defaultValue: 'main',
description: '代码分支')
string(name: 'IMAGE_TAG', defaultValue: '',
description: '镜像 tag(留空用 commit SHA)')
booleanParam(name: 'SKIP_TEST', defaultValue: false,
description: '跳过测试')
booleanParam(name: 'FORCE_DEPLOY', defaultValue: false,
description: '强制部署(跳过健康检查)')
}
environment {
// 全局环境变量
APP_NAME = 'order-service'
HARBOR_PROJECT = 'myteam'
K8S_NAMESPACE = "${params.ENV}"
GIT_COMMIT = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
IMAGE_TAG = "${params.IMAGE_TAG?.trim() ? params.IMAGE_TAG : env.GIT_COMMIT}"
IMAGE_NAME = "harbor.xxx.com/${HARBOR_PROJECT}/${APP_NAME}"
DOCKER_IMAGE = "${IMAGE_NAME}:${IMAGE_TAG}"
}
stages {
// ===== 阶段 1:Checkout =====
stage('Checkout') {
agent { label 'maven' }
steps {
checkout scm
sh 'git status'
sh 'git log -1 --oneline'
}
}
// ===== 阶段 2:Build =====
stage('Build') {
agent { label 'maven' }
steps {
sh 'mvn -B -V -T 4 clean compile'
}
post {
failure {
echo 'Build 失败,请检查代码编译错误'
}
}
}
// ===== 阶段 3:Test =====
stage('Test') {
agent { label 'maven' }
when {
expression { return !params.SKIP_TEST }
}
steps {
sh 'mvn -B test'
// 归档测试报告
junit '**/target/surefire-reports/*.xml'
// 代码覆盖率
jacoco(execPattern: '**/target/jacoco.exec',
minimumLineCoverage: '0.60')
}
}
// ===== 阶段 4:Code Scan =====
stage('Code Scan') {
agent { label 'maven' }
steps {
withSonarQubeEnv('sonarqube-server') {
sh 'mvn sonar:sonar -Dsonar.projectKey=${APP_NAME}'
}
// 等待 Quality Gate 结果(10 分钟超时)
timeout(time: 10, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
// ===== 阶段 5:Package =====
stage('Package') {
agent { label 'maven' }
steps {
sh 'mvn -B -DskipTests clean package'
// 归档 jar
archiveArtifacts artifacts: '**/target/*.jar',
fingerprint: true,
allowEmptyArchive: false
}
}
// ===== 阶段 6:Docker Build =====
stage('Docker Build') {
agent { label 'docker' }
steps {
sh """
docker build \
--build-arg JAR_FILE=${APP_NAME}/target/*.jar \
-t ${DOCKER_IMAGE} \
-t ${IMAGE_NAME}:latest \
.
"""
}
}
// ===== 阶段 7:Push to Harbor =====
stage('Push') {
agent { label 'docker' }
steps {
withCredentials([usernamePassword(
credentialsId: 'harbor-creds',
usernameVariable: 'HARBOR_USER',
passwordVariable: 'HARBOR_PASS'
)]) {
sh """
docker login -u ${HARBOR_USER} -p ${HARBOR_PASS} harbor.xxx.com
retry 3 docker push ${DOCKER_IMAGE}
docker push ${IMAGE_NAME}:latest
"""
}
}
}
// ===== 阶段 8:Deploy =====
stage('Deploy') {
agent { label 'k8s' }
when {
expression { return params.ENV != '' }
}
steps {
script {
def credMap = [
dev: 'k8s-dev-creds',
staging: 'k8s-stg-creds',
prod: 'k8s-prod-creds'
]
withCredentials([file(
credentialsId: credMap[params.ENV],
variable: 'KUBECONFIG'
)]) {
sh """
kubectl --kubeconfig=${KUBECONFIG} \
set image deployment/${APP_NAME} \
${APP_NAME}=${DOCKER_IMAGE} \
-n ${K8S_NAMESPACE}
kubectl --kubeconfig=${KUBECONFIG} \
rollout status deployment/${APP_NAME} \
-n ${K8S_NAMESPACE} \
--timeout=5m
"""
}
}
}
}
}
// ===== 构建后操作 =====
post {
// 始终执行
always {
echo "构建结束:${currentBuild.currentResult}"
// 清理 workspace
cleanWs()
}
// 成功
success {
script {
def msg = """
✅ 构建成功
Job: ${env.JOB_NAME} #${env.BUILD_NUMBER}
分支: ${params.BRANCH}
环境: ${params.ENV}
镜像: ${DOCKER_IMAGE}
提交: ${env.GIT_COMMIT}
链接: ${env.BUILD_URL}
""".stripIndent()
dingtalk(
type: 'MARKDOWN',
title: '✅ Jenkins 构建成功',
text: msg,
at: []
)
}
}
// 失败
failure {
script {
// 归档失败日志
archiveArtifacts artifacts: '**/build.log',
allowEmptyArchive: true
// 钉钉告警
dingtalk(
type: 'MARKDOWN',
title: '❌ Jenkins 构建失败',
text: """
❌ 构建失败
Job: ${env.JOB_NAME} #${env.BUILD_NUMBER}
分支: ${params.BRANCH}
提交: ${env.GIT_COMMIT}
链接: ${env.BUILD_URL}
失败阶段: ${env.STAGE_NAME ?: '未知'}
""".stripIndent()
)
}
}
// 终止
aborted {
echo '构建被人工终止'
}
}
}
|
这段 Jenkinsfile 包含 8 个 stage、150+ 行,是生产可用的最小完整版本。复制过去改 4 个变量就能用:
APP_NAME:你的服务名HARBOR_PROJECT:Harbor 项目名harbor.xxx.com:Harbor 地址sonarqube-server:SonarQube 配置名(在 Jenkins 全局配置里配)
3.3 Checkout 阶段详解
checkout scm 是声明式 Pipeline 里最简洁也最强大的一行——它会自动用 Job 配置的 SCM 拉代码。
多仓库场景(order-service 依赖 common-lib):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| stage('Checkout') {
agent { label 'maven' }
steps {
checkout([
$class: 'GitSCM',
branches: [[name: params.BRANCH]],
userRemoteConfigs: [[
url: 'git@github.com:myteam/order-service.git',
credentialsId: 'github-ssh-key'
]],
extensions: [
// 拉 common-lib 子模块
[$class: 'SubmoduleOption',
recursiveSubmodules: true,
parentCredentials: true],
// 浅克隆(只拉最近 50 个 commit,加速)
[$class: 'CloneOption',
depth: 50, shallow: true, noTags: false]
]
])
}
}
|
关键参数:
credentialsId:用 SSH key 凭据,别用 https 用户名密码(每次 build 都会触发 2FA)submodule:如果项目用 Git submodule 拉公共库,必须 recursiveSubmodules: trueshallow: true:浅克隆,从 5 分钟缩到 30 秒LFS:大文件用 Git LFS 时要额外配
3.4 Build + Test 阶段详解
Maven 加速 5 个技巧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 1. 批模式:-B 减少 Jenkins 日志噪音
sh 'mvn -B clean package'
// 2. 版本校验:-V 强制用父 POM 版本(防 dep version 漂移)
sh 'mvn -B -V clean package'
// 3. 并行构建:-T 4(4 线程并行)
sh 'mvn -B -T 4 clean package'
// 4. 跳过测试:-DskipTests
sh 'mvn -B -DskipTests package'
// 5. 增量编译:am(also-make)只编译依赖模块
sh 'mvn -B -pl order-service -am clean package'
|
测试报告:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| stage('Test') {
agent { label 'maven' }
steps {
sh 'mvn -B test'
// JUnit 报告聚合
junit allowEmptyResults: true,
testResults: '**/target/surefire-reports/*.xml'
// 覆盖率门槛
jacoco(execPattern: '**/target/jacoco.exec',
minimumLineCoverage: '0.60',
minimumBranchCoverage: '0.50')
}
}
|
minimumLineCoverage: '0.60' 是个强约束——单测覆盖率低于 60% 直接 fail。我团队的策略是 60% 起步、季度提升 5%,3 年从 60% 干到 78%。
Maven 本地仓库缓存:
如果 Agent 是容器,每次构建都重新下载依赖。解决方法:
1
2
3
4
5
| # docker-compose.yml 给 maven agent 挂 .m2
services:
maven-agent:
volumes:
- ~/.m2:/root/.m2 # 共享宿主机 .m2 目录
|
这样 100 个微服务用同一份 .m2,首次 5 分钟,后续 30 秒。
3.5 SonarQube + Docker 阶段详解
SonarQube 集成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| stage('Code Scan') {
agent { label 'maven' }
steps {
// 1. 注入 SonarQube 配置(从 Jenkins 全局配置里拿 token)
withSonarQubeEnv('sonarqube-server') {
sh 'mvn sonar:sonar -Dsonar.projectKey=order-service'
}
// 2. 阻塞等结果(10 分钟超时)
timeout(time: 10, unit: 'MINUTES') {
// 3. Quality Gate 不通过 abortPipeline: true
waitForQualityGate abortPipeline: true
}
}
}
|
sonar-project.properties(放项目根目录):
1
2
3
4
5
6
7
8
9
| sonar.projectKey=order-service
sonar.projectName=Order Service
sonar.projectVersion=1.0.0
sonar.sources=order-service/src/main/java
sonar.tests=order-service/src/test/java
sonar.java.binaries=order-service/target/classes
sonar.java.coveragePlugin=jacoco
sonar.coverage.jacoco.xmlReportPaths=order-service/target/site/jacoco/jacoco.xml
sonar.exclusions=**/generated/**,**/proto/**
|
Quality Gate 规则推荐:
- 新代码覆盖率 ≥ 80%
- 新代码 0 个 Blocker/Critical 漏洞
- 代码异味 < 50
- 重复率 < 3%
Docker 构建 + 推送:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| stage('Docker Build') {
agent { label 'docker' }
steps {
sh """
docker build \
--build-arg JAR_FILE=order-service/target/*.jar \
-t ${DOCKER_IMAGE} \
-t ${IMAGE_NAME}:latest \
.
"""
}
}
stage('Push') {
agent { label 'docker' }
steps {
withCredentials([usernamePassword(
credentialsId: 'harbor-creds',
usernameVariable: 'HARBOR_USER',
passwordVariable: 'HARBOR_PASS'
)]) {
sh """
docker login -u ${HARBOR_USER} -p ${HARBOR_PASS} harbor.xxx.com
docker push ${DOCKER_IMAGE}
docker push ${IMAGE_NAME}:latest
"""
}
}
}
|
3 个优化点:
- 多 tag 同时打:commit SHA + latest,让 K8s 既能精确回滚到某次 commit,也能用 latest 拉最新
--build-arg 注入 jar:多模块项目镜像只装当前服务的 jar,不装别的- Harbor 项目隔离:每个团队一个 Harbor project,用 RBAC 控制谁能 push/pull
3.6 Deploy 阶段详解 + 金丝雀
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| stage('Deploy') {
agent { label 'k8s' }
when {
expression { return params.ENV != '' }
}
steps {
script {
// 凭据切换(多环境)
def credMap = [
dev: 'k8s-dev-creds',
staging: 'k8s-stg-creds',
prod: 'k8s-prod-creds'
]
withCredentials([file(
credentialsId: credMap[params.ENV],
variable: 'KUBECONFIG'
)]) {
sh """
# 1. 替换镜像版本
kubectl --kubeconfig=\${KUBECONFIG} \\
set image deployment/${APP_NAME} \\
${APP_NAME}=${DOCKER_IMAGE} \\
-n ${K8S_NAMESPACE}
# 2. 等待滚动更新完成
kubectl --kubeconfig=\${KUBECONFIG} \\
rollout status deployment/${APP_NAME} \\
-n ${K8S_NAMESPACE} \\
--timeout=5m
"""
}
}
}
}
|
注意:
set image 是原地更新,会触发 K8s 滚动更新(默认 25% maxSurge / 25% maxUnavailable)- 如果你用的是 Argo Rollouts(推荐),改用
kubectl argo rollouts set image,后面第七章详讲 - 镜像 tag 必须是固定值(commit SHA),不要用 latest——latest 会导致 K8s 永远拿最新镜像,回滚失败
3.7 完整流水线时序图
sequenceDiagram
participant Dev as 开发者
participant Git as Git 仓库
participant J as Jenkins
participant MA as maven agent
participant DA as docker agent
participant H as Harbor
participant K as K8s
Dev->>Git: push 代码
Git->>J: Webhook 触发
J->>MA: 分配构建任务
MA->>MA: mvn compile
MA->>MA: mvn test
MA->>J: 报告测试结果
MA->>MA: sonar scan
J->>MA: Quality Gate check
MA-->>J: pass / fail
MA->>MA: mvn package
MA->>J: archive jar
J->>DA: 分配构建任务
DA->>DA: docker build
DA->>H: docker push
H-->>DA: success
J->>K: kubectl set image
K->>K: 滚动更新
K-->>J: rollout complete
J->>Dev: 钉钉通知
第三章总结:一份 150+ 行的 Jenkinsfile 覆盖 8 个 stage、4 类 stage agent、6 种 stage 后置动作——这是 Java 微服务 CI/CD 的最小完整骨架。
下一章我们把这个 Jenkinsfile 接到多环境分支策略——develop / staging / release / master 的 GitFlow 怎么映射到 Jenkins Job。
四、多环境 + GitFlow 集成
4.1 GitFlow 分支策略:dev / staging / release / master
Jenkinsfile 写好后,下一步是让它在不同分支跑不同的逻辑——这就是「GitFlow + Jenkins」的精髓。
我们团队用的 GitFlow(2020 年版简化版,去掉了一些繁琐环节):
gitGraph
commit
commit
branch develop
checkout develop
commit
commit
branch feature/order-v2
checkout feature/order-v2
commit
commit
checkout develop
merge feature/order-v2
commit
branch release/1.0.0
checkout release/1.0.0
commit
checkout master
merge release/1.0.0 tag:"v1.0.0"
checkout develop
merge release/1.0.0
branch hotfix/critical-bug
checkout hotfix/critical-bug
commit
checkout master
merge hotfix/critical-bug tag:"v1.0.1"
checkout develop
merge hotfix/critical-bug6 类分支各自对应一个环境 + 触发器:
| 分支 | 环境 | 触发器 | 凭据 | 通知人 |
|---|
feature/* | dev | PR push | dev k8s | 作者 |
develop | dev | push | dev k8s | 全员 |
staging | staging | push | staging k8s | QA 团队 |
release/* | staging | push | staging k8s | QA + 研发负责人 |
master | prod | push | prod k8s | 全员 + 总监 |
hotfix/* | prod | push | prod k8s | 全员 + 总监 |
核心规则:
- feature → develop:开发者 fork feature 分支开发,合到 develop 自动跑 dev 流水线
- develop → staging:每周末从 develop 切 staging 分支,QA 在 staging 环境做集成测试
- staging → release → master:QA 通过后切 release 分支,再合 master + 打 tag,自动触发 prod 流水线
- hotfix → master:生产事故从 master 拉 hotfix 分支,修完直接合 master,同时 cherry-pick 回 develop
4.2 Webhook 配置:从 git push 到 Jenkins 触发
GitHub Webhook(Settings → Webhooks → Add):
1
2
3
| Payload URL: http://jenkins.xxx.com/generic-webhook-trigger/invoke?token=order-service-token
Content type: application/json
Events: push, pull_request
|
GitLab Webhook(Settings → Webhooks):
1
2
| URL: http://jenkins.xxx.com/project/order-service
Events: Push events, Merge request events
|
Jenkins 端配 Generic Webhook Trigger:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // Jenkinsfile 顶部
triggers {
GenericTrigger(
// token 唯一标识这个 job
token: 'order-service-token',
// 打印 payload 到控制台(调试用)
printPostContent: true,
// 解析 push 事件
regexpFilterText: '$ref',
regexpFilterExpression: '^refs/heads/(.*)$',
// 提取分支名
causeString: 'Triggered by push on branch',
// 参数化注入
genericVariables: [
[key: 'BRANCH_FROM_PAYLOAD', value: '$.ref'],
]
)
}
|
通用 Webhook 的 5 大优势(vs 老版 GitHub/GitLab 插件):
- 支持任意 Git 平台:GitHub / GitLab / Gitee / 自建 Git,配置都一样
- 支持任意事件:push / pull_request / tag / release
- 支持复杂过滤:只触发特定分支、特定文件变更
- 支持参数化注入:把 webhook payload 里的 commit、author、message 注入到 Job 参数
- 调试友好:
printPostContent: true 把整个 payload 打到控制台
4.3 Parameterized Build + 多环境凭据切换
3 个核心参数:
1
2
3
4
5
6
7
8
| parameters {
choice(name: 'ENV', choices: ['dev', 'staging', 'prod'],
description: '部署环境')
string(name: 'BRANCH', defaultValue: 'main',
description: '代码分支')
string(name: 'IMAGE_TAG', defaultValue: '',
description: '镜像 tag(留空用 commit SHA)')
}
|
多环境凭据切换(最常见的坑):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| stage('Deploy') {
steps {
script {
def credMap = [
dev: 'k8s-dev-creds',
staging: 'k8s-stg-creds',
prod: 'k8s-prod-creds'
]
def credId = credMap[params.ENV]
if (!credId) {
error("未知环境:${params.ENV}")
}
withCredentials([file(
credentialsId: credId,
variable: 'KUBECONFIG'
)]) {
sh "kubectl --kubeconfig=${KUBECONFIG} apply -f deploy/"
}
}
}
}
|
凭据 ID 必须用 Map 管理——不要在每个 stage 写 if (params.ENV == 'prod') 这种硬编码。
4.4 通知 + 审批流:钉钉机器人
钉钉自定义机器人配置:
- 钉钉群 → 群设置 → 智能群助手 → 添加机器人 → 自定义
- 复制 Webhook URL(含签名)
- Jenkins 凭据库添加:
dingtalk-webhook,类型 Secret text,值是 Webhook URL
Jenkinsfile 里发钉钉:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| post {
success {
script {
withCredentials([string(
credentialsId: 'dingtalk-webhook',
variable: 'WEBHOOK'
)]) {
sh """
curl -X POST ${WEBHOOK} \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "markdown",
"markdown": {
"title": "✅ 构建成功",
"text": "## ✅ ${env.JOB_NAME} #${env.BUILD_NUMBER}\n\n**环境**: ${params.ENV}\n**分支**: ${params.BRANCH}\n**提交**: ${env.GIT_COMMIT}\n\n[查看构建](${env.BUILD_URL})"
}
}'
"""
}
}
}
failure {
// 失败也发,加 @ 负责人
...
}
}
|
生产部署的「5 分钟审批缓冲」(用 input 步骤):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| stage('Deploy to Prod') {
when {
expression { return params.ENV == 'prod' }
}
steps {
script {
// 1. 部署到 prod
deployToProd()
// 2. 等 5 分钟(缓冲回滚)
echo '等待 5 分钟观察监控...'
sleep 300
// 3. 人工确认
timeout(time: 10, unit: 'MINUTES') {
input(
message: '生产部署后 5 分钟观察期已到,是否确认发布?',
ok: '确认(继续)',
submitter: 'tech-lead,devops',
submitterParameter: 'APPROVER'
)
}
// 4. 记录审批人
echo "Prod 部署由 ${APPROVER} 确认"
}
}
}
|
submitter: 'tech-lead,devops' 表示只有技术负责人或运维才能点确认,其他人在 Jenkins 上看不到「确认」按钮。
双保险:
- 技术层:
input 步骤人工确认 - 流程层:生产部署走单独的 Job(不是 dev/staging 同一个 Job),且需要 tech-lead 才能触发
第四章总结:GitFlow 分支映射到 Jenkins Job,Webhook 自动触发,参数化构建 + 多环境凭据切换,钉钉通知 + 生产审批流——这套组合拳让「代码提交 → 生产部署」全过程可追溯、可审计、可回滚。
下一章我们把 Jenkinsfile 里的可复用逻辑抽成 Shared Library——80 个微服务共用同一份构建代码。
五、共享库封装:把可复用 Stage 抽成库
5.1 共享库目录结构
到 2021 年,团队里有 80 个微服务,每个微服务的 Jenkinsfile 都长得差不多——都是 Build/Test/Sonar/Package/Docker/Push/Deploy 7 步。
问题是「差不多但不完全一样」:
- 30% 是 Spring Boot,10% 是 Node.js,5% 是 Python
- 不同服务 Docker 基础镜像不同(JDK 17 / JDK 21 / JDK 11)
- 不同服务 K8s namespace 不同
- 不同服务 SonarQube projectKey 不同
一开始我每个服务的 Jenkinsfile 复制粘贴一遍。结果:
- 80 个 Jenkinsfile 改了 80 次
- 改错一个地方,80 个服务出 bug
- 新人入职配一个服务的 Jenkinsfile 要 2 小时
2022 年我引入 Shared Library(共享库)——把 Jenkinsfile 里的可复用逻辑抽到一个独立 Git 仓库,80 个微服务共用。
graph TB
Repo1[jenkins-shared-library
独立 Git 仓库] --> Vars[vars/
buildMaven.groovy
deployK8s.groovy]
Repo1 --> Src[src/com/myteam/ci/
Utils.groovy
DockerUtils.groovy]
Repo1 --> Res[resources/
sonar-template.properties
dockerfile-templates/]
M1[order-service
Jenkinsfile] -->|@Library| Vars
M2[user-service
Jenkinsfile] -->|@Library| Vars
M3[payment-service
Jenkinsfile] -->|@Library| Vars
style Repo1 fill:#3c82dc,color:#fff
style M1 fill:#5aa0e8,color:#fff
style M2 fill:#5aa0e8,color:#fff
style M3 fill:#5aa0e8,color:#fff目录结构(Jenkins 规定):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| jenkins-shared-library/ # 独立 Git 仓库
├── vars/ # 【声明式直接调用的方法】
│ ├── buildMaven.groovy # 文件名 = 方法名
│ ├── buildNode.groovy
│ ├── buildPython.groovy
│ ├── dockerBuild.groovy
│ ├── dockerPush.groovy
│ ├── deployK8s.groovy
│ ├── deployArgoRollout.groovy
│ ├── notifyDingTalk.groovy
│ └── sonarScan.groovy
├── src/ # 【普通 Groovy 类】
│ └── com/myteam/ci/
│ ├── Utils.groovy # 工具类
│ ├── DockerUtils.groovy
│ └── K8sUtils.groovy
├── resources/ # 【静态资源】
│ ├── com/myteam/ci/
│ │ └── Dockerfile-template.txt # Dockerfile 模板
│ └── sonar/
│ └── sonar-template.properties
├── test/ # 【单元测试】
│ └── com/myteam/ci/
│ └── UtilsTest.groovy
├── README.md
└── CHANGELOG.md
|
关键约定:
vars/ 下的 .groovy 文件文件名 = 方法名,声明式 Pipeline 里直接调用src/ 下的类不会被声明式直接调用,需要 import 或 def utils = new com.myteam.ci.Utils()resources/ 下的文件运行时可通过 libraryResource 'path/to/file' 读取
5.2 完整 vars/ 脚本
vars/buildMaven.groovy(构建 Java 服务):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // vars/buildMaven.groovy
import com.myteam.ci.Utils
def call(Map args) {
// 参数校验
if (!args.projectName) {
error "buildMaven 必须传 projectName 参数"
}
def projectName = args.projectName
def goals = args.goals ?: 'clean package'
def skipTests = args.skipTests ?: false
def useJacoco = args.useJacoco ?: true
// 打印分隔
Utils.printBanner("Maven Build: ${projectName}")
// 1. 编译
sh "mvn -B -V -T 4 -pl ${projectName} -am clean compile"
// 2. 测试
if (!skipTests) {
sh "mvn -B -pl ${projectName} -am test"
junit allowEmptyResults: true,
testResults: "**/${projectName}/target/surefire-reports/*.xml"
if (useJacoco) {
jacoco(execPattern: "**/${projectName}/target/jacoco.exec",
minimumLineCoverage: '0.60')
}
}
// 3. 打包
sh "mvn -B -pl ${projectName} -am -DskipTests clean package"
archiveArtifacts artifacts: "${projectName}/target/*.jar",
fingerprint: true
}
|
vars/deployK8s.groovy(部署到 K8s):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| // vars/deployK8s.groovy
import com.myteam.ci.K8sUtils
def call(Map args) {
def env = args.env
def appName = args.appName
def image = args.image
def namespace = args.namespace ?: env
def replicas = args.replicas ?: 3
def waitTime = args.waitTime ?: '5m'
// 环境校验
if (!['dev', 'staging', 'prod'].contains(env)) {
error "deployK8s env 必须是 dev/staging/prod,当前:${env}"
}
// 凭据切换
def credMap = [
dev: 'k8s-dev-creds',
staging: 'k8s-stg-creds',
prod: 'k8s-prod-creds'
]
withCredentials([file(
credentialsId: credMap[env],
variable: 'KUBECONFIG'
)]) {
Utils.printBanner("Deploy ${appName} to ${env}")
// 1. 替换镜像
sh """
kubectl --kubeconfig=\${KUBECONFIG} \
set image deployment/${appName} \
${appName}=${image} \
-n ${namespace}
"""
// 2. 等待 rollout
sh """
kubectl --kubeconfig=\${KUBECONFIG} \
rollout status deployment/${appName} \
-n ${namespace} \
--timeout=${waitTime}
"""
// 3. 健康检查
K8sUtils.healthCheck(
kubeconfig: env.KUBECONFIG,
appName: appName,
namespace: namespace
)
}
}
|
src/com/myteam/ci/Utils.groovy(共享工具类):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // src/com/myteam/ci/Utils.groovy
package com.myteam.ci
class Utils implements Serializable {
private static final long serialVersionUID = 1L
static void printBanner(String text) {
def line = '*' * (text.length() + 8)
println ""
println line
println "*** ${text} ***"
println line
}
static String getGitShortSha() {
return sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
static Map parseEnv(String envStr) {
def parts = envStr.split('/')
return [
cluster: parts[0],
namespace: parts.size() > 1 ? parts[1] : 'default'
]
}
}
|
声明式 Pipeline 里调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| // order-service/Jenkinsfile
@Library('jenkins-shared-library@1.0.0') _ // 锁版本!
pipeline {
agent none
stages {
stage('Build') {
agent { label 'maven' }
steps {
buildMaven(
projectName: 'order-service',
goals: 'clean package',
skipTests: false
)
}
}
stage('Deploy') {
agent { label 'k8s' }
steps {
deployK8s(
env: 'dev',
appName: 'order-service',
image: "${DOCKER_IMAGE}"
)
}
}
}
}
|
对比两种写法:
| 维度 | 每个项目独立 Jenkinsfile | 用 Shared Library |
|---|
| 新服务接入 | 复制粘贴 Jenkinsfile,改 5 处 | 加 1 个调用,改 1 处 |
| 改构建逻辑 | 80 个 Jenkinsfile 改 80 次 | 共享库改 1 处,80 个服务生效 |
| 复用率 | 30-40% 代码重复 | 90%+ 复用 |
| 学习成本 | 看懂每个项目的 Jenkinsfile | 看懂共享库 |
| 推荐 | 5 个服务以内 | 5+ 服务必上 |
5.3 版本管理与灰度发布
共享库版本策略:
1
2
3
4
5
6
| # 共享库仓库打 tag
git tag -a v1.0.0 -m "stable"
git push origin v1.0.0
# Jenkinsfile 锁版本
@Library('jenkins-shared-library@1.0.0') _
|
SemVer 规范:
- v1.x.y:1.x 互相兼容,小版本 y 加 feature
- v2.0.0:大版本不兼容,必须升级所有调用方
灰度发布策略(推荐):
graph LR
A[v1.0.0 老 Pipeline 继续] --> B[新 Pipeline 切 v2.0.0]
B --> C[观察 1 个月]
C --> D[所有 Pipeline 升级 v2.0.0]
D --> E[v1.0.0 弃用]Jenkins 全局配置共享库:
- Manage Jenkins → System → Global Pipeline Libraries
- Name:
jenkins-shared-library - Default version:
main(开发分支) - 勾选:
Allow default version to be overridden / Include @Library changes in build results - Retrieval method:
Modern SCM → Git - Project repository:
git@github.com:myteam/jenkins-shared-library.git - Credentials:
github-ssh-key
强制锁版本:
Jenkinsfile 第一行必须显式写版本号——@Library('jenkins-shared-library@1.0.0') _,不能省略版本。否则共享库主干一改,所有 Pipeline 立刻受影响,生产事故高发。
5.4 共享库单元测试
共享库没有测试 = 改一行所有服务崩。Jenkins 官方推荐 Jenkins Pipeline Unit 框架:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // test/com/myteam/ci/UtilsTest.groovy
package com.myteam.ci
import org.junit.Test
import static org.junit.Assert.assertEquals
class UtilsTest {
@Test
void testParseEnv() {
def result = Utils.parseEnv('prod/order')
assertEquals 'prod', result.cluster
assertEquals 'order', result.namespace
}
@Test
void testParseEnvWithoutNamespace() {
def result = Utils.parseEnv('dev')
assertEquals 'dev', result.cluster
assertEquals 'default', result.namespace
}
}
|
跑测试:
1
| ./gradlew test # 或 mvn test
|
测试覆盖率要求:共享库核心方法覆盖率 ≥ 80%。
第五章总结:共享库让 80 个微服务共用同一份构建/部署逻辑,改一处生效全栈。版本管理 + 灰度发布保证升级安全。
下一章我们把流水线接到容器化部署——Jenkins 构建 Docker 镜像 + 推 Harbor + Helm 打包 K8s manifests。
六、Docker + K8s 集成
6.1 Docker Agent 设计:DIND vs Docker Socket
Jenkins 构建 Docker 镜像有 3 种方案,选错会爆:
| 方案 | 原理 | 优势 | 劣势 |
|---|
| DIND(Docker in Docker) | Agent 容器内跑独立 docker daemon | 完全隔离、安全 | 慢(每个 build 起 daemon)、镜像缓存不共享 |
| Socket 挂载 | 挂宿主机 /var/run/docker.sock | 快(复用宿主机 daemon)、镜像共享 | 权限大(容器内能改宿主机所有镜像) |
| Kaniko | 不需要 docker daemon,纯 rootless | 适合 K8s Pod 内构建、rootless | 学习曲线、调试难 |
我的选型:
- 本地 demo / 开发环境:Socket 挂载——构建快、镜像能直接在 K8s 看到
- 生产 CI:DIND——隔离好,每个 build 是干净的 docker daemon
- K8s Pod Agent:Kaniko——Pod 内 rootless 构建
DIND 配置(生产推荐):
1
2
3
4
5
6
7
8
9
10
| # docker-compose.yml
docker-agent:
image: docker:24-dind
privileged: true # DIND 必须特权模式
environment:
- DOCKER_TLS_CERTDIR= # 关闭 TLS(容器内用)
volumes:
- docker_data:/var/lib/docker # 独立 docker 数据卷
networks:
- jenkins-net
|
Socket 挂载配置(本地 demo):
1
2
3
4
5
6
| # docker-compose.yml
maven-agent:
image: maven:3.9-eclipse-temurin-17
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /usr/bin/docker:/usr/bin/docker:ro
|
注意:
- Socket 挂载时
user 必须是 root(默认 user 1000 没有 docker.sock 权限) - 或者把 docker.sock 权限改成 666(
chmod 666 /var/run/docker.sock) - 容器内能调宿主机 docker = 能 rm 任何镜像、起任何容器,生产环境慎用
6.2 Dockerfile 最佳实践
生产级 Dockerfile(Java 17 + Spring Boot):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| # syntax=docker/dockerfile:1.7
# 多阶段构建:第一阶段用 JDK 编译,第二阶段用 JRE 跑
# ===== Stage 1: Build =====
FROM eclipse-temurin:17-jdk AS builder
WORKDIR /build
# 1. 先复制 POM(利用 Docker 缓存)
COPY pom.xml ./
COPY common-lib/pom.xml common-lib/
COPY order-service/pom.xml order-service/
RUN mvn -B -T 4 -q dependency:go-offline
# 2. 复制源码
COPY . .
# 3. 打包
RUN mvn -B -pl order-service -am -DskipTests clean package
# ===== Stage 2: Runtime =====
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
# 4. 安装必要工具
RUN apt-get update && \
apt-get install -y --no-install-recommends curl tini && \
rm -rf /var/lib/apt/lists/*
# 5. 复制 jar
COPY --from=builder /build/order-service/target/*.jar app.jar
# 6. 标签元数据
LABEL org.opencontainers.image.title="order-service" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.revision="${GIT_SHA}" \
org.opencontainers.image.source="https://github.com/myteam/order-service"
# 7. 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# 8. 用 tini 作为 init 进程(处理僵尸进程)
ENTRYPOINT ["/usr/bin/tini", "--", "java", \
"-XX:+UseContainerSupport", \
"-XX:MaxRAMPercentage=75.0", \
"-Djava.security.egd=file:/dev/./urandom", \
"-jar", "/app/app.jar"]
EXPOSE 8080
|
5 大最佳实践:
- 多阶段构建:构建阶段用 JDK(含编译工具),运行阶段用 JRE(省 200MB)
- 先 COPY POM 再 COPY 源码:利用 Docker layer 缓存,Maven 依赖不重下
-XX:MaxRAMPercentage=75.0:让 JVM 自动适配 K8s limit,不写死 -Xmxtini 作为 init 进程:处理孤儿进程、转发信号,Java 进程优雅退出- HEALTHCHECK 内置:让 K8s 知道容器是否健康,livenessProbe 不用调外部脚本
镜像体积优化对比:
| 基础镜像 | 体积 | 启动时间 |
|---|
openjdk:17-jdk | 700MB | 慢 |
eclipse-temurin:17-jdk | 480MB | 中 |
eclipse-temurin:17-jre | 280MB | 快 |
eclipse-temurin:17-jre-alpine | 180MB | 最快 |
eclipse-temurin:17-jre-jammy | 280MB | 快(推荐) |
Alpine 注意:musl libc 跟 glibc 不完全兼容,Netty / DNS 解析偶尔有坑,生产建议用 jammy(Ubuntu 22.04 基础)。
6.3 Helm Chart 打包:Deployment + Service + Ingress
Helm Chart 目录结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| charts/order-service/
├── Chart.yaml
├── values.yaml
├── values-dev.yaml
├── values-staging.yaml
├── values-prod.yaml
└── templates/
├── deployment.yaml
├── service.yaml
├── ingress.yaml
├── configmap.yaml
├── serviceaccount.yaml
├── hpa.yaml
├── pdb.yaml
└── _helpers.tpl
|
Chart.yaml:
1
2
3
4
5
6
7
8
9
| apiVersion: v2
name: order-service
description: Order Service for MyTeam
type: application
version: 1.0.0
appVersion: "1.0.0"
maintainers:
- name: MyTeam DevOps
email: devops@xxx.com
|
values.yaml(默认值):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| replicaCount: 3
image:
repository: harbor.xxx.com/myteam/order-service
pullPolicy: IfNotPresent
tag: "" # 留空,CI 覆盖
imagePullSecrets:
- name: harbor-pull-secret
service:
type: ClusterIP
port: 8080
targetPort: 8080
ingress:
enabled: true
className: nginx
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
hosts:
- host: order-service.xxx.com
paths:
- path: /
pathType: Prefix
tls:
- hosts:
- order-service.xxx.com
secretName: order-service-tls
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 2000m
memory: 2Gi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70
probes:
liveness:
path: /actuator/health/liveness
initialDelaySeconds: 60
periodSeconds: 10
readiness:
path: /actuator/health/readiness
initialDelaySeconds: 30
periodSeconds: 5
env:
- name: SPRING_PROFILES_ACTIVE
value: prod
- name: JAVA_OPTS
value: "-Xms512m -Xmx1536m"
|
templates/deployment.yaml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "order-service.fullname" . }}
labels:
{{- include "order-service.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "order-service.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "order-service.selectorLabels" . | nindent 8 }}
spec:
imagePullSecrets:
{{- toYaml .Values.imagePullSecrets | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
livenessProbe:
{{- toYaml .Values.probes.liveness | nindent 12 }}
readinessProbe:
{{- toYaml .Values.probes.readiness | nindent 12 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
env:
{{- toYaml .Values.env | nindent 12 }}
|
多环境 values 覆盖(values-prod.yaml):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| replicaCount: 5
resources:
requests:
cpu: 1000m
memory: 2Gi
limits:
cpu: 4000m
memory: 4Gi
autoscaling:
minReplicas: 5
maxReplicas: 20
env:
- name: SPRING_PROFILES_ACTIVE
value: prod
- name: LOG_LEVEL
value: INFO
|
6.4 Jenkinsfile 中调用 Helm 部署
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| stage('Deploy with Helm') {
agent { label 'k8s' }
steps {
script {
def credMap = [
dev: 'k8s-dev-creds',
staging: 'k8s-stg-creds',
prod: 'k8s-prod-creds'
]
withCredentials([file(
credentialsId: credMap[params.ENV],
variable: 'KUBECONFIG'
)]) {
// 1. 校验
sh """
helm lint ./charts/order-service \
--values ./charts/order-service/values-${params.ENV}.yaml
"""
// 2. 模板预览(dry run)
sh """
helm template order-service ./charts/order-service \
--values ./charts/order-service/values-${params.ENV}.yaml \
--set image.tag=${IMAGE_TAG}
"""
// 3. 部署
sh """
helm upgrade --install order-service ./charts/order-service \
--kubeconfig=\${KUBECONFIG} \
--namespace ${K8S_NAMESPACE} \
--values ./charts/order-service/values-${params.ENV}.yaml \
--set image.tag=${IMAGE_TAG} \
--wait \
--timeout 5m \
--history-max 10
"""
}
}
}
}
|
关键参数解析:
--wait:等到所有 Pod Ready 才返回(阻塞)--timeout 5m:超时 5 分钟--history-max 10:保留 10 个历史版本(便于回滚)--set image.tag=${IMAGE_TAG}:动态覆盖镜像 tag
回滚命令(生产事故救命):
1
2
3
4
5
6
7
8
9
10
11
12
| # 列出历史版本
helm history order-service -n prod
# 输出:
# REVISION STATUS CHART APP VERSION DESCRIPTION
# 1 failed order-service 1.0.0 Install failed
# 2 superseded order-service 1.0.0 Upgrade complete
# 3 deployed order-service 1.0.0 Upgrade complete
# 4 superseded order-service 1.0.0 Upgrade complete
# 5 deployed order-service 1.0.0 Upgrade complete
# 回滚到版本 4
helm rollback order-service 4 -n prod
|
Helm + GitOps:把 values.yaml + Chart.yaml + templates/ 推 Git,Argo CD 监听 Git 自动部署(强烈推荐,下章细讲)。
第六章总结:Docker 多阶段构建 + 镜像体积优化 + Helm Chart 多环境覆盖 + Jenkinsfile 调 Helm 部署,容器化部署全链路打通。
下一章我们把 K8s 原生蓝绿发布做透——双 Service + 双 Deployment + 一键切流。
七、蓝绿发布全流程
7.1 蓝绿发布原理:双 Service 双 Deployment
到 2022 年中,我们团队的事故率降到接近 0——主要靠「蓝绿发布」兜底。
蓝绿发布的核心思想:任意时刻只有一套环境接流量。新版本先部署到「非活跃」环境,验证通过后一键切流量;出问题了切回原版本,2 秒回滚。
stateDiagram-v2
[*] --> BlueActive: 初始状态
BlueActive --> GreenDeploying: 部署 v2 到 green
GreenDeploying --> GreenReady: 健康检查通过
GreenReady --> GreenReady: 5 分钟观察
GreenReady --> GreenActive: 切流量到 green
GreenActive --> GreenActive: 监控 1 小时
GreenActive --> BlueActive: 保留 blue 1 小时(回滚缓冲)
GreenActive --> Failed: 出问题
Failed --> BlueActive: 一键切回 blue(2 秒)
GreenActive --> [*]4 大优势:
- 回滚极快——切 Service selector 而非重新部署,2 秒回滚
- 风险窗口小——切流瞬间只有「活跃服务」接流量,没有「老服务还在接新流量」的中间态
- 零停机——整个过程用户无感知
- 数据库兼容测试——可以在 green 跑数据库 schema migration,确认无问题再切
3 大劣势:
- 资源 2x——同时跑 2 套环境,资源成本翻倍
- 数据库 schema 必须向前向后兼容——green 和 blue 读同一份数据库,不能有 incompatible 变更
- 不适合「session 黏性」应用——切流后用户的 session 状态丢失(除非共享 session 存储)
适用场景:
- 金融、支付类(风险厌恶)
- 7x24 业务(不能有停机窗口)
- 单服务变更不频繁(资源成本可接受)
7.2 K8s 资源模板:双 Service + 双 Deployment
核心资源:1 个 Ingress(对外)+ 2 个 Service(blue/green)+ 2 个 Deployment(blue/green)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| # deployment-blue.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service-blue
labels:
app: order-service
version: blue
spec:
replicas: 3
selector:
matchLabels:
app: order-service
version: blue
template:
metadata:
labels:
app: order-service
version: blue
spec:
containers:
- name: order-service
image: harbor.xxx.com/myteam/order-service:v1.0.0
ports:
- containerPort: 8080
resources:
requests: { cpu: '500m', memory: '1Gi' }
limits: { cpu: '2000m', memory: '2Gi' }
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 8080 }
initialDelaySeconds: 30
periodSeconds: 5
---
# deployment-green.yaml(与 blue 完全相同,只是 name 和 version 不同)
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service-green
labels:
app: order-service
version: green
spec:
replicas: 3
selector:
matchLabels:
app: order-service
version: green
template:
metadata:
labels:
app: order-service
version: green
spec:
containers:
- name: order-service
image: harbor.xxx.com/myteam/order-service:v1.0.0
ports:
- containerPort: 8080 }
# ... 同 blue
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| # service-blue.yaml(流量接到 blue)
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
version: blue # ← 关键:这里决定流量去哪个 Deployment
ports:
- port: 80
targetPort: 8080
---
# service-green-dummy.yaml(用于健康检查)
apiVersion: v1
kind: Service
metadata:
name: order-service-green-debug
spec:
selector:
app: order-service
version: green
ports:
- port: 80
targetPort: 8080
|
关键设计:
- 主 Service 名为
order-service,不带 blue/green 后缀——Ingress 引用它,对外 URL 不变 service-green-debug 只用于调试访问(开发/QA 内部访问 green 版本,不影响生产流量)- HPA 必须分别配——blue HPA 和 green HPA 独立伸缩
- PodDisruptionBudget 分别配——保证滚动更新时至少 1 个 Pod 可用
7.3 Jenkinsfile 蓝绿切流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
| stage('Blue-Green Deploy') {
agent { label 'k8s' }
steps {
script {
def credId = credMap[params.ENV]
withCredentials([file(credentialsId: credId, variable: 'KUBECONFIG')]) {
// 1. 判定当前活跃色
def activeColor = sh(
returnStdout: true,
script: """
kubectl --kubeconfig=\${KUBECONFIG} get svc order-service \
-n ${K8S_NAMESPACE} \
-o jsonpath='{.spec.selector.version}'
"""
).trim()
def standbyColor = (activeColor == 'blue') ? 'green' : 'blue'
echo "当前活跃:${activeColor},将部署到:${standbyColor}"
// 2. 部署新版本到 standby
sh """
kubectl --kubeconfig=\${KUBECONFIG} \
set image deployment/order-service-${standbyColor} \
order-service=${DOCKER_IMAGE} \
-n ${K8S_NAMESPACE}
kubectl --kubeconfig=\${KUBECONFIG} \
rollout status deployment/order-service-${standbyColor} \
-n ${K8S_NAMESPACE} \
--timeout=5m
"""
// 3. 健康检查(访问 green-debug Service)
sh """
sleep 30
for i in {1..10}; do
if kubectl --kubeconfig=\${KUBECONFIG} \
exec deploy/order-service-${standbyColor} \
-n ${K8S_NAMESPACE} -- \
curl -sf http://localhost:8080/actuator/health; then
echo "健康检查通过"
break
fi
echo "等待健康检查... \${i}/10"
sleep 10
done
"""
// 4. 灰度(可选):先切 10% 流量观察
if (params.ENV == 'prod') {
input(
message: "新版本已部署到 ${standbyColor},是否切流?",
ok: "切流到 ${standbyColor}",
submitter: 'tech-lead,devops'
)
}
// 5. 切流量
sh """
kubectl --kubeconfig=\${KUBECONFIG} \
patch svc order-service \
-n ${K8S_NAMESPACE} \
-p '{"spec":{"selector":{"version":"${standbyColor}"}}}'
echo "流量已切到 ${standbyColor}"
"""
// 6. 观察 1 小时
echo "观察 1 小时(保留 ${activeColor} 部署做回滚缓冲)"
sleep 3600
// 7. 清理旧部署
sh """
kubectl --kubeconfig=\${KUBECONFIG} \
scale deployment/order-service-${activeColor} \
--replicas=0 \
-n ${K8S_NAMESPACE}
"""
}
}
}
}
|
5 大安全卡点:
- 判定活跃色——自动判断当前在 blue 还是 green
- 健康检查——切流前确认新版本健康
- 生产人工审批——
input 步骤让 tech-lead 确认 - 保留旧部署 1 小时——出问题一键切回
- 1 小时后清理——避免资源浪费
回滚操作(生产事故):
1
2
3
| # 2 秒回滚(假设当前在 green,blue 还在)
kubectl patch svc order-service -n prod \
-p '{"spec":{"selector":{"version":"blue"}}}'
|
回滚后检查清单:
7.4 蓝绿 vs Argo Rollouts:什么时候用哪个
| 维度 | 蓝绿 | Argo Rollouts |
|---|
| 切流粒度 | 二选一(100% / 0%) | 任意比例(1% / 10% / 50% / 100%) |
| 自动判定 | 需要人工审批 | 可按 Prometheus 指标自动推进/回滚 |
| 资源占用 | 2x | 1.x(滚动过程短暂 2x) |
| 复杂度 | 中(手工管) | 高(Argo Rollouts 概念多) |
| 适用 | 风险厌恶业务 | 快速迭代业务 |
我的混合策略:
- 核心业务(支付、订单):用蓝绿——简单、可控、回滚 2 秒
- 非核心业务(CMS、运营后台):用 Argo Rollouts 金丝雀——自动化、节省资源
下一章我们把 Argo Rollouts 金丝雀做透——按 Prometheus 指标自动推进/回滚。
八、金丝雀发布全流程
8.1 金丝雀 vs 蓝绿:渐进式 vs 一次性
2023 年初,我们团队开始用 Argo Rollouts 做金丝雀发布——金丝雀是「渐进式切流」,蓝绿是「一次性切流」。
stateDiagram-v2
[*] --> Stable: 100% 流量老版本
Stable --> Canary1: 部署金丝雀 5% 流量
Canary1 --> Analysis1: 跑 5 分钟
Analysis1 --> Canary2: 指标通过 → 15%
Analysis1 --> Aborted: 指标失败
Canary2 --> Analysis2: 跑 5 分钟
Analysis2 --> Canary3: 指标通过 → 30%
Analysis2 --> Aborted: 指标失败
Canary3 --> Analysis3: 跑 5 分钟
Analysis3 --> Canary4: 指标通过 → 50%
Analysis3 --> Aborted: 指标失败
Canary4 --> ManualApproval: 跑 5 分钟
ManualApproval --> Full: 人工批准 → 100%
ManualApproval --> Aborted: 人工拒绝
Aborted --> Stable: 一键回滚
Full --> Stable: 部署完成5 大优势:
- 风险最小——开始只有 5% 流量出错,爆炸半径小
- 指标驱动——按 Prometheus 指标自动判定,不需要人工盯
- 资源相对省——不像蓝绿需要 2x 资源,只是短暂 2x
- 可重复——同样的发布配置,每个服务都能复用
- 可观测——每个阶段的指标都看得到
3 大挑战:
- 概念多——Rollout、AnalysisTemplate、AnalysisRun、Experiment、Step
- 依赖 Prometheus——必须先有完善的指标采集
- 调试复杂——出问题需要看 Rollout 状态 + AnalysisRun 详情 + Service Mesh 流量分发
8.2 Argo Rollouts 是什么:K8s Deployment 的「超集」
Argo Rollouts 是 Intuit 开源(被 CNCF 接纳为 Incubating)的 K8s 控制器,提供高级部署策略:
- Blue-Green(蓝绿)
- Canary(金丝雀)
- Progressive Delivery(渐进式)
- Analysis(基于指标的自动判定)
3 个核心 CRD:
graph LR
Rollout[Rollout
替代 Deployment] --> AnalysisTemplate[AnalysisTemplate
指标查询模板]
Rollout --> Service[Service
流量入口]
AnalysisTemplate -.引用.-> AnalysisRun[AnalysisRun
单次执行]
AnalysisRun -.查指标.-> Prometheus[Prometheus]
style Rollout fill:#3c82dc,color:#fff
style AnalysisTemplate fill:#ff9a3c,color:#fff
style AnalysisRun fill:#5aa0e8,color:#fff- Rollout:替代 Deployment 的部署对象,支持蓝绿/金丝雀策略
- AnalysisTemplate:指标查询模板(PromQL + 成功/失败条件)
- AnalysisRun:AnalysisTemplate 的一次执行,返回 success / failure
安装(在 k3d 本地或生产 K8s):
1
2
3
4
5
6
7
8
9
10
11
| # 安装 controller
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/download/v1.7.0/install.yaml
# 安装 kubectl plugin(本地)
curl -LO https://github.com/argoproj/argo-rollouts/releases/download/v1.7.0/kubectl-argo-rollouts-linux-amd64
chmod +x kubectl-argo-rollouts-linux-amd64
mv kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts
# 验证
kubectl argo rollouts version
|
8.3 Rollout 资源模板
rollout.yaml(金丝雀策略,5 阶段渐进):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: order-service
namespace: prod
spec:
replicas: 5
revisionHistoryLimit: 3
selector:
matchLabels:
app: order-service
strategy:
canary:
# 5 阶段渐进:5% → 15% → 30% → 50% → 100%
steps:
- setWeight: 5
- pause: { duration: 5m } # 5 分钟观察
- setWeight: 15
- pause: { duration: 5m }
- setWeight: 30
- pause: { duration: 5m }
- setWeight: 50
- pause: { duration: 5m }
# 最后一步:人工确认才 100%
- setWeight: 80
- pause: { duration: 10m }
# 自动判定:跑 AnalysisTemplate
- analysis:
templates:
- templateName: success-rate
- templateName: latency-p99
- setWeight: 100
# 流量切分:Nginx Ingress
trafficRouting:
nginx:
stableIngressName: order-service-stable
additionalIngressAnnotations:
canary-by-header: X-Canary # 支持按 header 切
canary-by-header-value: enabled
# 失败回滚:自动 abort
abortScaleDownDelaySeconds: 30
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: harbor.xxx.com/myteam/order-service:v1.0.0
ports:
- containerPort: 8080
resources:
requests: { cpu: '500m', memory: '1Gi' }
limits: { cpu: '2000m', memory: '2Gi' }
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 8080 }
initialDelaySeconds: 30
periodSeconds: 5
|
配套 Service(金丝雀模式下 Service 指向 stable Service):
1
2
3
4
5
6
7
8
9
10
| apiVersion: v1
kind: Service
metadata:
name: order-service-stable
spec:
selector:
app: order-service
ports:
- port: 80
targetPort: 8080
|
配套 Ingress(用 Nginx Ingress 做流量切分):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: order-service-stable
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: order-service.xxx.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: order-service-stable
port:
number: 80
|
Argo Rollouts 自动生成 order-service-canary Ingress,Nginx 控制器按 weight 分发流量。
8.4 AnalysisTemplate:按 Prometheus 指标自动判定
3 个核心指标模板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
| # 1. 成功率(HTTP 5xx 率 < 1%)
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
namespace: prod
spec:
args:
- name: service-name
- name: canary-hash
metrics:
- name: http-5xx-rate
interval: 60s
count: 5 # 连续 5 次(5 分钟)指标都满足才通过
successCondition: result[0] < 0.01
failureLimit: 2 # 连续 2 次失败就 abort
failureCondition: result[0] > 0.05
provider:
prometheus:
address: http://prometheus.monitoring.svc.cluster.local:9090
query: |
sum(rate(
http_requests_total{
app="{{args.service-name}}",
version="canary",
status=~"5.."
}[5m]
)) /
sum(rate(
http_requests_total{
app="{{args.service-name}}",
version="canary"
}[5m]
))
---
# 2. P99 延迟(< 500ms)
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: latency-p99
namespace: prod
spec:
args:
- name: service-name
metrics:
- name: p99-latency
interval: 60s
count: 5
successCondition: result[0] < 0.5
failureLimit: 2
provider:
prometheus:
address: http://prometheus.monitoring.svc.cluster.local:9090
query: |
histogram_quantile(0.99,
sum(rate(
http_request_duration_seconds_bucket{
app="{{args.service-name}}",
version="canary"
}[5m]
)) by (le)
)
---
# 3. 业务指标(订单成功率 > 99%)
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: business-conversion
namespace: prod
spec:
args:
- name: service-name
metrics:
- name: order-success-rate
interval: 60s
count: 5
successCondition: result[0] > 0.99
failureLimit: 2
provider:
prometheus:
address: http://prometheus.monitoring.svc.cluster.local:9090
query: |
sum(rate(
orders_created_total{
app="{{args.service-name}}",
status="success",
version="canary"
}[5m]
)) /
sum(rate(
orders_created_total{
app="{{args.service-name}}",
version="canary"
}[5m]
))
|
判定逻辑解读:
interval: 60s + count: 5 = 每 60 秒查一次,连续 5 次都满足才通过successCondition: result[0] < 0.01 = 5xx 率 < 1% 算成功failureLimit: 2 = 连续 2 次失败就 abort- 业务指标 + 技术指标双保险——不仅 HTTP 5xx 少,订单成功率也要高
4 大推荐指标:
| 指标类型 | 阈值 | 失败动作 |
|---|
| HTTP 5xx 率 | < 1% | abort |
| P99 延迟 | < 500ms | abort |
| JVM 堆使用率 | < 80% | warn(不 abort) |
| 业务转化率 | > 99% | abort |
8.5 Jenkinsfile + Argo Rollouts 调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
| stage('Canary Deploy') {
agent { label 'k8s' }
steps {
script {
def credId = credMap[params.ENV]
withCredentials([file(credentialsId: credId, variable: 'KUBECONFIG')]) {
// 1. 更新 Rollout 镜像
sh """
kubectl --kubeconfig=\${KUBECONFIG} \
argo rollouts set image order-service \
order-service=${DOCKER_IMAGE} \
-n ${K8S_NAMESPACE}
"""
// 2. 阻塞等金丝雀完成
sh """
kubectl --kubeconfig=\${KUBECONFIG} \
argo rollouts get rollout order-service \
-n ${K8S_NAMESPACE} \
--watch \
--timeout 1h
"""
// 3. 检查结果
sh """
STATUS=\$(kubectl --kubeconfig=\${KUBECONFIG} \
argo rollouts get rollout order-service \
-n ${K8S_NAMESPACE} \
-o jsonpath='{.status.phase}')
if [ "\$STATUS" != "Healthy" ]; then
echo "金丝雀发布未健康完成:\$STATUS"
exit 1
fi
"""
}
}
}
post {
success {
echo '金丝雀发布成功!'
}
failure {
script {
// 自动 abort + 告警
sh """
kubectl --kubeconfig=\${KUBECONFIG} \
argo rollouts abort order-service \
-n ${K8S_NAMESPACE}
"""
// 钉钉告警
...
}
}
}
}
|
5 个核心操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # 1. 查看状态
kubectl argo rollouts get rollout order-service -n prod
# 2. 暂停金丝雀(手动介入)
kubectl argo rollouts pause order-service -n prod
# 3. 恢复金丝雀
kubectl argo rollouts resume order-service -n prod
# 4. 手动 promote 到 100%
kubectl argo rollouts promote order-service -n prod
# 5. Abort 回滚
kubectl argo rollouts abort order-service -n prod
|
Dashboard 可视化(强烈推荐):
1
2
3
4
5
6
7
8
| # 安装 Argo Rollouts Dashboard
kubectl apply -f https://github.com/argoproj/argo-rollouts/releases/download/v1.7.0/dashboard-install.yaml
# 端口转发
kubectl port-forward svc/argo-rollouts-dashboard 31000:31000 -n argo-rollouts
# 访问 http://localhost:31000
# 可以看到所有 Rollout 的金丝雀进度、AnalysisTemplate 状态、历史
|
8.6 金丝雀 5 大实战经验
- 指标必须有,否则别上金丝雀——没指标的「渐进式发布」等于「盲切」,出问题不知道怎么回滚
- prometheus-adapter 必须装——Argo Rollouts 通过 ServiceMonitor 找 Prometheus
- canary-hash 标签自动注入——Rollout Controller 会给金丝雀 Pod 打
rollouts-pod-template-hash 标签,PromQL 用来区分版本 - 可以按 header 切(灰度测试)——设置
X-Canary: enabled header,流量强制走新版本,QA 验证用 - 失败立即 abort——
failureLimit: 2,不要等 5 次失败才反应(5 分钟已经太久了)
第八章总结:Argo Rollouts 是 K8s Deployment 的「超集」,金丝雀发布按 Prometheus 指标自动推进/回滚,5 阶段渐进(5% → 15% → 30% → 50% → 100%),Jenkinsfile 一行命令触发。
九、收尾:从 CI 到 GitOps 闭环
9.1 Argo CD 集成:让 CI 推 Git、CD 同步 K8s
到 2024 年,我们团队把流水线演进到 GitOps 闭环——CI 和 CD 完全解耦:
- Jenkins 责任:代码 → 构建 → 测试 → 扫描 → 推镜像
- Argo CD 责任:监听 Git 仓库 → 同步 K8s 状态
graph LR
Dev[开发者] -->|push| Git[Git 仓库
业务代码 + Helm Chart]
Git -->|webhook| Jen[Jenkins
CI 流水线]
Jen -->|build + scan| Harbor[Harbor 镜像库]
Jen -->|update image tag| Git
Argo[Argo CD
CD 控制器] -->|watch| Git
Argo -->|apply| K8s[K8s 集群]
style Jen fill:#3c82dc,color:#fff
style Argo fill:#ff9a3c,color:#fff3 大优势:
- K8s 状态可追溯——Git 是 single source of truth,所有部署都能从 Git 还原
- 一键回滚——
git revert 一次 commit,Argo CD 自动同步,回滚时间从 5 分钟缩到 30 秒 - 多环境标准化——dev / staging / prod 用同一份 Helm Chart + 不同 values,K8s 部署逻辑一致
Argo CD Application 模板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: order-service-prod
namespace: argocd
spec:
project: default
source:
repoURL: git@github.com:myteam/order-service.git
targetRevision: HEAD
path: charts/order-service
helm:
valueFiles:
- values-prod.yaml
destination:
server: https://kubernetes.default.svc
namespace: prod
syncPolicy:
automated:
prune: true # 自动清理孤儿资源
selfHeal: true # 自动修复偏离
syncOptions:
- CreateNamespace=true
|
Jenkinsfile 只需要 update Git,不再 apply K8s:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| stage('Update Git Manifest') {
agent { label 'maven' }
steps {
withCredentials([usernamePassword(
credentialsId: 'github-token',
usernameVariable: 'GIT_USER',
passwordVariable: 'GIT_TOKEN'
)]) {
sh """
# 1. 配置 git
git config user.email "jenkins@xxx.com"
git config user.name "Jenkins Bot"
# 2. 更新 Helm values 里的 image.tag
sed -i "s/tag: .*/tag: ${IMAGE_TAG}/" \
charts/order-service/values-prod.yaml
# 3. 提交 + push
git add charts/order-service/values-prod.yaml
git commit -m "ci: update image tag to ${IMAGE_TAG} [skip ci]"
git push https://${GIT_USER}:${GIT_TOKEN}@github.com/myteam/order-service.git
"""
}
}
}
|
Argo CD 自动监听 Git,30 秒内自动同步到 K8s。
Why GitOps 优于传统 Jenkins apply:
| 维度 | 传统 Jenkins | GitOps |
|---|
| K8s 状态源 | Jenkins 任务(临时) | Git(永久) |
| 一键回滚 | 重新跑 Jenkins job | git revert |
| 集群被改后修复 | 需要手动 sync | 自动 selfHeal |
| 多集群管理 | 需配多个 Jenkins job | 1 个 Argo App |
| 审计 | Jenkins 日志 | Git commit 历史 |
9.2 配套工具链 + 系列回顾
完整 CI/CD 工具链地图:
graph TB
subgraph Code[代码层]
Git[Git 仓库
GitHub / GitLab]
PR[PR / Code Review]
end
subgraph CI[CI 层 - Jenkins]
JM[Jenkins Master]
JA[Agents
maven / docker / k8s]
end
subgraph Quality[质量层]
Sonar[SonarQube
代码扫描]
JaCoCo[覆盖率]
end
subgraph Storage[制品层]
Harbor[Harbor
镜像仓库]
Nexus[Nexus
Maven 制品]
end
subgraph CD[CD 层]
Argo[Argo CD
GitOps]
Roll[Argo Rollouts
灰度发布]
Helm[Helm
Chart]
end
subgraph Runtime[运行时]
K8s[K8s 集群]
Prom[Prometheus]
Grafana[Grafana]
Loki[Loki]
end
Git --> JM
PR --> Git
JM --> Sonar
JM --> JaCoCo
JM --> Harbor
JM --> Nexus
JM --> Argo
Argo --> K8s
Roll --> K8s
Helm --> K8s
K8s --> Prom
Prom --> Grafana
K8s --> Loki
style JM fill:#3c82dc,color:#fff
style Argo fill:#ff9a3c,color:#fff
style K8s fill:#5aa0e8,color:#fff6 大配套工具:
| 工具 | 用途 | 部署位置 |
|---|
| Prometheus | 指标采集 + 告警 | K8s 内(prometheus-operator) |
| Grafana | 指标可视化 | K8s 内 |
| Loki | 日志聚合 | K8s 内 |
| AlertManager | 告警路由 | K8s 内 |
| Jaeger | 分布式追踪 | K8s 内(可选) |
| Argo CD | GitOps 控制器 | K8s 内(argocd namespace) |
监控流水线本身的指标:
- 构建成功率(按服务)—— 失败率 > 10% 告警
- 构建时长(P50 / P95 / P99)—— P95 > 30 分钟告警
- 镜像大小 —— 单镜像 > 1GB 告警
- Maven 依赖下载量 —— 单构建 > 2GB 告警
- Jenkins 队列长度 —— 等待 > 10 个 job 告警
系列回顾(Java Web 微服务 1-9 篇):
| 篇 | 主题 | 类型 |
|---|
| 1 | 异地多活:高可用终极形态 | 架构 |
| 2 | 反规范化设计:性能与一致性的平衡 | 架构 |
| 3 | K8s 容器编排 | 基础设施 |
| 4 | 技术选型:Spring Cloud Alibaba + Dubbo 3 | 架构 |
| 5 | Nacos:服务中心 + 配置中心 | 架构 |
| 6 | 流量调度:网关 + 限流 + 负载均衡 | 架构 |
| 7 | 熔断限流:Sentinel + Resilience4j | 架构 |
| 8 | 数据库演化:MySQL → 分布式 | 数据 |
| 9 | Jenkins 流水线:完整 CI/CD 实战 | 工程收官 |
| 后续 | 可观测性:Metrics / Logging / Tracing 三支柱 | 待写 |
一句话总结:
「Jenkins 不是银弹,Pipeline as Code + GitOps + 渐进式发布才是 2026 年 Java 微服务交付的答案。」
附录 A:常见问题 FAQ
Q1:Jenkins 2 已经够用,为什么不用 GitLab CI / GitHub Actions / Tekton?
A:Jenkins 在「插件生态」和「国内企业落地经验」上还是最强的。GitLab CI 适合 GitLab 仓库一体化的团队;GitHub Actions 适合开源项目;Tekton 是云原生未来但生态还弱。对绝大多数中大企业,Jenkins 仍是首选。
Q2:Jenkinsfile 必须放在项目根目录吗?
A:默认是。也可以放在子目录(如 cicd/Jenkinsfile),但 Jenkins Job 创建时要选「Pipeline script from SCM」并指定 Script Path。
Q3:build 出来的 jar 怎么找?
A:两种方式:
archiveArtifacts 归档(推荐)—— 在 Jenkins Job 页面「Build Artifacts」下载- 推 Nexus —— 用 Nexus Artifact Uploader 插件
Q4:生产环境怎么回滚到 3 个版本之前?
A:
- Helm:
helm rollback order-service 3(3 是 revision number) - Argo Rollouts:
kubectl argo rollouts undo order-service --to-revision=3 - 更推荐 GitOps:
git revert 对应 commit,Argo CD 自动同步
Q5:流水线跑一半挂了,重跑时跳过已经成功的 stage 怎么实现?
A:Jenkins 原生不支持 stage 级别 skip,两个变通方案:
- 每个 stage 加
when 条件判断参数 - 用 Pipeline Stage 视图手动「重跑此 stage」(Blue Ocean 插件支持)
Q6:Argo Rollouts 和 Istio VirtualService 怎么选?
A:
- 简单场景:用 Argo Rollouts + Nginx Ingress(本文方案)
- 复杂服务网格:用 Istio VirtualService(更强大但更重)
- 多协议支持:用 SMI(Service Mesh Interface)
附录 B:Jenkins 性能调优清单
如果你遇到 Jenkins 慢/卡/OOM,按这个清单逐项排查:
| 现象 | 根因 | 优化 |
|---|
| 早高峰 job 排队 | Agent 数量不足 | 加 maven / docker agent |
| Master 内存持续上涨 | 插件内存泄漏 | 升级插件 / 减插件 / 调大 -Xmx |
| 第一次构建 5 分钟 | 镜像拉取慢 | 预热基础镜像 / 用 k3d image import |
| Maven 下载慢 | 无本地缓存 | 挂 .m2 目录到 host |
| Docker build 慢 | 镜像没缓存 | 优化 Dockerfile 层级 / 用 BuildKit |
| Webhook 不触发 | 网络/防火墙 | 检查 tcpdump + Jenkins Manage Webhooks 日志 |
| 制品归档慢 | 归档大文件 | 不归档 target/ 整个目录,只归档 jar |
| SonarQube 扫描慢 | 仓库太大 | 拆分项目 / 增量扫描 |
| 磁盘满 | 工作区残留 | 装 Workspace Cleanup 插件 + 定时清理 |
JVM 调优(生产 Master):
1
2
| # 8G 内存的 Master
export JAVA_OPTS="-Xms4g -Xmx6g -XX:MaxMetaspaceSize=1g -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
|
Master 高可用(生产级):
- 2 个 Master 节点共享 NFS 卷
- 前面挂 HAProxy / Nginx 做 active-passive
- 共享存储用 GlusterFS / Ceph
第九章总结 + 系列收官:Jenkins Pipeline + GitOps + 渐进式发布,从代码提交到金丝雀 100% 全自动。配套 Prometheus + Grafana + Loki + Argo CD,完整可观测 + 完整可回滚。这是 Java 微服务从「架构」走向「生产」的最后一公里。
下一篇文章将进入可观测性专题——Metrics / Logging / Tracing 三支柱怎么落地,敬请期待。