Skip to content

CI/CD 流水线设计 ​

S4-028建议学高级约 90 分钟JDK 21企业开发进阶 · CI/CD

1. 本节定位 ​

S4 的最后一节。前面讲了微服务(《微服务架构与拆分原则》)、容器(《Docker 基础与镜像容器》)、 K8s(《Kubernetes 入门》);本节讲**"怎么把它们串起来自动化"**。

★ 流水线的目标只有一个 ✓✓:

text
让「发现问题」的时间尽可能短 ✓

★ 本节的实测用真实数据量出三个最有效的优化 ✓✓:

text
① 并行化:8 阶段从 345 秒降到 290 秒
② 缓存:本项目自己的流水线从 149.9 秒降到 111.6 秒(省 26%)✓✓
③ 阶段顺序:把静态检查放最前,低级错误的发现时间从 320 秒降到 25 秒 ✓✓

2. 学习目标 ​

  • 能说清"流水线慢"的真正危害(它会改变团队的工作方式) ★★★;
  • 知道优化方向是"缩短关键路径"而不是"让每阶段都快一点" ★★★;
  • 能说出三层缓存与"缓存 key 必须含依赖文件" ★★★;
  • 知道阶段要按"发现问题的成本"从低到高排 ★★★;
  • 能背出四条原则,并知道"可回滚"最重要 ★★★;
  • 知道 CI 里必须"总是重新构建" ★★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看"流水线慢"的真正危害 ✗✗:

text
8 个阶段全串行 = 345 秒(约 6 分钟)
  · 开发者提交后要等 6 分钟才知道结果 ✗
  · 如果失败,改一行再等一轮 → **一天只能迭代几次** ✗✗
  · 于是大家开始"减少提交频率" → **最后变成一周合并一次** ✗✗

★★ 这才是"流水线慢"最致命的地方 ✓✓:

它会改变团队的工作方式 —— 从"小步快跑"退化成"大爆炸式合并" ✗✗

而"大爆炸合并"意味着冲突多、审查难、风险高 ✓

5. 核心概念 ​

5.1 关键路径:优化方向不是"让每阶段都快" ★★★ ​

实测(一个典型 8 阶段流水线) ✓:

text
  静态检查(Checkstyle/SpotBugs)   25 s   ← 无依赖
  单元测试                          45 s   ← 无依赖
  集成测试(含容器启动)             90 s   ← 依赖单元测试
  安全扫描(依赖漏洞)               30 s   ← 无依赖
  构建镜像                          60 s   ← 依赖单元测试、集成测试
  推送镜像                          20 s   ← 依赖构建镜像
  部署测试环境                      40 s   ← 依赖推送镜像
  冒烟测试                          35 s   ← 依赖部署

  ① 全串行:25+45+90+30+60+20+40+35 = **345 秒** ✗
  ② 并行化(静态检查 / 单元测试 / 安全扫描 同时跑):
     max(25,45,30) + 90 + 60 + 20 + 40 + 35 = **290 秒** ✓✓(省 16%)

★★ 一条方法论 ✓✓:

流水线的耗时 = 关键路径的长度,不是所有阶段之和 ✓✓

所以优化的方向是缩小关键路径,而不是"让每个阶段都快一点" ✓✓

★ 哪三类阶段最容易并行 ✓:

类别例子
互不依赖的检查 ★★★静态检查 / 安全扫描 / License 检查 ✓✓
多模块测试 ★★★Maven 多模块 -T 4(本项目 22 个模块)✓✓
多环境部署 ★★测试环境与预发环境同时部署 ✓

★ 一条"不要做" ✗:

不要为了并行而拆掉依赖关系 ——

"集成测试依赖单元测试"是有道理的(单元测试不过就别跑集成)✓

强行并行只会浪费机器时间在注定失败的阶段上 ✗✗

5.2 缓存:本项目自己的真实数据 ★★★ ​

实测(本项目 scripts/pipeline.ps1) ✓✓:

