一、为什么你的Gitlab CI构建像蜗牛爬?

每次提交代码后,你是不是总得泡杯咖啡才能等到构建完成?我们团队曾经有个项目,完整构建需要45分钟,开发人员每天要浪费两小时在等待上。这就像在高速公路上开老爷车,明明可以跑120码,结果你只能开30码。

构建缓慢的罪魁祸首通常有几个:不必要的阶段执行、重复的依赖下载、未利用缓存机制、资源配置不合理等。举个例子,前端项目每次都要重新下载node_modules,尽管package.json根本没变化。

二、优化配置的五大杀手锏

1. 巧用缓存机制

这是最容易见效的优化点。Gitlab CI提供了cache关键字,但很多人不会正确使用。看这个Node.js项目的例子:

# 正确缓存node_modules的配置示例
cache:
  key: $CI_COMMIT_REF_SLUG
  paths:
    - node_modules/
    - .next/cache/
  policy: pull-push  # 明确指定缓存策略

build:
  script:
    - npm ci --prefer-offline  # 优先使用缓存
    - npm run build

关键点在于:

  • 使用pull-push策略明确缓存流向
  • --prefer-offline让npm优先使用缓存
  • 为缓存设置合适的key,这里用分支名

2. 阶段并行化处理

把串行任务改成并行能大幅缩短时间。比如一个Java项目:

stages:
  - prepare
  - test
  - build

# 并行执行的单元测试
unit-test:
  stage: test
  script: 
    - mvn test -Dtest=UserServiceTest
  parallel: 3  # 分成3个并行任务

integration-test:
  stage: test
  script:
    - mvn verify -Pintegration

这里我们把测试拆分成并行的子任务,充分利用Runner资源。注意要确保测试之间没有依赖关系。

3. 依赖项智能安装

很多构建时间浪费在重复安装依赖上。这是Python项目的优化示例:

install-deps:
  stage: prepare
  script:
    - pip install -r requirements.txt --cache-dir .pip-cache
  artifacts:
    paths:
      - .pip-cache/
      - venv/

test:
  stage: test
  dependencies:
    - install-deps
  script:
    - source venv/bin/activate
    - pytest

通过artifacts传递虚拟环境,避免了每次重新创建。--cache-dir参数加速后续安装。

4. 选择性触发构建

不是每次提交都需要全量构建。使用only/exceptrules控制:

deploy:
  stage: deploy
  script: ./deploy.sh
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual
    - if: $CI_COMMIT_TAG

这个配置实现了:

  • 只有main分支合并和打tag时才部署
  • main分支部署需要手动触发
  • 其他情况跳过部署阶段

5. Runner资源配置

选择合适的Runner很关键。在.gitlab-ci.yml中添加:

variables:
  FF_USE_FASTZIP: "true"  # 启用快速压缩
  DOCKER_DRIVER: overlay2 # 优化Docker存储驱动

job:
  tags:
    - docker-large  # 使用配置更高的Runner
  resource_group: frontend-builds # 资源组避免冲突

三、实战:一个Go项目的优化历程

我们有个Go微服务项目,原始构建需要22分钟。优化后的配置:

image: golang:1.18

stages:
  - setup
  - test
  - build

variables:
  GOPATH: $CI_PROJECT_DIR/.go
  GO_CACHE: $CI_PROJECT_DIR/.cache/go-build

setup:
  stage: setup
  script:
    - mkdir -p $GO_CACHE
    - go mod download
  cache:
    key: go-mod-$CI_COMMIT_REF_SLUG
    paths:
      - $GOPATH/pkg/mod/
      - $GO_CACHE
    policy: pull-push

unit-test:
  stage: test
  parallel: 4
  script:
    - go test -v -short ./...

build:
  stage: build
  script:
    - go build -o app .
  artifacts:
    paths:
      - app
  only:
    - main
    - tags

优化效果:

  1. 通过缓存go mod依赖,节省了85%的依赖下载时间
  2. 测试并行化后时间从8分钟降到3分钟
  3. 选择性构建避免了不必要的编译
  4. 总构建时间从22分钟降到7分钟

四、避坑指南与进阶技巧

常见陷阱

  1. 缓存污染:错误的cache key会导致缓存混乱。建议:

    cache:
      key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
    
  2. artifact过大:限制artifact大小:

    artifacts:
      max_size: 100MB
      expire_in: 1 week
    
  3. 并行竞争:资源竞争时使用:

    resource_group: $CI_PROJECT_NAME-$CI_JOB_NAME
    

进阶技巧

  1. 分阶段缓存

    cache:
      - key: "node-$CI_COMMIT_REF_SLUG"
        paths: [node_modules/]
        policy: pull-push
      - key: "build-cache"
        paths: [dist/]
        policy: pull
    
  2. 动态生成配置

    # 根据修改文件决定测试范围
    CHANGED_FILES=$(git diff --name-only HEAD~1)
    echo "TEST_SCOPE=$(get_test_scope $CHANGED_FILES)" >> .env
    
  3. 使用needs加速

    job1:
      stage: build
      script: echo "Job1"
    
    job2:
      needs: ["job1"]
      script: echo "Job2"  # 不必等待同阶段其他任务
    

五、不同技术栈的优化要点

Java/Maven项目

cache:
  paths:
    - .m2/repository/
    - target/

variables:
  MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"

Ruby/Rails项目

cache:
  key: "rails-$CI_COMMIT_REF_SLUG"
  paths:
    - vendor/bundle
    - tmp/cache

before_script:
  - bundle install --jobs $(nproc) --path vendor/bundle

Docker构建

build:
  image: docker:20.10
  services:
    - docker:20.10-dind
  script:
    - docker build --cache-from $CI_REGISTRY_IMAGE:latest .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

六、效果评估与持续改进

建议建立构建监控看板,跟踪:

  • 各阶段耗时变化
  • 缓存命中率
  • Runner利用率

每月进行一次配置评审,删除无用阶段,合并相似任务。记住:优化是持续过程,不是一劳永逸的。

通过以上方法,我们团队成功将平均构建时间从32分钟降到9分钟。最重要的是培养了优化意识 - 每个新功能开发时都会考虑CI影响。现在,咖啡还没泡好,构建就已经完成了!