4.11 CI 平台深入——Jenkins + GitLab CI 对比与实践

预计阅读时间:11 分钟

📖 目录

4.9:CI/CD 持续部署 和 cicd01 介绍了 GitHub Actions 的流水线实践。在企业级场景中,Jenkins 和 GitLab CI 是更常见的选择:Jenkins 拥有庞大的插件生态和灵活性,GitLab CI 与代码仓库深度集成、开箱即用。本文从安装配置到大规模优化,系统梳理两大平台的核心用法和选型策略。

学习目标

  • 掌握 Jenkins 声明式 Pipeline 语法与 Pipeline as Code 实践
  • 能用共享库(Shared Library)抽象并复用构建逻辑
  • 掌握 GitLab CI 的 Runner 配置、stages、rules 与 environment 机制
  • 理解 Artifact 与 Cache 的区别并设计合理的缓存策略
  • 能对比 Jenkins、GitLab CI、GitHub Actions 的适用场景并做出选型
  • 掌握矩阵并行构建与分层缓存等大规模 CI 优化手段

前置知识

1. Jenkins 安装与 Pipeline as Code

1.1 安装 Jenkins

# Ubuntu / Debian
curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | sudo tee /usr/share/keyrings/jenkins-keyring.asc > /dev/null
echo deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/ | sudo tee /etc/apt/sources.list.d/jenkins.list
sudo apt-get update && sudo apt-get install -y jenkins

# Docker 方式(推荐)
docker run -d --name jenkins -p 8080:8080 -p 50000:50000 \
  -v jenkins_home:/var/jenkins_home \
  -v /var/run/docker.sock:/var/run/docker.sock \
  jenkins/jenkins:lts

# 获取初始管理员密码
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword

1.2 Jenkinsfile 基础语法

// 声明式 Pipeline(推荐)
pipeline {
    agent any

    environment {
        REGISTRY = 'harbor.example.com'
        IMAGE = "${REGISTRY}/myapp"
    }

    options {
        timeout(time: 30, unit: 'MINUTES')
        disableConcurrentBuilds()
        buildDiscarder(logRotator(numToKeepStr: '10'))
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Build') {
            steps {
                sh 'docker build -t ${IMAGE}:${BUILD_NUMBER} .'
            }
        }

        stage('Test') {
            parallel {
                stage('Unit Test') {
                    steps {
                        sh 'docker run --rm ${IMAGE}:${BUILD_NUMBER} npm test'
                    }
                }
                stage('Lint') {
                    steps {
                        sh 'docker run --rm ${IMAGE}:${BUILD_NUMBER} npm run lint'
                    }
                }
            }
        }

        stage('Push') {
            when { branch 'main' }
            steps {
                withCredentials([usernamePassword(
                    credentialsId: 'harbor-creds',
                    usernameVariable: 'USER',
                    passwordVariable: 'PASS'
                )]) {
                    sh 'echo $PASS | docker login $REGISTRY -u $USER --password-stdin'
                    sh 'docker push ${IMAGE}:${BUILD_NUMBER}'
                    sh 'docker tag ${IMAGE}:${BUILD_NUMBER} ${IMAGE}:latest'
                    sh 'docker push ${IMAGE}:latest'
                }
            }
        }
    }

    post {
        always {
            sh 'docker rmi ${IMAGE}:${BUILD_NUMBER} || true'
        }
        failure {
            mail to: 'team@example.com',
                 subject: "构建失败: ${currentBuild.fullDisplayName}",
                 body: "详情: ${env.BUILD_URL}"
        }
    }
}
Jenkinsfile 存放位置 两种方式:① 脚本式(Scripted)放在 Jenkins UI 的 Pipeline 配置中;② 声明式(Declarative)推荐将 Jenkinsfile 提交到代码仓库根目录,实现 Pipeline as Code,与代码版本同步。

1.3 Jenkins 共享库(Shared Library)