text
  第一次跑(Maven 依赖未下载):
    L1 登记表与结构校验      3.4 s
    L2 快速单元测试         65.9 s
    L3 慢测试(Spring)     80.6 s
    合计                    **149.9 s** ✗

  依赖已缓存(本地仓库已有依赖):
    L1 登记表与结构校验      2.5 s
    L2 快速单元测试         37.2 s
    L3 慢测试(Spring)     71.9 s
    合计                    **111.6 s** ✓✓(省 26%)

★ 缓存的三层(CI 里都要配) ✓✓:

缓存什么怎么配省下什么
依赖仓库 ★★★~/.m2、~/.npm、~/.gradle下载时间(最大头) ✓✓
构建产物 ★★target/、编译结果重新编译时间 ✓
Docker 层 ★★★buildx cache、registry cache镜像构建(《Docker 基础与镜像容器》)✓✓

★ 一条经验值 ✓:缓存命中时,流水线通常能快 2~5 倍 —— 而配置成本只是几十行 YAML ✓✓(投入产出比最高的优化之一)

★★ 但缓存有两个坑 ✓✓:

#坑后果
①缓存 key 不含依赖描述文件 ✗✗依赖变了缓存没变 → 用旧依赖构建(极难排查)✓✓
②缓存无法失效 ✗依赖出问题时没法强制刷新 ✓

★ 正确写法 ✓:

yaml
- uses: actions/cache@v4
  with:
    path: ~/.m2
    key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}   # ★ key 含依赖文件

★★ 本项目踩过的同类坑 ✓✓:"页面改了但跑的是旧 jar" ✗ —— 所以 scripts/ui-test.ps1 里写死了"总是重新打包" ✓✓

5.3 阶段顺序:按"发现问题的成本"排序 ★★★ ​

★ 场景:这次提交有一个低级错误(代码风格不合规)✓:

阶段顺序发现耗时评价
静态检查放最后 ✗✗320 秒等 5 分钟才知道少个分号 ✗✗
静态检查放最前 ✓✓25 秒半分钟内反馈 ✓✓

★★ 一条排序原则 ✓✓:

按「发现问题的成本」从低到高排 ✓✓

text
静态检查(秒级)→ 单元测试(分钟级)→ 集成测试 → 部署 → 冒烟

★ 失败即停(fail-fast)的取舍 ✓:

策略优点缺点
失败即停 ✓✓反馈最快、不浪费机器看不到后续阶段是否也有问题
全量运行 ⚠️一次拿到全部问题慢、消耗更多资源

★ 折中方案(推荐) ✓✓:

场景策略
快速阶段(静态检查、单元测试)★★★失败即停 ✓✓
慢速且昂贵(安全扫描)★可以"允许失败"(先只告警)✓
分支策略 ★★★主线严格、特性分支宽松(PR 跑全量,push 跑核心)✓✓

★★ 一条重要的"团队心理"经验 ✓✓:

如果流水线经常因为"不是代码问题"而失败,大家就会开始忽略它 ✗✗

所以:不稳定(flaky)的测试要么修,要么移出阻塞门禁 ✓✓

(呼应 T2-011:重试是止痛药,不是解药)✓

5.4 四条原则与质量门禁 ★★★ ​

★ 四条原则(按重要性排序) ✓✓:

原则含义不做的后果
可回滚 ★★★任何一次发布都能一键退回出事故时只能手忙脚乱 ✗✗
可重复 ★★★同样输入 → 同样输出"在我这能跑" ✗✗
快反馈 ★★★常见错误在 1 分钟内暴露提交频率下降 ✗
有门禁 ★★不合规的代码进不了主干质量靠自觉 ✗

★★★ 如果只能做好一件事,选"可回滚" ✓✓:

因为:发布出问题的概率永远不是零 ✗

· 有回滚 → 事故是"几分钟的抖动" ✓

· 没回滚 → 事故是"通宵排查 + 手改数据库" ✗✗

(对应 K8s 的 kubectl rollout undo,《Kubernetes 入门》)✓✓

★ 四类质量门禁 ✓:

类别典型门禁
代码质量 ★★静态检查通过、测试覆盖率不下降 ✓
测试 ★★★单元/集成/冒烟全绿 ✓✓
安全 ★★★依赖漏洞(高危必修)、镜像扫描、密钥不进 Git ✓✓
发布 ★★★能回滚、数据库变更可逆或已备份、有灰度方案 ✓✓

