主题
界面
主题
S4 的最后一节。前面讲了微服务(《微服务架构与拆分原则》)、容器(《Docker 基础与镜像容器》)、 K8s(《Kubernetes 入门》);本节讲**"怎么把它们串起来自动化"**。
★ 流水线的目标只有一个 ✓✓:
让「发现问题」的时间尽可能短 ✓★ 本节的实测用真实数据量出三个最有效的优化 ✓✓:
① 并行化:8 阶段从 345 秒降到 290 秒
② 缓存:本项目自己的流水线从 149.9 秒降到 111.6 秒(省 26%)✓✓
③ 阶段顺序:把静态检查放最前,低级错误的发现时间从 320 秒降到 25 秒 ✓✓先看"流水线慢"的真正危害 ✗✗:
8 个阶段全串行 = 345 秒(约 6 分钟)
· 开发者提交后要等 6 分钟才知道结果 ✗
· 如果失败,改一行再等一轮 → **一天只能迭代几次** ✗✗
· 于是大家开始"减少提交频率" → **最后变成一周合并一次** ✗✗★★ 这才是"流水线慢"最致命的地方 ✓✓:
它会改变团队的工作方式 —— 从"小步快跑"退化成"大爆炸式合并" ✗✗
而"大爆炸合并"意味着冲突多、审查难、风险高 ✓
实测(一个典型 8 阶段流水线) ✓:
静态检查(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 个模块)✓✓ |
| 多环境部署 ★★ | 测试环境与预发环境同时部署 ✓ |
★ 一条"不要做" ✗:
不要为了并行而拆掉依赖关系 ——
"集成测试依赖单元测试"是有道理的(单元测试不过就别跑集成)✓
强行并行只会浪费机器时间在注定失败的阶段上 ✗✗
实测(本项目 scripts/pipeline.ps1) ✓✓:
第一次跑(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 不含依赖描述文件 ✗✗ | 依赖变了缓存没变 → 用旧依赖构建(极难排查)✓✓ |
| ② | 缓存无法失效 ✗ | 依赖出问题时没法强制刷新 ✓ |
★ 正确写法 ✓:
- uses: actions/cache@v4
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} # ★ key 含依赖文件★★ 本项目踩过的同类坑 ✓✓:"页面改了但跑的是旧 jar" ✗ —— 所以 scripts/ui-test.ps1 里写死了"总是重新打包" ✓✓
★ 场景:这次提交有一个低级错误(代码风格不合规)✓:
| 阶段顺序 | 发现耗时 | 评价 |
|---|---|---|
| 静态检查放最后 ✗✗ | 320 秒 | 等 5 分钟才知道少个分号 ✗✗ |
| 静态检查放最前 ✓✓ | 25 秒 | 半分钟内反馈 ✓✓ |
★★ 一条排序原则 ✓✓:
按「发现问题的成本」从低到高排 ✓✓
text静态检查(秒级)→ 单元测试(分钟级)→ 集成测试 → 部署 → 冒烟1
★ 失败即停(fail-fast)的取舍 ✓:
| 策略 | 优点 | 缺点 |
|---|---|---|
| 失败即停 ✓✓ | 反馈最快、不浪费机器 | 看不到后续阶段是否也有问题 |
| 全量运行 ⚠️ | 一次拿到全部问题 | 慢、消耗更多资源 |
★ 折中方案(推荐) ✓✓:
| 场景 | 策略 |
|---|---|
| 快速阶段(静态检查、单元测试)★★★ | 失败即停 ✓✓ |
| 慢速且昂贵(安全扫描)★ | 可以"允许失败"(先只告警)✓ |
| 分支策略 ★★★ | 主线严格、特性分支宽松(PR 跑全量,push 跑核心)✓✓ |
★★ 一条重要的"团队心理"经验 ✓✓:
如果流水线经常因为"不是代码问题"而失败,大家就会开始忽略它 ✗✗
所以:不稳定(flaky)的测试要么修,要么移出阻塞门禁 ✓✓
(呼应 T2-011:重试是止痛药,不是解药)✓
★ 四条原则(按重要性排序) ✓✓:
| 原则 | 含义 | 不做的后果 |
|---|---|---|
| 可回滚 ★★★ | 任何一次发布都能一键退回 | 出事故时只能手忙脚乱 ✗✗ |
| 可重复 ★★★ | 同样输入 → 同样输出 | "在我这能跑" ✗✗ |
| 快反馈 ★★★ | 常见错误在 1 分钟内暴露 | 提交频率下降 ✗ |
| 有门禁 ★★ | 不合规的代码进不了主干 | 质量靠自觉 ✗ |
★★★ 如果只能做好一件事,选"可回滚" ✓✓:
因为:发布出问题的概率永远不是零 ✗
· 有回滚 → 事故是"几分钟的抖动" ✓
· 没回滚 → 事故是"通宵排查 + 手改数据库" ✗✗
(对应 K8s 的
kubectl rollout undo,《Kubernetes 入门》)✓✓
★ 四类质量门禁 ✓:
| 类别 | 典型门禁 |
|---|---|
| 代码质量 ★★ | 静态检查通过、测试覆盖率不下降 ✓ |
| 测试 ★★★ | 单元/集成/冒烟全绿 ✓✓ |
| 安全 ★★★ | 依赖漏洞(高危必修)、镜像扫描、密钥不进 Git ✓✓ |
| 发布 ★★★ | 能回滚、数据库变更可逆或已备份、有灰度方案 ✓✓ |
★ 三层次分开跑 ✓✓:
| 层 | 内容 | 耗时 |
|---|---|---|
| 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 上才失败"的玄学问题(本地能复现)✓✓
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' # ★ 只有主分支才部署# ① 构建带版本号的镜像(不用 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# ✗ 危险:缓存了构建产物 → 代码改了却用了旧 jar
- uses: actions/cache@v4
with:
path: target/ # ✗ 绝不能缓存"输出目录"★ 本项目就踩过这个坑 ✓:页面代码改了,但 UI 测试跑的是旧 jar,测试报 404 ✗✗
代码:labs/distributed-lab/src/main/java/cn/kb/learn/dist/cicd/PipelineDesignLesson.java
运行:
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 ✓✓ |
| # | 错误 | 说明 |
|---|---|---|
| 1 | 缓存构建产物(target/) ✗✗ | 用旧产物构建(本项目踩过)✓✓ |
| 2 | 缓存 key 不含依赖文件 ✗✗ | 依赖变了用旧的 ✓ |
| 3 | 把慢检查放最前 ✗ | 反馈变慢 ✓ |
| 4 | 为了并行拆掉真实依赖 ✗ | 浪费机器 ✓ |
| 5 | 忽视 flaky 测试 ✗✗ | 大家开始忽略流水线 ✓✓ |
| 6 | 没有回滚能力 ✗✗ | 事故 = 通宵 ✓✓ |
| 7 | CI 与本地跑的脚本不一样 ✗✗ | "CI 上才失败"的玄学 ✓ |
目标:给自己项目的流水线做一次"体检 + 优化"。
步骤:
PipelineDesignLesson,看懂"关键路径"这个概念 ✓✓;pom.xml),量出收益 ✓✓;验收方式:能说出"你的流水线关键路径是哪条、最该优化的三点是什么"。
扩展任务:设计一个"特性分支 vs 主线"的两套流水线:
- uses: actions/cache@v4
with:
path: |
~/.m2
target/
key: build-cache # 固定 key这段配置有哪两个问题? 5. (设计)给一个"一天合并 20 次 PR"的团队设计流水线。
target/(输出目录) ✗✗;② key 固定为 build-cache,不含依赖文件 ✗ —— 依赖变了不会失效 ✓;正确做法:只缓存 ~/.m2,key 用 hashFiles('**/pom.xml') ✓✓S4 阶段到此完成(28/28) ✓✓。 下一步进入 S5 源码与原理,从 JDK 集合的源码开始。
暂无