// vars/buildDockerImage.groovy(共享库)
def call(Map config) {
    sh "docker build -t ${config.registry}/${config.image}:${config.tag} ."
    withCredentials([usernamePassword(credentialsId: 'registry-creds', usernameVariable: 'USER', passwordVariable: 'PASS')]) {
        sh "echo $PASS | docker login ${config.registry} -u $USER --password-stdin"
        sh "docker push ${config.registry}/${config.image}:${config.tag}"
    }
}

// Jenkinsfile 中调用
@Library('my-shared-lib') _
buildDockerImage(registry: 'harbor.example.com', image: 'myapp', tag: env.BUILD_NUMBER)

2. GitLab CI 深入

2.1 Runner 配置

# 安装 GitLab Runner
curl -L --output /usr/local/bin/gitlab-runner \
  https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64
sudo chmod +x /usr/local/bin/gitlab-runner
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash

# 注册 Runner
sudo gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.example.com/" \
  --token "glrt-xxxxxxxx" \  # GitLab 17.0+ 弃用 registration-token,改用 runner token
  --executor "docker" \
  --docker-image "alpine:latest" \
  --description "docker-runner" \
  --tag-list "docker,linux" \
  --run-untagged="true"

# Runner 配置文件 /etc/gitlab-runner/config.toml
concurrent = 10        # 并行任务数
check_interval = 3

[[runners]]
  name = "docker-runner"
  executor = "docker"
  [runners.docker]
    image = "alpine:latest"
    privileged = true       # 需要构建 Docker 镜像时开启
    volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock"]
    shm_size = 0
  [runners.cache]
    Type = "s3"             # 分布式缓存后端
    Shared = true
    [runners.cache.s3]
      BucketName = "gitlab-runner-cache"
      BucketLocation = "eu-west-1"

2.2 GitLab CI 完整示例

# .gitlab-ci.yml
stages:
  - build
  - test
  - scan
  - deploy

variables:
  DOCKER_TLS_CERTDIR: "/certs"
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

build:
  stage: build
  tags: [docker]
  script:
    - docker build -t $IMAGE_TAG .
    - docker push $IMAGE_TAG
  rules:
    - if: $CI_MERGE_REQUEST_IID
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

test:unit:
  stage: test
  tags: [docker]
  image: $IMAGE_TAG
  script:
    - npm ci --cache .npm
    - npm run test:unit -- --coverage
  coverage: '/Statements\s*:\s*(\d+\.?\d*)%/'
  artifacts:
    reports:
      junit: test-results.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura.xml
    paths:
      - coverage/
    expire_in: 7 days

test:integration:
  stage: test
  tags: [docker]
  services:
    - name: postgres:16-alpine
      alias: db
      variables:
        POSTGRES_PASSWORD: test
    - name: redis:7-alpine
      alias: cache
  image: $IMAGE_TAG
  script:
    - DB_HOST=db npm run test:integration

scan:trivy:
  stage: scan
  tags: [docker]
  image:
    name: aquasec/trivy:latest
    entrypoint: [""]
  script:
    - trivy image --exit-code 1 --severity HIGH,CRITICAL $IMAGE_TAG

deploy:staging:
  stage: deploy
  tags: [docker]
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
  script:
    - ssh deployer@staging "docker pull $IMAGE_TAG && docker compose -f /app/docker-compose.yml up -d"
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

deploy:production:
  stage: deploy
  tags: [docker]
  script:
    - ssh deployer@prod "docker pull $IMAGE_TAG && docker compose -f /app/docker-compose.yml up -d"
  environment:
    name: production
    url: https://example.com
  when: manual
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/

2.3 Artifact 与 Cache 管理

特性ArtifactCache
用途构建产物(二进制、报告、包)依赖缓存(node_modules、.m2)
存储位置GitLab 服务器(可通过 S3/MinIO 后端)Runner 本地或 S3 分布式缓存
生命周期按 expire_in 策略保留按 Job 触发自动管理
下载方式通过 UI / API 下载 zip自动注入到工作目录
# artifact 示例:保存构建产物
build:
  artifacts:
    paths:
      - dist/
      - build/
    reports:
      junit: test-results.xml
    expire_in: 1 week