5.5 本项目的实践(可作参考)★★★ ​

★ 三层次分开跑 ✓✓:

层内容耗时
L1登记表与结构校验2.5 s ✓✓
L2快速单元测试(纯 Java 模块)37.2 s ✓
L3慢测试(Spring 模块:切片 + 集成)71.9 s ⚠️

★★ 三条设计要点 ✓✓:

#要点
①L1 最快、最先跑(它失败说明登记表与文件不一致,不需要跑测试)✓✓
②L2 与 L3 按"是否依赖 Spring 上下文"分层(上下文启动要几秒,不该混进快测试)✓✓
③UI 测试用 profile 隔离(默认不跑,因为要 168 秒)✓ ——见 .codebuddy/rules/build-and-verify.md 约束 6.2

★★★ 一条最有价值的实践 ✓✓:

把流水线写成脚本,CI 和本地跑的是同一份 ✓✓

本项目:scripts/pipeline.ps1 —— 本地和 CI 都跑它 ✓✓

好处:避免"CI 上才失败"的玄学问题(本地能复现)✓✓

6. 递进示例 ​

6.1 最小示例:GitHub Actions 的并行与缓存 ​

yaml
jobs:
  checks:                                  # 并行组:两个检查同时跑
    strategy:
      matrix:
        task: [lint, security-scan]        # ★ 矩阵并行
    steps:
      - uses: actions/cache@v4
        with:
          path: ~/.m2
          key: ${{ hashFiles('**/pom.xml') }}    # ★ key 含依赖文件
      - run: ./mvnw -B -ntp test

  deploy:
    needs: [tests]                         # ★ 依赖关系
    if: github.ref == 'refs/heads/main'    # ★ 只有主分支才部署

6.2 业务示例:可回滚的发布 ​

bash
# ① 构建带版本号的镜像(不用 latest)✓
docker build -t registry/order-service:1.2.4 .
# ② 部署
kubectl set image deploy/order-service app=registry/order-service:1.2.4
# ③ 观察
kubectl rollout status deploy/order-service
# ④ 出问题 → 一秒回滚 ✓✓
kubectl rollout undo deploy/order-service

6.3 反例:把"上次的产物"缓存起来 ​

yaml
# ✗ 危险:缓存了构建产物 → 代码改了却用了旧 jar
- uses: actions/cache@v4
  with:
    path: target/          # ✗ 绝不能缓存"输出目录"

★ 本项目就踩过这个坑 ✓:页面代码改了,但 UI 测试跑的是旧 jar,测试报 404 ✗✗

7. 完整可运行代码与实测 ​

代码:labs/distributed-lab/src/main/java/cn/kb/learn/dist/cicd/PipelineDesignLesson.java

运行:

powershell
cd labs
.\mvnw.cmd -pl distributed-lab compile
java "-Dstdout.encoding=UTF-8" -cp distributed-lab/target/classes cn.kb.learn.dist.cicd.PipelineDesignLesson

★ 本实验计算三件事 ✓:

实验量出什么
全串行基线8 阶段 345 秒 ✓
并行化290 秒(省 16%),且说明"关键路径"这个概念 ✓✓
缓存效果用本项目真实数据:149.9 s → 111.6 s ✓✓

★ 三条可落地的结论 ✓:

#结论依据
①优化关键路径,不是优化每个阶段并行省 16% ✓
②缓存是投入产出比最高的优化省 26% ✓✓
③阶段顺序决定"发现有多快"320 s → 25 s ✓✓

8. Debug 观察点 ​

  1. 统计每个阶段的耗时:找出关键路径(最长的链)✓✓;
  2. 统计"每次提交到反馈"的时间:超过 10 分钟就该优化 ⚠️;
  3. 统计流水线的失败率与"非代码原因"失败的比例:后者高说明流水线不可信 ✗✗;
  4. 把缓存去掉再跑一次:量出缓存的真实收益 ✓;
  5. 试试"本地跑一遍流水线":能不能复现 CI 的失败 ✓✓。

