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 优化手段
前置知识
- 4.9:CI/CD 持续部署 CI/CD 持续部署入门——流水线基本概念与 GitHub Actions 实践
- cicd01 CI/CD 生产实践——多环境、审批与交付流水线
- 3.1:Docker 容器入门 Docker 容器入门——镜像构建与容器化交付基础
- 4.18:Git 进阶 Git 进阶——分支策略与 Git tag 语义
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}"
}
}
}
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 管理
| 特性 | Artifact | Cache |
|---|---|---|
| 用途 | 构建产物(二进制、报告、包) | 依赖缓存(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]
3. Jenkins vs GitLab CI vs GitHub Actions 对比
| 维度 | Jenkins | GitLab CI | GitHub 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 Registry | GitHub 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 Repository | Harbor |
|---|---|---|
| 定位 | 通用制品仓库(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 复用(制品下载) | 避免重复构建相同产物 | 多环境部署同一制品 |
常见错误
| 问题 | 原因 | 解决方案 |
|---|---|---|
Jenkinsfile 语法报错 Expected a step | 声明式 Pipeline 中使用了脚本式语法(如直接写 node {}) | 声明式 Pipeline 中用 script {} 块包裹脚本式代码,或改用 sh/steps 标准步骤 |
GitLab Runner 执行报 permission denied | Docker executor 挂载 docker.sock 后容器内用户无权限 | 在 Runner 配置中设置 privileged = true,或将宿主机 docker 组 GID 传入容器 |
| Cache 命中但构建仍然很慢 | cache key 设计不当导致每次都重建缓存;或 policy: pull-only 未在生产阶段写入 | 基于 lock 文件 hash 生成 key,保证依赖不变时缓存不变;合理分阶段设置 pull/push 策略 |
| Secret 变量在日志中泄露 | echo $MY_SECRET 或 docker 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(设置
masked和protected),生产凭证仅限保护分支使用。 - 构建产物版本化:同时打语义版本号(
v1.2.3)和 Git SHA 短哈希(sha-abc1234)双 tag,兼顾可读性和可追溯性。 - 并行测试:单元测试、Lint、安全扫描拆分为并行 stage,缩短整体构建时间。利用 Matrix 构建覆盖多版本/多数据库组合。
- Runner 资源隔离:按任务类型给 Runner 打 tag(
docker、k8s、high-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 # 打印当前配置便于排障
练习题
- 多阶段 Pipeline:为一个 Node.js 项目编写完整的 .gitlab-ci.yml,包含 build → lint → unit test → integration test(使用 PostgreSQL service)→ Docker 镜像构建 → 部署到 staging(手动触发)五个阶段,要求 artifact 保存覆盖率报告。
- 分布式缓存:配置 GitLab Runner 使用 S3 后端作为分布式缓存,确保同一项目的多个 Runner 节点可以共享
node_modules缓存,并验证 cache key 基于package-lock.json的 hash 变化。 - 安全扫描集成:在现有 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 生产部署实战