# cache 示例:缓存依赖加速构建
build:
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - node_modules/
    policy: pull-push    # pull-push(默认)| pull-only | push-only

# 分布式缓存(S3 后端)
# config.toml 中配置 [runners.cache.s3]
Cache 不等于 Artifact Cache 是"尽力而为"的缓存,可能被 Runner 清理;Artifact 是 GitLab 管理的持久化产物。不要用 Cache 存放必须保留的构建结果。

3. Jenkins vs GitLab CI vs GitHub Actions 对比

维度JenkinsGitLab CIGitHub Actions
部署方式自托管(Master + Agent)SaaS 或自托管(Runner)SaaS(GitHub 托管)或自托管 Runner
配置语言Jenkinsfile(Groovy).gitlab-ci.yml(YAML).github/workflows/*.yml(YAML)
插件生态1800+ 插件,极其丰富内置功能完整,插件较少Marketplace 丰富,社区 Actions 多
与代码仓库集成需额外配置 Webhook原生深度集成原生深度集成
并行与矩阵构建支持(Matrix Axis)支持(parallel + matrix)支持(strategy.matrix)
密钥管理Credentials 插件CI/CD Variables(项目/组/实例级)Secrets(仓库/Org/Environment 级)
制品管理插件(Nexus/Artifactory)内置 Package RegistryGitHub Packages
扩展性极高(Groovy 脚本 + 插件)中(Runner 自定义 executor)中(Composite Actions)
维护成本高(需运维 Jenkins 集群)中(Runner 需自管)低(SaaS 模式)
适用场景大型企业、复杂流水线、遗留系统自托管 GitLab、DevOps 全流程GitHub 仓库、中小团队、快速上手

4. 构建产物管理

4.1 Docker 镜像版本化策略

# 语义化版本 + Git SHA 双 tag
VERSION=$(grep version package.json | head -1 | awk -F '"' '{print $4}')
SHORT_SHA=$(git rev-parse --short HEAD)

docker build \
  -t harbor.example.com/myapp:${VERSION} \
  -t harbor.example.com/myapp:sha-${SHORT_SHA} \
  -t harbor.example.com/myapp:latest \
  .

# CI 中根据触发条件决定 tag
# Git tag 触发 → 使用语义版本
# 分支 push   → 使用 sha-xxxxxxx
if [[ "$CI_COMMIT_TAG" =~ ^v ]]; then
  IMAGE_TAG="${CI_COMMIT_TAG#v}"
else
  IMAGE_TAG="sha-${CI_COMMIT_SHORT_SHA}"
fi

4.2 Nexus 与 Harbor 对比

特性Nexus RepositoryHarbor
定位通用制品仓库(Maven/npm/Docker/Yum 等)专注容器镜像(OCI 标准)
镜像安全基础扫描Trivy/Clair 漏洞扫描 + 签名验证
访问控制RBAC + LDAP/AD 集成RBAC + OIDC + LDAP + 项目隔离
复制与高可用Nexus Pro 支持多节点内置多实例复制(Replication)
适用场景多语言制品统一管理容器镜像专用仓库
# Harbor 镜像推送(GitLab CI 示例)
deploy:image:
  stage: deploy
  script:
    - docker login -u $HARBOR_USER -p $HARBOR_PASS harbor.example.com
    - docker tag $IMAGE_TAG harbor.example.com/myapp:$IMAGE_TAG
    - docker push harbor.example.com/myapp:$IMAGE_TAG
  # Harbor 自动触发漏洞扫描

# Nexus npm 仓库配置
# 项目 .npmrc:
registry=https://nexus.example.com/repository/npm-group/
//nexus.example.com/repository/npm-/:_authToken=${NPM_TOKEN}

5. 大规模 CI 优化

5.1 并行构建策略

# GitLab CI:parallel 矩阵构建
test:matrix:
  stage: test
  parallel:
    matrix:
      - NODE_VERSION: [18, 20, 22]
        DB: [postgres, mysql]
  image: node:${NODE_VERSION}
  services:
    - name: $DB:latest
      alias: db
  script:
    - npm ci
    - npm test

# Jenkins:Matrix 构建
pipeline {
    agent {
        docker { label 'builder' }
    }
    stages {
        stage('Matrix Build') {
            matrix {
                axes {
                    axis {
                        name 'NODE_VERSION'
                        values '18', '20', '22'
                    }
                    axis {
                        name 'DB'
                        values 'postgres', 'mysql'
                    }
                }
                stages {
                    stage('Test') {
                        steps {
                            sh "docker run node:${NODE_VERSION} npm test"
                        }
                    }
                }
            }
        }
    }
}

5.2 缓存策略优化

# GitLab CI 分层缓存
# 1. 依赖缓存(最快,每次构建)
dependencies:
  cache:
    key:
      files:
        - package-lock.json   # 基于 lock 文件 hash
    paths:
      - node_modules/
    policy: pull-push

# 2. Docker 层缓存(中等,跨构建复用)
build:
  image: docker:24
  services:
    - docker:24-dind
  variables:
    DOCKER_BUILDKIT: "1"
  script:
    # 从 Registry 拉取上次构建的镜像作为缓存源
    - docker pull $IMAGE_TAG || true
    - docker build --cache-from $IMAGE_TAG -t $IMAGE_TAG .

# 3. 本地构建缓存挂载(最快,同一 Runner)
build:cache-mount:
  script:
    - docker build \
        --mount=type=cache,target=/root/.npm \
        --mount=type=cache,target=/app/.next/cache \
        -t $IMAGE_TAG .

5.3 其他优化手段

手段效果适用场景
自托管 Runner + 本地缓存避免每次下载依赖,构建提速 50-80%大型 monorepo、频繁构建
Docker BuildKit / Buildx并行构建镜像层,支持跨平台多架构镜像构建
增量构建(Change Detection)只构建变更模块monorepo 多服务项目
构建结果缓存(Remote Cache)跨 Runner 共享缓存CI 集群多节点
Artifact 复用(制品下载)避免重复构建相同产物多环境部署同一制品
Runner 资源规划 大规模 CI 场景下,Runner 需要按队列分组:构建密集型任务(docker build)分配高内存节点;测试密集型任务分配高 CPU 节点;I/O 密集型任务( artifact 下载)分配高带宽节点。通过 Runner 的 tag 机制实现任务调度。

常见错误

问题原因解决方案
Jenkinsfile 语法报错 Expected a step声明式 Pipeline 中使用了脚本式语法(如直接写 node {}声明式 Pipeline 中用 script {} 块包裹脚本式代码,或改用 sh/steps 标准步骤
GitLab Runner 执行报 permission deniedDocker executor 挂载 docker.sock 后容器内用户无权限在 Runner 配置中设置 privileged = true,或将宿主机 docker 组 GID 传入容器
Cache 命中但构建仍然很慢cache key 设计不当导致每次都重建缓存;或 policy: pull-only 未在生产阶段写入基于 lock 文件 hash 生成 key,保证依赖不变时缓存不变;合理分阶段设置 pull/push 策略
Secret 变量在日志中泄露echo $MY_SECRETdocker login 密码未隐藏使用 masked: true(GitLab)或 credentials()(Jenkins)包装变量,CI 日志自动脱敏
Matrix 构建缺少组合过滤不想测试所有组合(如 MySQL + Node 22 不兼容)却未排除exclude 规则(GitLab CI parallel.matrix.exclude)或 Jenkins excludes 排除特定组合

最佳实践

  • Pipeline as Code:将 Jenkinsfile / .gitlab-ci.yml 提交到代码仓库,与代码版本同步,禁止通过 UI 手动修改流水线定义。
  • 凭证管理:永远不要在配置文件中硬编码密码或 token。Jenkins 使用 Credentials 插件,GitLab 使用 CI/CD Variables(设置 maskedprotected),生产凭证仅限保护分支使用。
  • 构建产物版本化:同时打语义版本号(v1.2.3)和 Git SHA 短哈希(sha-abc1234)双 tag,兼顾可读性和可追溯性。
  • 并行测试:单元测试、Lint、安全扫描拆分为并行 stage,缩短整体构建时间。利用 Matrix 构建覆盖多版本/多数据库组合。
  • Runner 资源隔离:按任务类型给 Runner 打 tag(dockerk8shigh-mem),构建密集型任务分配高内存节点,测试密集型分配高 CPU 节点。

故障排查案例:Jenkins Pipeline 构建卡死

现象:Jenkins Pipeline 在 Checkout 阶段卡住,日志显示 Timeout waiting for Jenkins to finish,但 Git 服务器连通正常。

排查:执行 ps aux | grep jenkins 发现多个 git-fetch 进程处于 D 状态(不可中断 I/O);检查 Jenkins Agent 磁盘使用率 > 95%。

根因:Jenkins Workspace 目录积累了大量旧构建产物,磁盘空间耗尽导致 Git 操作无法写入临时文件。

修复:① 清理 /var/jenkins_home/workspace/ 下旧项目目录;② 在 Jenkins 全局配置中设置 buildDiscarder(logRotator(numToKeepStr: '10')) 自动保留最近 10 次构建;③ 配置定时任务 find /var/jenkins_home -name "lastStable" -mtime +7 -exec rm -rf {} \; 清理 7 天前的临时文件。

Jenkins Pipeline 调试技巧

# 在 Jenkinsfile 中开启详细日志
pipeline {
    agent any
    options {
        timestamp()                    // 每行输出加时间戳
    }
    stages {
        stage('Debug') {
            steps {
                script {
                    // 打印环境变量排查 PATH / JAVA_HOME 问题
                    sh 'env | sort'
                    // 打印 Jenkins 节点信息
                    sh 'echo "Node: ${NODE_NAME} | Workspace: ${WORKSPACE}"'
                    // 验证工具可用性
                    sh 'which docker && docker --version'
                }
            }
        }
    }
}

# GitLab CI 中开启调试
test:debug:
  script:
    - echo "Pipeline ID: $CI_PIPELINE_ID"
    - echo "Runner: $CI_RUNNER_DESCRIPTION"
    - echo "Docker Image: $CI_JOB_IMAGE"
    - cat .gitlab-ci.yml    # 打印当前配置便于排障
调试原则 CI/CD 调试的核心是「环境还原」——打印运行时环境变量、节点信息、工具版本,快速定位「本地能跑、线上不行」的根因。

练习题

  1. 多阶段 Pipeline:为一个 Node.js 项目编写完整的 .gitlab-ci.yml,包含 build → lint → unit test → integration test(使用 PostgreSQL service)→ Docker 镜像构建 → 部署到 staging(手动触发)五个阶段,要求 artifact 保存覆盖率报告。
  2. 分布式缓存:配置 GitLab Runner 使用 S3 后端作为分布式缓存,确保同一项目的多个 Runner 节点可以共享 node_modules 缓存,并验证 cache key 基于 package-lock.json 的 hash 变化。
  3. 安全扫描集成:在现有 CI 流水线中集成 Trivy 镜像扫描,要求:HIGH 和 CRITICAL 漏洞导致构建失败,MEDIUM 及以下仅输出警告;扫描结果保存为 GitLab artifact 供后续审计。

本章总结

企业级 CI 平台选型无绝对优劣,关键在于与团队规模、代码托管方式及维护能力的匹配。无论 Jenkins 还是 GitLab CI,核心工程实践一致:Pipeline as Code 与代码同版本演进、凭证统一脱敏管理、产物 SemVer + SHA 双 tag、矩阵并行与缓存优化控制成本。平台只是工具,工程纪律才是效率的保障。

延伸阅读

  • 4.9:CI/CD 持续部署 CI/CD 持续部署入门
  • cicd01 CI/CD 生产实践——多环境、审批与交付流水线
  • container_sec01 容器安全:Trivy 镜像漏洞扫描
  • container_ops01 Docker Compose 生产部署实战
↑ 回到顶部