9. 常见错误 ​

#错误说明
1缓存构建产物(target/) ✗✗用旧产物构建(本项目踩过)✓✓
2缓存 key 不含依赖文件 ✗✗依赖变了用旧的 ✓
3把慢检查放最前 ✗反馈变慢 ✓
4为了并行拆掉真实依赖 ✗浪费机器 ✓
5忽视 flaky 测试 ✗✗大家开始忽略流水线 ✓✓
6没有回滚能力 ✗✗事故 = 通宵 ✓✓
7CI 与本地跑的脚本不一样 ✗✗"CI 上才失败"的玄学 ✓

10. 动手实验 ​

目标:给自己项目的流水线做一次"体检 + 优化"。

步骤:

  1. 跑 PipelineDesignLesson,看懂"关键路径"这个概念 ✓✓;
  2. 统计你项目流水线的每个阶段耗时,画出关键路径 ★★★;
  3. 把能并行的阶段并行,量出优化前后的总时长 ✓;
  4. 加依赖缓存(key 含 pom.xml),量出收益 ✓✓;
  5. 检查"回滚能力":能不能在 1 分钟内把上一次发布退回去 ★★★。

验收方式:能说出"你的流水线关键路径是哪条、最该优化的三点是什么"。

扩展任务:设计一个"特性分支 vs 主线"的两套流水线:

  • 特性分支(PR):快速检查 + 单元测试(3 分钟内)✓✓;
  • 主线合并:全量(含集成、UI、镜像构建)✓;
  • 发布分支:加安全扫描与灰度 ✓;
  • 说明:为什么"两套流水线"比"一套全都跑"更好(提示:反馈速度 vs 覆盖度的权衡)★★

11. 练习题 ​

  1. 判断:流水线的耗时等于所有阶段耗时之和。(对 / 错)
  2. 填空:四条原则是 、、、。
  3. 简答:为什么"缓存构建产物(target/)"是危险的?
  4. (配置阅读)
yaml
- uses: actions/cache@v4
  with:
    path: |
      ~/.m2
      target/
    key: build-cache          # 固定 key

这段配置有哪两个问题? 5. (设计)给一个"一天合并 20 次 PR"的团队设计流水线。

参考答案(要点)
  1. 错。等于关键路径的长度 ✗ ——并行阶段取最长的那条 ✓✓★
  2. 可回滚;可重复;快反馈;有门禁 ✓
  3. 因为它是"输出"而不是"输入" ✗ —— 缓存它会导致代码改了却用旧产物构建(本项目踩过:页面改了但跑的是旧 jar)✗✗★
  4. ① 缓存了 target/(输出目录) ✗✗;② key 固定为 build-cache,不含依赖文件 ✗ —— 依赖变了不会失效 ✓;正确做法:只缓存 ~/.m2,key 用 hashFiles('**/pom.xml') ✓✓
  5. 要点:PR 上跑"快检查"(< 3 分钟:静态检查 + 单元测试) ✓✓; 合并到主线后跑全量(集成 + UI + 镜像 + 部署测试环境)✓; 并行化 + 依赖缓存(缩短关键路径)✓✓; 必须有回滚(发布失败能立刻退回)✓✓; flaky 测试移出阻塞门禁(保证流水线可信)✓

12. 掌握标准 ​

  • [ ] 能说清"流水线慢"会改变团队工作方式 ★★★;
  • [ ] 知道优化方向是缩小关键路径 ★★★;
  • [ ] 能说出三层缓存与"key 必含依赖文件" ★★★;
  • [ ] 知道阶段要按"发现成本"从低到高排 ★★★;
  • [ ] 能背出四条原则,并知道"可回滚"最重要 ★★★;
  • [ ] 知道绝不能缓存构建产物 ★★★;
  • [ ] 知道 CI 与本地要跑同一份脚本 ★★★。

13. 扩展资源 ​

14. 下一步学习 ​

S4 阶段到此完成(28/28) ✓✓。 下一步进入 S5 源码与原理,从 JDK 集合的源码开始。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引