流水线是工程效率的基础设施,但很多团队的流水线都处于「能用就行」的状态——慢、不稳定、失败了也不知道为什么。本文整理了我们在多个项目上迭代流水线设计的经验,重点是那些容易被忽视但影响很大的细节。
CI 阶段:构建速度是第一优先级#
CI 慢是工程效率的最大杀手。开发者提交代码后等 15 分钟才能看到结果,反馈循环太长,会直接影响开发节奏。
缓存策略#
缓存的核心原则:缓存粒度要细,key 要精准。
# GitHub Actions 缓存示例(Go 项目)
- name: Cache Go modules
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
# 缓存 key 的设计原则:
# - 用 go.sum / package-lock.json 的 hash,而不是日期
# - restore-keys 提供降级匹配,在精确 key 未命中时用上次的缓存
# - 不同 OS/平台要分开缓存(runner.os 前缀)
Docker layer 缓存 是另一个大头。CI 环境通常每次起新的 runner,本地 layer 缓存全无。解法是用 registry 作为缓存后端:
# 使用 ECR 作为 Docker 构建缓存
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ env.IMAGE_URI }}:${{ github.sha }}
cache-from: type=registry,ref=${{ env.ECR_REPO }}:cache
cache-to: type=registry,ref=${{ env.ECR_REPO }}:cache,mode=max
mode=max 会缓存所有中间层,而不只是最终层,对多阶段构建效果尤其好。
并行测试#
单元测试和集成测试串行跑是浪费。大部分 CI 系统支持 job 级别的并行:
jobs:
unit-test:
runs-on: ubuntu-latest
steps:
- run: go test ./... -short -count=1
lint:
runs-on: ubuntu-latest
steps:
- run: golangci-lint run
integration-test:
runs-on: ubuntu-latest
needs: unit-test # 只有单元测试通过才跑集成测试
steps:
- run: go test ./... -run Integration -count=1
build:
runs-on: ubuntu-latest
needs: [unit-test, lint] # 两个都通过才构建
steps:
- run: docker build ...
这样 unit-test 和 lint 并行跑,总耗时取决于较慢的那个,而不是两者之和。
Docker 镜像构建最佳实践#
多阶段构建#
多阶段构建的价值不只是减小镜像体积,更重要的是将构建环境和运行环境完全隔离,避免构建工具、源码、中间产物泄露到生产镜像。
# Go 应用的标准多阶段构建
FROM golang:1.23-alpine AS builder
WORKDIR /app
# 先复制依赖文件,利用 Docker layer 缓存
# 如果只改了业务代码,go.mod/go.sum 没变,这层直接命中缓存
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags="-w -s -X main.Version=${VERSION}" \
-o /app/server ./cmd/server
# 运行时镜像:distroless 没有 shell,攻击面极小
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
COPY --from=builder /app/configs /configs
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]
常见的 Dockerfile 反模式:
COPY . .放在go mod download之前——源码变动会使依赖层缓存失效- 用
latest基础镜像——构建不可复现,某天基础镜像更新可能引入问题 - 运行时镜像包含构建工具——镜像体积大,安全扫描会扫出大量漏洞
- 以 root 运行——容器逃逸时风险极高
镜像 Tag 策略#
镜像 tag 是可追溯性的基础。我们的命名规范:
# 格式:<registry>/<service>:<branch>-<short-sha>-<build-number>
123456789.dkr.ecr.us-west-2.amazonaws.com/my-service:main-a3f9c12-142
# 好处:
# - 从 tag 可以直接追回到 Git commit
# - build-number 是单调递增的,方便排序
# - 不用 latest,每次部署都有唯一标识
CD 阶段:CI 管构建,GitOps 管部署#
这是流水线设计中最重要的架构决策:CI 和 CD 要有清晰的边界。
CI 的职责止于:测试通过 → 构建镜像 → 推送到 Registry → 更新 GitOps 仓库里的镜像 tag。
CD(ArgoCD)的职责:检测到 GitOps 仓库变更 → 与集群实际状态对比 → 执行同步。
为什么要分离?如果 CI 直接 kubectl apply 到生产集群:
- 集群状态不透明,没有唯一 source of truth
- CI runner 需要有生产集群的 kubeconfig,权限管理混乱
- 回滚需要重新触发 CI,而不是直接 git revert
CI 更新 GitOps 仓库的标准做法:
# CI 流水线最后一步:更新 GitOps 仓库的镜像 tag
update_gitops() {
local SERVICE=$1
local NEW_TAG=$2
local ENV=$3
git clone https://github.com/org/gitops-repo.git /tmp/gitops
cd /tmp/gitops
# 用 yq 精确更新,避免 sed 出现意外匹配
yq e ".spec.template.spec.containers[0].image = \"${ECR_REPO}:${NEW_TAG}\"" \
-i "apps/${ENV}/${SERVICE}/deployment.yaml"
git config user.email "ci@company.com"
git config user.name "CI Bot"
git add .
git commit -m "chore: bump ${SERVICE} to ${NEW_TAG} in ${ENV}"
git push
}
update_gitops "my-service" "${IMAGE_TAG}" "production"
ArgoCD 检测到 GitOps 仓库变更(轮询或 webhook 触发),自动同步到集群。
多分支策略与环境对应#
分支策略决定了代码如何流向各个环境,要根据团队规模和发布节奏设计:
feature/* → 只跑 CI(单元测试 + lint),不部署
dev/main → CI + 部署到 QA 环境(自动)
release/* → CI + 部署到 PRE 环境(自动)+ 部署到 PROD(需手动审批)
# 云效 Flow 的分支触发配置示例
sources:
- type: codeup
name: source
props:
triggeredEvents:
- push
branchesFilter:
type: regex
rules:
included:
- "^main$"
- "^release/.*"
excluded:
- "^feature/.*"
环境隔离的关键点:
- 不同环境的 namespace 要隔离,不要共用
- QA 环境可以用比较宽松的资源限制,PRE 要接近 PROD 配置
- PRE 环境要和 PROD 用同样的 ConfigMap 结构(值可以不同),否则 PROD 部署时才发现配置缺失
回滚策略#
回滚是流水线设计中经常被忽视的部分,等到出问题了才发现流程没定好。
ArgoCD Rollback#
ArgoCD 保留历史部署记录,可以直接回滚到任意历史版本:
# 查看历史版本
argocd app history my-service
# 回滚到指定版本
argocd app rollback my-service <history-id>
# 或者通过 UI 操作,更直观
ArgoCD rollback 的本质是让 ArgoCD 重新 sync 到 GitOps 仓库的某个历史 commit。
Git Revert vs ArgoCD Rollback#
两者的选择取决于问题性质:
- ArgoCD Rollback:应急回滚,快,但 GitOps 仓库的 commit 还在,下次 sync 时会再次部署出问题的版本。适合临时止血。
- Git Revert:彻底回滚,在 GitOps 仓库里创建一个新的 revert commit,之后的 sync 都会用回滚后的版本。适合确认问题之后的正式处理。
实际流程:
# 1. ArgoCD 先回滚止血
argocd app rollback my-service <last-good-history-id>
# 2. 定位问题,在 GitOps 仓库执行 git revert
cd gitops-repo
git log --oneline apps/production/my-service/
git revert <bad-commit-sha>
git push
# 3. ArgoCD 检测到新 commit,自动同步(此时和应急回滚状态一致)
流水线失败的常见原因与排查#
按我的经验,流水线失败的原因大概是这样分布的:
1. 测试本身的问题(约 40%)
- 依赖外部服务(数据库、第三方 API)的测试在 CI 环境没有 mock
- 测试有隐性的时序依赖(
sleep(1000)之类的),在慢机器上会超时 - 测试并行跑有资源竞争(同一个端口、同一个测试数据库)
排查:优先看 test output,注意 timeout 和 connection refused 错误。
2. 构建环境问题(约 30%)
- 基础镜像拉不下来(网络问题或镜像被删)
- 缓存 key 设计有问题,导致缓存命中率为 0,每次都全量构建
- 工具版本不一致(CI 用的 Go 1.22,本地用 1.23)
排查:检查 runner 的系统日志,确认工具版本,加 --no-cache 复现。
3. 权限问题(约 20%)
- CI 推镜像到 ECR 的 IAM 权限过期或不足
- 更新 GitOps 仓库的 token 过期
- 访问 secret manager 的权限被修改
排查:找 403 Forbidden 或 denied 关键字,检查 IAM policy 和 token 有效期。
4. 基础设施问题(约 10%)
- Runner 磁盘满(Docker 镜像没清理)
- Runner 内存不足(并行 job 过多)
- Registry 出问题
排查:检查 runner 的 disk/memory 使用,查 registry 状态页。
# CI runner 磁盘清理(如果是自托管 runner)
docker system prune -af --volumes
df -h # 确认清理效果
小结#
一条好的 CI/CD 流水线需要在速度、可靠性和清晰边界三个维度上做好。速度靠缓存和并行,可靠性靠构建的可复现性和完善的测试,清晰边界靠严格区分 CI 和 CD 的职责。流水线也是需要持续维护的基础设施,不是搭好就一劳永逸的。






