CI/CD 流水线设计:从提交到上线的完整链路
一条好流水线的四个标准:快、可信、可回、可追溯。完整 GitLab CI 与 GitHub Actions 配置,以及六个最常见的反模式。
「我们上了 CI/CD」这句话经常名不副实。实际的流水线是:手动点一下构建 → 等 8 分钟 → 手动下载产物 → 手动 scp 到服务器 → 手动重启。这叫「自动构建」,不叫 CI/CD。
一条真正可用的流水线要解决四个问题:多快、多可信、出问题能不能回、谁在什么时候改了什么。
这篇文章按这四个维度组织,给出一个从提交到上线的完整设计,以及六个常见的反模式。
一、先定义清楚每个阶段
commit → ①lint/编译 → ②单测 → ③构建镜像 → ④推送 → ⑤部署测试环境
→ ⑥集成测试 → ⑦人工审批 → ⑧部署生产 → ⑨健康检查 → ⑩完成
每个阶段的「失败该不该阻断」是不一样的:
| 阶段 | 目标耗时 | 失败是否阻断 | 说明 |
|---|---|---|---|
| ① lint / 编译 | < 60s | 是 | 最快发现问题,必须前置 |
| ② 单测 | < 3min | 是 | 覆盖率不达标也阻断 |
| ③ 构建镜像 | < 2min | 是 | 分层缓存能压到 20s |
| ④ 推送 | < 1min | 是 | 分层推送 |
| ⑤ 部署测试 | < 1min | 是 | — |
| ⑥ 集成测试 | < 5min | 是 | 核心链路 smoke |
| ⑦ 人工审批 | — | — | 生产必过,除非是 hotfix 通道 |
| ⑧ 部署生产 | < 2min | 是 | 滚动 + 健康检查 |
| ⑨ 健康检查 | < 2min | 是(自动回滚) | 关键 |
总时长目标:从 push 到测试环境就绪 < 10 分钟。 超过这个时间,开发者就会开始「等 CI 的时候去干别的」,反馈环断裂。
二、一份完整的 GitLab CI 配置
# .gitlab-ci.yml
stages:
- validate
- test
- build
- deploy-test
- integration
- deploy-prod
variables:
IMAGE: $CI_REGISTRY_IMAGE
TAG: $CI_COMMIT_SHORT_SHA
DOCKER_DRIVER: overlay2
# ---------- ① 静态检查 ----------
lint:
stage: validate
image: node:20-alpine
cache:
key: ${CI_COMMIT_REF_SLUG}-npm
paths: [node_modules/]
script:
- npm ci --prefer-offline
- npm run lint
- npx vue-tsc --noEmit
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# ---------- ② 单测 ----------
unit:
stage: test
image: maven:3.9-eclipse-temurin-17
cache:
key: ${CI_COMMIT_REF_SLUG}-m2
paths: [.m2/repository/]
variables:
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
script:
- mvn -B test
- mvn -B jacoco:report
coverage: '/Total.*?([0-9]{1,3})%/'
artifacts:
reports:
junit: target/surefire-reports/TEST-*.xml
coverage_report:
coverage_format: cobertura
path: target/site/jacoco/jacoco.xml
expire_in: 1 week
# ---------- ③ 构建镜像 ----------
build:
stage: build
image: docker:24
services: [docker:24-dind]
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- |
docker build \
--cache-from $IMAGE:latest \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-t $IMAGE:$TAG \
-t $IMAGE:latest \
.
- docker push $IMAGE:$TAG
- docker push $IMAGE:latest
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# ---------- ⑤ 部署测试环境 ----------
deploy-test:
stage: deploy-test
image: bitnami/kubectl:1.29
environment:
name: test
url: https://test.example.com
script:
- kubectl set image deployment/api api=$IMAGE:$TAG -n test
- kubectl rollout status deployment/api -n test --timeout=180s
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# ---------- ⑥ 集成测试 ----------
integration:
stage: integration
image: curlimages/curl:8.4.0
script:
- ./scripts/smoke-test.sh https://test.example.com
artifacts:
when: on_failure
paths: [smoke-logs/]
# ---------- ⑦⑧ 生产部署 ----------
deploy-prod:
stage: deploy-prod
image: bitnami/kubectl:1.29
environment:
name: production
url: https://example.com
when: manual # 人工点击
allow_failure: false
script:
- kubectl set image deployment/api api=$IMAGE:$TAG -n prod
- kubectl rollout status deployment/api -n prod --timeout=300s
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
几个关键设计:
--cache-from + BUILDKIT_INLINE_CACHE:让 CI 复用上次构建的层。这能把镜像构建从 3 分钟压到 30 秒。
rollout status:不是发完命令就认为成功,而是等 k8s 确认所有 Pod 就绪。失败会返回非 0,流水线自动标红。
expire_in:测试报告一周就过期,避免占满存储。
coverage 正则:从输出里提取覆盖率,GitLab 会画成趋势图。
三、GitHub Actions 版本
name: CI/CD
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven' # 自动缓存 ~/.m2
- name: Test
run: mvn -B verify
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-reports
path: target/surefire-reports/
build:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha # GitHub Actions 缓存
cache-to: type=gha,mode=max
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- uses: actions/checkout@v4
- name: Deploy
run: |
echo "${{ secrets.KUBECONFIG }}" > /tmp/kubeconfig
kubectl set image deployment/api api=ghcr.io/${{ github.repository }}:${{ github.sha }}
kubectl rollout status deployment/api --timeout=300s
cache-from: type=gha 是 GitHub Actions 的原生缓存,比手动维护 registry 缓存省事得多。
environment 配合仓库设置里的「Required reviewers」,就实现了人工审批。
四、测试环境与生产环境的差异
最常见的反模式:测试环境跑得很好,生产一上线就出问题。原因是两个环境不一致。
差异来源
| 项 | 测试环境 | 生产环境 | 风险 |
|---|---|---|---|
| 数据量 | 1 万行 | 500 万行 | 慢查询只在生产暴露 |
| 副本数 | 1 | 6 | 并发 bug 只在生产出现 |
| 配置 | 手改 | 手改 | 两边不一致 |
| 中间件 | 单机 | 集群 | Cluster 的跨槽限制只在生产暴露 |
| 流量 | 无 | 真实 | 缓存穿透只在生产出现 |
缓解措施
1. 配置用同一份模板 + 环境变量注入
# k8s 里用 ConfigMap + Secret,不用手改文件
envFrom:
- configMapRef: { name: app-config }
- secretRef: { name: app-secret }
2. 测试环境也跑多副本
replicas: 2 # 至少 2 个,能暴露会话共享、缓存不一致问题
3. 用生产数据脱敏后回灌
mysqldump -h prod --single-transaction --no-create-info \
--where="id % 100 = 0" production orders | \
mysql -h test testdb
按 1% 采样,既能暴露数据量相关问题,又不至于太大。
4. 定期在生产做只读的「彩排」
部署到生产但只接 1% 流量(灰度),观察 30 分钟。这比测试环境充分得多。
五、六个反模式
反模式 1:流水线跑 40 分钟
问题:开发者 push 后去干别的,2 小时后再看结果
后果:反馈环断裂,"CI 挂了先不管"成为常态
修法:拆分快慢两条流水线。
# 快流水线:每次 push 跑,5 分钟内
quick-check:
script:
- mvn -B compile # 只编译,不测试
- ./scripts/lint.sh
# 慢流水线:合并到主干才跑,可以慢
full-test:
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
反模式 2:只跑单测,不跑集成测试
单测全绿但接口 500 的事每周都发生。原因是单测 mock 了所有外部依赖,无法发现「数据库迁移没执行」「环境变量配错了」这类问题。
修法:加 smoke test。
#!/bin/bash
# scripts/smoke-test.sh
set -e
BASE=$1
echo "1. 健康检查"
curl -sf "$BASE/actuator/health" | grep -q '"status":"UP"'
echo "2. 登录"
TOKEN=$(curl -sf -X POST "$BASE/api/login" \
-H 'Content-Type: application/json' \
-d '{"username":"smoke","password":"smoke123"}' | jq -r .token)
[ -n "$TOKEN" ] && [ "$TOKEN" != "null" ]
echo "3. 核心接口"
curl -sf -H "Authorization: Bearer $TOKEN" "$BASE/api/orders?page=1" | jq -e '.items' > /dev/null
echo "4. 数据库连通"
curl -sf -H "Authorization: Bearer $TOKEN" "$BASE/api/orders/1" | jq -e '.id'
echo "全部通过"
四条命令,覆盖「服务活着、能登录、核心接口可用、数据库通」这四个必须项。执行 10 秒,拦住一半的部署事故。
反模式 3:镜像 tag 用 latest
image: registry.example.com/api:latest # ✗
问题:
- 回滚时不知道回退到哪个版本
- 两个环境可能拉到不同的 latest
- 无法追溯「线上跑的是哪个 commit」
修法:用 git sha。
image: registry.example.com/api:${{ github.sha }}
同时打上语义版本 tag 便于人读:
docker tag $IMAGE:$SHA $IMAGE:v1.4.2
docker tag $IMAGE:$SHA $IMAGE:latest
回滚时用 sha,不用 latest。
反模式 4:部署不等待就绪
kubectl set image deployment/api api=$IMAGE # 命令返回就认为成功
set image 只是更新了 deployment spec,Pod 可能还在拉镜像、还在启动、还在崩溃重启。
修法:
kubectl set image deployment/api api=$IMAGE -n prod
kubectl rollout status deployment/api -n prod --timeout=300s || {
echo "部署失败,自动回滚"
kubectl rollout undo deployment/api -n prod
exit 1
}
反模式 5:数据库迁移和代码部署绑在一起
script:
- ./migrate.sh # 先迁移
- kubectl set image ... # 再部署
问题:迁移成功但部署失败 → 数据库结构变了,旧代码跑不起来。而且迁移脚本通常不可回滚。
修法:迁移和代码保持兼容,分两步走(Expand-Contract 模式):
第 1 次发布:加新字段(可空),代码双写,读旧字段
第 2 次发布:代码读写新字段
第 3 次发布:删旧字段
每一步都是向后兼容的,任何一步失败都能回滚。
反模式 6:密钥写进仓库
# ✗ 明文
env:
DB_PASSWORD: "Prod@123456"
哪怕是私有仓库,密钥也会出现在:构建日志、镜像层、PR diff、分支历史里。
修法:
env:
DB_PASSWORD: $DB_PASSWORD # 从 CI 变量注入
配合密钥管理服务(Vault / AWS Secrets Manager / k8s Secret + 外部 secrets operator)。
额外一条:密钥一旦提交过,就视为泄露。 改掉密钥 + 清理历史(git filter-repo),而不是只删文件。
六、加速 CI 的五个手段
| 手段 | 收益 | 前提 |
|---|---|---|
依赖缓存(~/.m2、node_modules) |
40~60% | 依赖不常变 |
镜像层缓存(--cache-from) |
60~80% | Dockerfile 分层合理 |
| 并行执行 job | 30~50% | job 之间无依赖 |
| 只跑受影响的测试 | 50~70% | 有模块化测试结构 |
| 自建 runner(就近网络) | 20~40% | 有机器资源 |
「只跑受影响的测试」收益最大也最容易做:
# 找出改动的模块
CHANGED=$(git diff --name-only origin/main...HEAD | cut -d/ -f1-2 | sort -u)
# 只跑这些模块的测试
mvn -pl $CHANGED -am test
七、监控流水线本身
流水线也是生产系统,需要监控四个指标:
| 指标 | 目标 | 说明 |
|---|---|---|
| 构建成功率 | > 95% | 低于 90% 说明测试不稳定(flaky) |
| 构建时长(P90) | < 10min | 变长说明缓存失效或有新阻塞 |
| 部署频率 | 越高越好 | DORA 指标之一 |
| 变更失败率 | < 15% | 部署后导致故障的比例 |
| 平均恢复时间(MTTR) | < 1h | 从故障到恢复 |
「构建成功率」尤其重要。 如果经常有 flaky test,团队就会养成「重跑一次」的习惯——这时候 CI 的信任就崩了,红灯不再被重视。
flaky test 的处理:标记、隔离、限期修复。不要长期保留。
@Tag("flaky")
@Disabled("https://jira.example.com/ISSUE-123 修复中")
小结
一条好的流水线满足四个标准:
- 快:push 到测试环境就绪 < 10 分钟
- 可信:红灯一定是真问题,绿灯一定可上线
- 可回:任何一次部署都能一条命令回滚
- 可追溯:从线上镜像能查到 commit、从 commit 能查到谁改了什么
六个反模式里,最该先修的是「部署不等待就绪」和「镜像用 latest」——这两个改起来只要几行,但能消掉大部分「上线后才发现问题」的事故。
正在加载评论…