09-CI 核心概念
CI(Continuous Integration,持续集成)是 DevOps 工具链的核心环节。本章不讲具体工具,讲 CI 的概念框架:为什么需要 CI、流水线的标准结构、缓存与并行、触发策略、以及一个完整 CI 闭环的六个环节。
集成地狱#
没有 CI 的团队会陷入"集成地狱":
开发 A 在自己分支写了 2 周
开发 B 在自己分支写了 2 周
开发 C 在自己分支写了 3 周
│
▼ 项目要上线了,开始合并
│
合并冲突像滚雪球
│
解决冲突用了 3 天
│
合完后测试挂了一半
│
修复又用了 2 天
│
上线延期一周这是"集成频率太低"的代价。Martin Fowler 的说法:集成痛苦总量是固定的,越晚集成单次痛苦越大。每天集成 10 次小冲突,远比 2 周后集成 1 次大冲突轻松。
CI 的核心思想就是高频集成 + 自动化验证:每次代码提交都自动跑构建 + 测试 + 扫描,让问题在"刚写完 5 分钟"时就被发现,而不是"2 周后合并时"。
流水线五阶段#
一条完整的 CI 流水线通常有五个阶段,从代码到报告:
① 代码 ──► ② 构建 ──► ③ 测试 ──► ④ 扫描 ──► ⑤ 报告
checkout build test scan report① 代码(Checkout)#
从仓库拉取代码。关键点:
- 浅克隆加速:默认 clone 拉全部历史,CI 只需要最新代码时用
--depth 1 - 完整历史场景:semantic-release、release-please 这类工具要算版本号,需要
fetch-depth: 0 - 分支策略:CI 通常只在 main/PR 触发完整流水线,feature 分支可只跑 lint+test
- submodule 处理:如果仓库有 submodule,CI 要加
--recurse-submodules或在配置里开启 submodule 拉取
② 构建(Build)#
把源码 + 依赖封装成可运行产物。对容器化应用,这步就是 docker build。关键点:
- 多阶段构建:构建时需要的工具链不进最终镜像,大幅减小镜像体积(详见 14 章)
- 构建缓存:层缓存、BuildKit cache mount、registry cache(详见 14 章)
- 多架构:amd64 + arm64 双架构镜像
- 多 registry 推送:海外 ECR + 国内 ACR 双推
③ 测试(Test)#
自动验证代码行为正确。层级:
- 单元测试:函数级别,秒级完成
- 集成测试:模块间协作,分钟级
- 端到端测试(E2E):完整流程,分钟级到小时级
# 典型测试 stage
test:
stage: test
script:
- go test -race -coverprofile=coverage.out ./...
- go tool cover -func=coverage.out | tail -1
coverage: '/total:\s+\(statements\)\s+(\d+\.\d+)%/'④ 扫描(Scan)#
在 CI 阶段把质量问题拦在合并前:
- 代码质量:SonarQube、CodeClimate
- Lint/格式化:golangci-lint、ESLint、black
- 依赖漏洞(SCA):Trivy、Snyk 扫描依赖中的 CVE
- 镜像漏洞:Trivy 扫描构建出的镜像
- 敏感信息:gitleaks 扫描代码里的密钥泄漏
- Schema 检查:DB schema diff(详见 13 章)
⑤ 报告(Report)#
把测试覆盖率、扫描结果、构建产物收集起来。关键点:
- artifact 传递:构建产物传给下游 stage(如测试报告)
- 报告可视化:覆盖率进 SonarQube,漏洞进 GitHub Security tab
- 通知:钉钉/Slack 通知流水线结果
报告 stage 常被忽视,但它是"让 CI 价值可见"的关键。没有报告,覆盖率数字只在日志里看一眼就丢;有了报告,覆盖率趋势能在 Grafana 看板里持续追踪,漏洞能在 GitHub Security tab 里集中管理。
流水线的失败处理#
CI 流水线的失败处理策略直接影响开发者体验:
- fail-fast:一个 job 失败立即取消其他并行 job。适合"确定要修就趁早"的场景
- fail-slow(
fail-fast: false):让所有 job 跑完再报失败。适合"想一次看完所有问题"的场景,如矩阵构建测试多版本兼容性 - allow_failure:某个 job 失败不影响整体状态。适合"警告但不阻塞"的检查(如可选的代码质量检查)
# GitHub Actions 矩阵不 fail-fast
strategy:
fail-fast: false
matrix:
go-version: ["1.21", "1.22", "1.23"]构建缓存与并行#
CI 速度是开发者体验的核心。两个杠杆:缓存和并行。
构建缓存#
缓存的核心思想:如果输入没变,输出可以复用。
# Go modules 缓存示例
cache:
key: "$CI_PROJECT_NAME-go-modules"
paths:
- .go/pkg/mod/
- .go/cache/Docker 镜像构建的缓存有两层:
- 层缓存:Dockerfile 每条指令一层,前面所有层命中缓存时这一层才复用。改了
COPY . .这层,后面的RUN go build会重建;但前面的COPY go.mod不变,RUN go mod download会复用。 - BuildKit cache mount:
--mount=type=cache让包管理器缓存(pip、npm、go mod)跨构建持久化,不进镜像层。
# BuildKit cache mount
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o /app/server ./cmd/server并行#
流水线里独立的部分应该并行执行。三种并行模式:
Job 级并行:同 stage 内多个 job 自动并行跑(GitLab CI 里就是几个 job 声明同一个 stage)。
unit-test:
stage: test
script: [go test ./...]
integration-test:
stage: test
script: [go test -tags=integration ./...]
e2e-test:
stage: test
script: [./e2e.sh]
# 三个 job 同属 test stage,自动并行矩阵构建:同一 job 在多环境并行。
test:
strategy:
matrix:
go-version: ["1.21", "1.22", "1.23"]
os: [ubuntu-latest, macos-latest]
# 6 个 job 并行跑Stage 间串行 + Stage 内并行:流水线整体是 stage 串行(test → build → deploy),每个 stage 内的 job 并行。这是大多数 CI 系统的默认模型。
触发策略#
什么时候触发流水线?四种主流触发方式:
| 触发方式 | 时机 | 典型用途 |
|---|---|---|
| push | 推到指定分支 | main 分支自动构建发版 |
| PR/MR | 创建或更新 PR/MR | 合并前跑完整检查 |
| 定时(schedule) | cron 定时 | 每晚跑 E2E、每周扫漏洞 |
| 路径过滤 | 只在特定路径变更时触发 | Monorepo 只构建改动的服务 |
| 手动(manual) | 手动点击 | PROD 部署需要人工触发 |
路径过滤(Monorepo 必备)#
# GitHub Actions 路径过滤
on:
pull_request:
paths:
- 'services/user-service/**'
- 'libs/auth-lib/**'
- '.github/workflows/user-service.yml'
# GitLab CI rules
build-user-service:
rules:
- changes:
- services/user-service/**/*
- libs/auth-lib/**/*分支策略#
# 只在 main 分支触发完整流水线
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
# PR 触发检查但不部署
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
# tag 触发发版
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'从代码到生产:CI 闭环六环节#
把 CI 放在整个 CICD 闭环里看,完整链路是六个环节:
①代码仓库 → ②触发 → ③构建(CI) → ④制品(镜像) → ⑤部署声明 → ⑥CD收敛(GitOps) → 线上
repo trigger build artifact manifest reconcile
▲ │
└──────────────────── 回滚 = revert 声明 + 重新收敛 ───────────┘① 仓库(Repo)#
代码组织方式(Monorepo vs Multirepo)+ 平台选型 + 分支模型(GitHub Flow 等)。详见前面章节。
仓库选型直接影响后续所有环节。Monorepo 改一处全局可见、原子提交、统一 CI,但权限/构建粒度粗,需要路径过滤触发;Polyrepo(一服务一仓)隔离清晰但跨服务改动要多 PR。GitOps 声明仓常单独一个——声明与业务代码分离,机器人回写 newTag 不污染业务仓历史。
分支模型上,main 受保护 + squash merge 是主流基线。squash merge 让主线历史线性可信,一个 PR 对应一个 commit,便于 revert 和 cherry-pick。
② 触发(Trigger)#
push / PR / manual / schedule / API,配合 branchesFilter 控制只让该触发的流水线触发。生产环境部署通常要 manual 审批。
触发策略的关键工程细节:
- branchesFilter 必须匹配 default branch:仓库默认分支是
master而 yaml 里写main,会静默不触发——这是所有"基于代码源触发"工具(云效、Jenkins multi-branch、GitLab CI)的共同坑 - 同仓多流水线要双重隔离:
branchesFilter+triggerFilter.exclude防止互相误触发 - PR 触发只跑无 secret 的 stage:CI 用默认的
pull_request事件即可——fork PR 天然拿不到 secrets(GITHUB_TOKEN 只读)。pull_request_target恰恰相反,它带着 base 仓库的写权限和 secrets 运行,只在确需 secrets(如自动打标签)时才用,且绝不 checkout 执行 PR 里的不可信代码,并加仓库归属检查 - cloneDepth 注意:semantic-release/changesets 这类工具要算版本号,需要
fetch-depth: 0拉完整历史;纯构建用--depth 1浅克隆加速
③ 构建(Build)#
源码 → 不可变制品(镜像)。关键原则:
- 一次构建,多次部署:同一个镜像 tag 从 QA → PRE → PROD,不重新构建。这是避免"测的不是发的"的根本措施。
- 构建即测试左移:单测、lint、安全扫描、schema 检查都在这阶段拦,越早拦越便宜。
构建阶段还要处理多架构(amd64 + arm64)和多 registry 推送(海外 ECR + 国内 ACR 双推)。双推的关键不是"推两次"而是"一次构建双推"——用同一个 Dockerfile、同一个 buildArgs,构建一次后 docker push 到两个 registry,保证字节级一致。CN 构建机如果在国内访问不了 Docker Hub,基础镜像要走私有 registry,包管理器走国内 mirror(npm/pip/goproxy)。
④ 制品(Artifact)#
镜像 tag 策略:{commit短hash}-{timestamp},全环境复用。绝不用 latest——不可溯源、不可回滚。
build ──► 镜像 abc123-2026-06-09-10-30 ──┬─► [QA] 自动部署
├─► [PRE] 人工审批后部署
└─► [PROD] 审批后部署
✓ 测的镜像 = 发的镜像(字节级一致)
✗ 反模式:每个环境各 build 一次⑤ 部署声明(Manifest)#
声明式 vs 命令式:
- 命令式(
kubectl apply -f):无单一事实源、漂移难查 - 声明式(GitOps):期望状态写进 Git,系统负责收敛
Kustomize 的 base + overlay 分层是多环境管理的标准方案:
base/{服务}/ overlay: clusters/{env}/applications/...
┌──────────────────────┐ ┌───────────────────────────────────────┐
│ deployment.yaml │ │ kustomization.yaml: │
│ service.yaml │◄──引用─│ resources: [../../base/.../服务] │
│ kustomization.yaml │ │ images: [{name:.., newTag: <commit>}] │
└──────────────────────┘ │ patches: [副本数/配额/...] │
└───────────────────────────────────────┘Kustomize 的关键能力:namePrefix/labels(批量加前缀/标签)、images(换 tag/registry)、patches(strategic-merge / json6902 精确改字段)、generators(ConfigMap/Secret 生成)。为什么 Kustomize 不用 Helm:Helm 是模板语言({{ }})+ values,灵活但易写出不可读的逻辑;Kustomize 是"拿现成 yaml 打补丁",无模板语言、更声明式、K8s 原生(kubectl -k),多环境 overlay 场景更清爽。复杂参数化/分发第三方 chart 才用 Helm。
ApplicationSet Generator 让"新服务建目录即被纳管"——Matrix(clusters × git directories)自动为每个集群每个服务目录生成一个 ArgoCD Application,目录即配置。
⑥ CD 收敛(GitOps)#
Pull-based(ArgoCD/Flux,主流)vs Push-based(流水线直接 kubectl apply):
- Pull 模式:CD 控制器在集群内主动拉 Git diff 收敛。好处=可审计、可回滚(revert commit)、防漂移、凭据不外泄。
- Push 模式:流水线直接 kubectl apply。简单但风险大——kubeconfig 给流水线、无单一事实源。
选 Pull-based。
一句话串起来#
代码进受保护的 main → 触发流水线 → 构建出可溯源镜像双推 registry → deploy.py 把镜像 tag 回写进 Kustomize overlay → ArgoCD pull 模式检测到 git 变化 → Kustomize build 渲染 → 收敛到集群 → 线上滚动更新;回滚 = revert 那个 commit,ArgoCD 自动收敛回旧版。
实战要点#
- CI 的目标是"高频小痛苦",不是"零痛苦"。每次提交跑 5 分钟 CI 比两周后跑 30 分钟集成要好。
- 一次构建多次部署是铁律。每个环境重新构建会引入"测的不是发的"风险。
- 缓存和并行是 CI 速度的两个杠杆。先做缓存(依赖层分离、cache mount),再做并行(job 级并行、矩阵构建)。
- 路径过滤是 Monorepo CI 的必备。没有路径过滤,Monorepo 的 CI 会变成"改一行,全仓库构建"。
- PROD 部署必须 manual。CI 自动跑到 PRE,PROD 要人工审批——这是"快速验证 + 谨慎上线"的平衡。
- 门禁要分层:pre warning(PR 时提醒)+ post fail(合并前阻塞)。详见 13 章。
- CI 失败要快速反馈。开发者的注意力窗口很短,CI 跑 15 分钟才报失败,开发已经切去别的事了。把 lint、format 这种快速检查放前面,慢的集成测试放后面。
- 流水线模板化是规模化 CI 的关键。超过 30 条流水线时必须做模板化,否则维护成本线性增长。详见后续工具章节。
小结#
CI 的核心是"高频集成 + 自动化验证"。一条完整的 CI 流水线有代码 → 构建 → 测试 → 扫描 → 报告五个阶段,配合缓存和并行加速,配合触发策略控制"什么时候跑什么"。
把 CI 放在整个 CICD 闭环里看,它是六个环节(代码 → 触发 → 构建 → 制品 → 声明 → 收敛)的前半段。CI 的产物(不可变镜像 + GitOps 声明)是 CD 的输入。后面几章讲具体的 CI 工具:GitHub Actions、GitLab CI、Jenkins。