概述
凌晨 1 点,你收到告警:GitLab CI 流水线队列堆积了 47 个 pending job。开发群炸了——“代码推了 40 分钟还没跑起来"“是不是 Runner 挂了"“赶紧加机器啊”。你登录控制台一看,3 个 Runner 全部满载,每个并发 10 个 job,30 个 slot 全占满了。加机器?Docker Machine 执行器拉起新 EC2 要 3 分钟,等机器 Ready 又要 2 分钟。5 分钟过去了,队列又涨了 20 个。
这不是假设。2025 年我在某出行项目做 CI/CD 平台重构时,就遇到过这个场景。当时的 Runner 架构是经典的 Docker Machine + AWS EC2 自动伸缩,高峰期排队 30-40 分钟是常态。后来我们迁移到 Kubernetes Executor + HPA 弹性伸缩,排队时间降到 30 秒以内。这中间踩了不少坑,也做了不少架构决策。
本文不讲 .gitlab-ci.yml 基础语法(那些 CSDN 上够多了),只聊 7 个真正影响生产效率的工程决策:执行器选型、资源规划、弹性伸缩、缓存设计、流水线编排、节点隔离、监控告警。每个决策都附实测数据和踩坑细节。
如果你在评估 CI/CD 平台选型,可以参考我之前的文章 相关文章:别让 Jenkins 决定你的发布节奏:用 Go 自研 DAG 调度引擎的架构决策与踩坑实录,里面对比了 Jenkins、GitLab CI 和自研调度引擎的差异。
决策一:执行器选型——Docker Machine 退役后怎么办
执行器全景对比
GitLab Runner 支持多种执行器(Executor),每种对应不同的运行环境和隔离方式。选错执行器,后面的弹性伸缩和缓存设计全是空中楼阁。
| 执行器 | 隔离方式 | 弹性伸缩 | 适用场景 | 维护状态 |
|---|---|---|---|---|
| Shell | 无隔离(直接在主机跑) | 不支持 | 单机调试、简单脚本 | 维护模式 |
| Docker | 容器隔离 | 不支持 | 固定 Runner、中等并发 | 活跃 |
| Kubernetes | Pod 隔离 | HPA + Cluster Autoscaler | 云原生环境、大规模 | 活跃(推荐) |
| Docker Autoscaler | 容器隔离 + fleeting | 自动伸缩 | 云 VM 环境 | 活跃(新) |
| Instance | 直接在主机跑 | 不支持 | 特殊硬件(GPU) | 活跃(新) |
| Docker Machine | 容器隔离 + VM 自动伸缩 | 自动伸缩 | AWS/Azure/GCP | 已废弃 |
Docker Machine 退役时间线
GitLab 17.5(2024 年 10 月发布)正式废弃了 Docker Machine 执行器。官方计划在 GitLab 20.0(2027 年 5 月)彻底移除。这意味着:
- 不再接受新功能开发
- 只修复影响 CI/CD 执行或成本的严重 bug
- 现有用户必须在此日期前迁移到 Docker Autoscaler 或 Kubernetes Executor
Docker Machine 执行器依赖 GitLab 维护的 Docker Machine fork(Docker 官方早已停止维护 Docker Machine)。这个 fork 需要持续跟进云厂商 API 变更,维护成本高,且无法支持一些新特性(如 fleeting 调度框架)。
我的推荐:Kubernetes Executor
在 2026 年的背景下,如果你的基础设施有 Kubernetes 集群,无脑选 Kubernetes Executor。原因:
- 原生弹性伸缩:配合 HPA + Cluster Autoscaler,Pod 级别的扩缩容比 VM 快 10 倍以上(秒级 vs 分钟级)
- 资源利用率高:Job Pod 执行完即销毁,不占用闲置资源。Docker Machine 方式下,EC2 实例即使空闲也要计费直到被回收
- 运维统一:Runner 管理面和 Job 执行面都在 K8s 体系内,用 kubectl 就能排查问题,不需要额外维护 Docker Machine 工具链
- 调度能力强:支持 nodeSelector、tolerations、affinity 等 K8s 调度特性,可以做精细的资源隔离
不推荐 Kubernetes Executor 的唯一场景:你的 CI/CD 任务需要直接操作宿主机(如内核模块测试、硬件驱动编译),这时 Shell 或 Instance 执行器更合适。
从 Docker Machine 迁移到 Kubernetes Executor 的路径
迁移不是一刀切。我推荐的策略是"双轨并行 + 灰度切换”:
# 步骤1:部署新的 K8s Runner,注册到同一个 GitLab 实例
# 用不同的 tag 区分新旧 Runner
# 旧 Runner tag: docker-machine
# 新 Runner tag: kubernetes
# 步骤2:在 .gitlab-ci.yml 中,逐步将 job 的 tags 从 docker-machine 切到 kubernetes
# 先切非核心 job(lint、文档生成),再切构建 job,最后切部署 job
# 步骤3:观察 1-2 周,确认无兼容性问题后,下线 Docker Machine Runner
迁移过程中最容易踩的坑:Docker Machine 执行器下,job 运行在独立的 EC2 上,网络是 flat 的;K8s Executor 下,job Pod 运行在 Pod 网络中。如果你的 CI/CD 脚本里写了硬编码的 IP 地址或依赖宿主机网络,迁移后一定会报错。建议迁移前用
grep -r '[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+' scripts/扫一遍。
决策二:Runner Manager Pod 资源规划——用数据说话
Manager Pod 的职责拆解
Kubernetes Executor 架构下,Runner Manager Pod 是整个 CI/CD 的调度中枢。它不执行具体的 build/test/deploy 任务,但负责:
- 日志处理:从 Job Pod 收集日志流,转发给 GitLab 实例
- 缓存管理:协调本地缓存和 S3 分布式缓存的上传下载
- K8s API 交互:创建、监控、删除 Job Pod
- GitLab API 通信:轮询获取 Job、上报执行状态
- Pod 生命周期管理:管理 Job Pod 的供给和清理
每个职责消耗不同类型的资源。GitLab 官方的性能测试给出了清晰的资源消耗模型。
官方实测数据
GitLab 团队用一个产生 4MB 日志的压测 Job 做了系统测试。测试方法是 parallel: 100 并发执行,每个 Job 生成 4MB 随机数据并逐块读取输出。结果:
| 并发 Job 数 | 峰值 CPU | 峰值内存 |
|---|---|---|
| 25 | 160m | 112MB |
| 50 | 308m | 261MB |
| 75 | 460m | 237MB |
| 100 | 657m | 369MB |
从数据可以推导出资源消耗公式:
- CPU:基础 10m + 每并发 Job 约 6m
- 内存:基础 50MB + 每并发 Job 约 2.5MB(按 4MB 日志量计算)
我的资源规划公式
基于官方数据,我在生产环境用的资源规划公式:
def calculate_manager_resources(concurrent_jobs, avg_log_mb=4):
"""GitLab Runner Manager Pod 资源规划"""
# CPU: ~6m per concurrent job + 10m base
base_cpu = 0.01
cpu_per_job = 0.006
total_cpu = base_cpu + (concurrent_jobs * cpu_per_job)
# Memory: ~2.5MB per job + 50MB base
base_memory = 50
memory_per_job = 2.5 * (avg_log_mb / 4)
total_memory = base_memory + (concurrent_jobs * memory_per_job)
return {
'cpu_request': f"{int(total_cpu * 1000)}m",
'cpu_limit': f"{int(total_cpu * 1.5 * 1000)}m", # 50% 余量
'memory_request': f"{int(total_memory)}Mi",
'memory_limit': f"{int(total_memory * 2.0)}Mi" # 100% 余量
}
实际使用中,50 并发 Job 的配置:
# 50 并发 Job 的 Manager Pod 资源配置
resources:
requests:
cpu: "310m" # 10m + (50 × 6m) = 310m
memory: "175Mi" # 50 + (50 × 2.5) = 175MB
limits:
cpu: "465m" # 50% 余量
memory: "350Mi" # 100% 余量
踩坑:内存不够导致日志截断
有一个坑特别隐蔽:Manager Pod 内存不足时,Job 日志会被静默截断。不会报错,GitLab 界面上看到日志正常结束,但最后几行丢了。如果你的 CI/CD 脚本依赖日志最后输出构建结果(比如 echo "BUILD_RESULT=success" 然后 parse),日志截断会导致解析失败。
排查方法:对比 GitLab 界面上 Job 日志的最后一行和 Job Pod 实际 stdout。如果不一致,就是日志截断。
解决方案:Manager Pod memory limit 至少留 100% 余量。公式中的 × 2.0 不是过度设计,是踩过坑的。
决策三:弹性伸缩——从排队 30 分钟到 30 秒
三层弹性伸缩架构
GitLab CI Runner 在 K8s 上的弹性伸缩不是单一机制,而是三层协作:
┌─────────────────────────────────────────────────────────┐
│ 第一层:Runner Manager 并发控制 │
│ config.toml: concurrent = 50 │
│ 作用:单个 Manager Pod 能同时处理多少 Job │
├─────────────────────────────────────────────────────────┤
│ 第二层:HPA 水平扩展 Manager Pod │
│ 作用:根据 CPU/内存利用率自动增减 Manager Pod 数量 │
│ minReplicas: 2 maxReplicas: 5 │
├─────────────────────────────────────────────────────────┤
│ 第三层:Cluster Autoscaler 扩容 K8s 节点 │
│ 作用:当 Job Pod pending 时自动新增节点 │
└─────────────────────────────────────────────────────────┘
HPA 配置
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: gitlab-runner-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: gitlab-runner
minReplicas: 2 # 至少 2 个,保证高可用
maxReplicas: 5 # 最多 5 个,每个 50 并发 = 250 总并发
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # CPU 超过 70% 触发扩容
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80 # 内存超过 80% 触发扩容
并发数怎么定
concurrent 参数(单个 Manager Pod 最大并发 Job 数)是性能调优的核心旋钮。太大,Manager Pod 资源不够导致 OOM;太小,需要更多 Manager Pod 增加管理开销。
我的经验值:
| 场景 | concurrent | Manager Pod 数 | 总并发 | 适用规模 |
|---|---|---|---|---|
| 小团队 | 20 | 2 | 40 | 日构建 <100 次 |
| 中型团队 | 50 | 3 | 150 | 日构建 100-500 次 |
| 大型团队 | 100 | 5 | 500 | 日构建 >500 次 |
关键参数 request_concurrency:控制 Manager Pod 同时向 GitLab API 请求新 Job 的并发数。默认值是 1,意味着即使 concurrent=50,Manager 也只会一次拉取 1 个 Job。如果你发现 Job 启动有延迟(排队但 Manager 闲着),把 request_concurrency 调到 5-10。
# config.toml 关键配置
concurrent = 50
[[runners]]
limit = 50
request_concurrency = 10 # 默认1,调高后 Job 拉取更快
[runners.kubernetes]
namespace = "gitlab-runner"
poll_interval = "5s" # 默认3s,降低K8s API压力可调到5s
poll_timeout = "180s"
实测对比:Docker Machine vs Kubernetes Executor
在某出行项目迁移过程中,我记录了对比数据:
| 指标 | Docker Machine + EC2 | Kubernetes Executor + HPA | 提升幅度 |
|---|---|---|---|
| Job 启动延迟(冷启动) | 180-240s | 8-15s | 15-20 倍 |
| Job 启动延迟(热启动) | 30-60s | 3-5s | 10 倍 |
| 高峰期最大排队时间 | 30-40min | <30s | 60 倍 |
| 闲置资源成本 | EC2 空闲计费 | Pod 销毁即释放 | 月省 ~40% |
| 运维复杂度 | 维护 Docker Machine fork | 标准 K8s 运维 | 显著降低 |
冷启动差异的核心原因:Docker Machine 需要调用云 API 创建 EC2 → 等待 Running → SSH 连接 → 拉取 Runner 镜像 → 启动容器,整个链路 3-4 分钟。K8s Executor 只需要 K8s API 创建 Pod → 调度到已有节点 → 拉取镜像(有本地缓存的话秒级)→ 启动容器,30 秒内完成。
决策四:分布式缓存设计——让构建提速 3 倍
Cache vs Artifacts:90% 的人搞混
这是 GitLab CI 最容易被误解的两个概念:
- Cache:存储依赖包(如
node_modules/、.m2/),跨 Pipeline 复用。存在 Runner 本地或 S3 上。不保证可用——Runner 可以随时清空缓存。 - Artifacts:存储构建产物(如
target/app.jar、dist/),在同一个 Pipeline 的不同 Stage 间传递。存在 GitLab 实例上,有过期时间。
简单说:Cache 是"依赖缓存”,Artifacts 是"阶段产物"。很多团队把编译结果放 Cache 里,然后发现跨 Stage 取不到——因为 Cache 是跨 Pipeline 的,不是跨 Stage 的。跨 Stage 传文件要用 Artifacts。
S3 分布式缓存配置
单机 Runner 用本地缓存就够了,多 Runner 必须上分布式缓存。否则 Runner A 生的缓存在 Runner B 上取不到,等于没缓存。
# config.toml 分布式缓存配置
[runners.cache]
Type = "s3"
Shared = true # 关键!开启后所有 Runner 共享同一个缓存桶
[runners.cache.s3]
ServerAddress = "minio.internal.example.com"
BucketName = "gitlab-runner-cache"
Insecure = false
AuthenticationType = "access-key"
# AccessKey 和 SecretKey 通过环境变量注入,不写在配置文件里
Cache Key 设计策略
Cache Key 决定了缓存命中率。设计不好,要么缓存永远命中不了(每次都重新下载依赖),要么缓存串版本(不同分支的依赖混在一起)。
# .gitlab-ci.yml 缓存配置
# 策略1:按依赖文件 hash 缓存(推荐)
# 依赖文件不变就用缓存,变了就重新下载
cache:
key:
files:
- go.sum # Go 依赖锁文件
- package-lock.json # Node 依赖锁文件
paths:
- .cache/go-build/
- node_modules/
# 策略2:按分支缓存(适合不同分支依赖差异大的场景)
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .m2/repository/
# 策略3:按分支 + 依赖文件组合缓存(最精细)
cache:
key:
key: "$CI_COMMIT_REF_SLUG"
files:
- go.sum
paths:
- .cache/
我推荐策略 1(按依赖文件 hash),原因:
- 大多数项目的依赖在不同分支间是相同的,按分支缓存会导致每个分支都重新下载一遍,浪费
- 依赖文件 hash 变化时自动失效,不需要手动管理缓存版本
Shared = true配合下,所有分支共享同一份缓存,缓存利用率最高
实测缓存效果
以一个 Go 项目为例(200+ 依赖,go.sum 约 50KB):
| 场景 | 无缓存 | 有缓存(首次) | 有缓存(命中) |
|---|---|---|---|
go mod download | 45s | 45s | 2s |
go build | 12s | 12s | 4s |
| 总构建时间 | 82s | 82s | 23s |
缓存命中后构建时间从 82 秒降到 23 秒,提速 3.5 倍。对于一个日构建 200 次的团队,每天节省约 3.3 小时的构建时间。
踩坑:缓存串版本
在某电商平台的项目中踩过一个坑:开发分支用了 v1.2 的某依赖,主干分支用的是 v1.1。Cache Key 只用了项目名(key: "myproject"),导致开发分支的缓存被主干分支的 Job 取到,编译报错。
排查了 2 小时才发现是缓存串版本。修复方法就是把 Cache Key 改成依赖文件 hash:
# 修复前(错误)
cache:
key: "myproject"
paths:
- vendor/
# 修复后(正确)
cache:
key:
files:
- go.mod
paths:
- vendor/
决策五:.gitlab-ci.yml 流水线编排——include 和动态流水线
include 模板复用
当你有 10+ 个微服务,每个服务的 .gitlab-ci.yml 90% 逻辑相同时,维护 10 份配置文件是灾难。GitLab CI 的 include 关键字可以复用模板:
# .gitlab-ci.yml(单个微服务项目)
include:
- project: 'devops/ci-templates'
file: '/go-service.yml'
ref: 'main'
# 项目特定变量
variables:
SERVICE_NAME: "user-service"
REGISTRY: "registry.example.com"
# 只需要覆盖项目特有的配置
# 通用构建、测试、部署逻辑都在 go-service.yml 模板里
模板文件 go-service.yml:
# go-service.yml(共享模板)
stages:
- lint
- test
- build
- deploy
variables:
# 默认值,各项目可覆盖
GO_VERSION: "1.22"
lint:
stage: lint
image: golang:${GO_VERSION}
script:
- go fmt ./...
- go vet ./...
- golangci-lint run
test:
stage: test
image: golang:${GO_VERSION}
cache:
key:
files: [go.sum]
paths: [.cache/]
script:
- go test -race -coverprofile=coverage.out ./...
build:
stage: build
image: docker:24.0
services: [docker:24.0-dind]
script:
- docker build -t $REGISTRY/$SERVICE_NAME:$CI_COMMIT_SHORT_SHA .
- docker push $REGISTRY/$SERVICE_NAME:$CI_COMMIT_SHORT_SHA
动态流水线(Parent-Child Pipeline)
当流水线逻辑复杂到需要根据条件动态生成时,用 Parent-Child Pipeline。比如:只有 src/ 目录有变更才跑构建,只有 docs/ 有变更才跑文档部署。
# .gitlab-ci.yml(父流水线)
stages:
- generate
- trigger
generate-config:
stage: generate
script:
- |
cat > generated-config.yml << 'EOF'
include:
- project: 'devops/ci-templates'
file: '/go-service.yml'
EOF
# 如果 docs/ 有变更,追加文档部署 job
if git diff --name-only HEAD~1 | grep -q "^docs/"; then
cat >> generated-config.yml << 'EOF'
deploy-docs:
stage: deploy
script:
- make docs-deploy
EOF
fi
artifacts:
paths: [generated-config.yml]
trigger-child:
stage: trigger
trigger:
include:
- artifact: generated-config.yml
job: generate-config
strategy: depend # 父流水线等待子流水线完成
这种模式的好处是配置文件动态生成,灵活度极高。缺点是调试复杂——出错时需要先看父流水线的 generate-config job 输出,确认生成的配置是否正确。
rules vs only/except
GitLab 13.12 开始推荐用 rules 替代 only/except。rules 更灵活,支持 if 条件和 changes 匹配:
# 推荐:rules
build:
stage: build
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: never # MR 流水线不跑构建
- if: '$CI_COMMIT_BRANCH == "main"'
changes:
- src/**/*
when: on_success # main 分支 src/ 有变更才构建
- when: manual # 其他情况手动触发
script:
- make build
不推荐
only/except的原因:only/except语法有已知限制(不支持复杂条件组合,且 GitLab 官方不再为其添加新特性)。新项目直接用rules,老项目迁移时逐步替换。
决策六:节点隔离——Manager 和 Job 不能混跑
为什么要隔离
默认配置下,Manager Pod 和 Job Pod 可能调度到同一个 K8s 节点。这会带来两个问题:
- 资源争抢:Job Pod(构建任务)CPU/内存消耗大,会影响 Manager Pod 的调度稳定性
- 日志处理延迟:Manager Pod 需要处理 Job Pod 的日志流。如果 Manager 自己被高负载的 Job Pod 拖慢,日志延迟会导致 GitLab 界面上看不到实时输出
节点隔离方案
# 给 Manager 节点打 taint 和 label
kubectl taint nodes node-pool-manager runner.gitlab.com/manager=:NoExecute
kubectl label nodes node-pool-manager runner.gitlab.com/workload-type=manager
# 给 Worker 节点打 taint 和 label
kubectl taint nodes node-pool-worker runner.gitlab.com/job=:NoExecute
kubectl label nodes node-pool-worker runner.gitlab.com/workload-type=job
Manager Pod 的调度配置:
# Manager Pod 配置 nodeSelector 和 tolerations
spec:
nodeSelector:
runner.gitlab.com/workload-type: manager
tolerations:
- key: runner.gitlab.com/manager
operator: Exists
effect: NoExecute
Job Pod 的调度配置(在 config.toml 中):
[runners.kubernetes.node_selector]
"runner.gitlab.com/workload-type" = "job"
[runners.kubernetes.node_tolerations]
"runner.gitlab.com/job=" = "NoExecute"
隔离前后的对比
在某出行项目的 200 并发场景下,隔离前后的 Manager Pod 稳定性对比:
| 指标 | 隔离前 | 隔离后 | 改善 |
|---|---|---|---|
| Manager Pod OOM 次数(月) | 3-4 次 | 0 次 | 100% |
| 日志延迟投诉 | 每周 2-3 次 | 0 次 | 100% |
| Job Pod 创建延迟(P99) | 45s | 12s | 73%↓ |
| Manager CPU 利用率波动 | 30%-95% | 40%-70% | 显著平稳 |
隔离后 Job Pod 创建延迟下降的原因:Manager Pod 不再被 Job Pod 争抢 CPU,K8s API 调用响应更快。这个收益是在我预期之外的。
节点隔离的代价是需要额外的 Manager 节点。对于小团队(<20 并发),这个成本不划算。我的建议是:并发超过 50 就上节点隔离,否则不用折腾。资源管理的基础知识可以参考 相关文章:Kubernetes 资源管理:Requests 与 Limits。
决策七:监控与告警——用 Prometheus 代替"用户投诉"
关键指标
GitLab Runner 暴露了 Prometheus metrics 端点(:9252/metrics)。以下指标是我在生产环境必监控的:
| 指标 | 含义 | 告警阈值 |
|---|---|---|
gitlab_runner_jobs | 当前运行中的 Job 数 | 持续 = gitlab_runner_limit 超过 5 分钟 |
gitlab_runner_limit | 配置的最大并发数 | — |
gitlab_runner_request_concurrency_exceeded_total | 超出并发限制的请求次数 | 5 分钟内 >10 |
gitlab_runner_errors_total | Runner 错误总数 | 5 分钟内 >0 |
container_cpu_usage_seconds_total | Manager Pod CPU 使用率 | 持续 >70% |
container_memory_working_set_bytes | Manager Pod 内存使用率 | 持续 >80% |
Prometheus 告警规则
# Manager Pod CPU 持续高利用率
groups:
- name: gitlab-runner
rules:
- alert: RunnerManagerHighCPU
expr: |
rate(container_cpu_usage_seconds_total{pod=~"gitlab-runner.*"}[5m]) * 1000
/ kube_pod_container_resource_limits{pod=~"gitlab-runner.*", resource="cpu"} * 1000
> 70
for: 10m
labels:
severity: warning
annotations:
summary: "Runner Manager CPU 使用率持续超过 70%"
- alert: RunnerManagerOOM
expr: |
container_memory_working_set_bytes{pod=~"gitlab-runner.*"}
/ kube_pod_container_resource_limits{pod=~"gitlab-runner.*", resource="memory"}
> 0.8
for: 5m
labels:
severity: critical
annotations:
summary: "Runner Manager 内存使用率超过 80%,可能即将 OOM"
- alert: RunnerJobQueueSaturation
expr: |
gitlab_runner_jobs / gitlab_runner_limit > 0.9
for: 5m
labels:
severity: warning
annotations:
summary: "Runner Job 队列饱和度超过 90%"
- alert: RunnerErrors
expr: increase(gitlab_runner_errors_total[5m]) > 0
labels:
severity: critical
annotations:
summary: "Runner 5 分钟内出现错误"
性能阈值参考
GitLab 官方给出的性能阈值参考表:
| 指标 | 警告 | 严重 | 建议动作 |
|---|---|---|---|
| CPU 使用率 | 70% 持续 | 85% 持续 | 扩容或优化 |
| 内存使用率 | 限额的 80% | 限额的 90% | 增加 limits |
| API 错误率 | 请求的 2% | 请求的 5% | 排查瓶颈 |
| Job 排队时间 | 30 秒 | 2 分钟 | 审查容量 |
我建议把 Job 排队时间的告警阈值调得更激进:警告 15 秒,严重 60 秒。排队 30 秒在开发体验上已经很差了——用户推代码后等 30 秒流水线才开始跑,体感上就是"系统卡了"。
诊断命令
排查 Runner 性能问题时常用的命令:
# 查看 Manager Pod 当前资源使用
kubectl top pods --containers -l app=gitlab-runner
# 检查最近 2 小时的错误日志
kubectl logs -l app=gitlab-runner --since=2h | grep -E "(error|timeout|failed)"
# 查看 pending 的 Job Pod(如果持续 pending 说明资源不够)
kubectl get pods -n gitlab-runner --field-selector=status.phase=Pending
# 检查 Manager Pod 是否被 OOMKill 过
kubectl get pods -l app=gitlab-runner -o jsonpath='{.items[].status.containerStatuses[].lastState.terminated.reason}'
总结
GitLab CI Runner 弹性架构的核心不是"加机器",而是"选对执行器 + 规划好资源 + 设计好缓存"。7 个决策的优先级:
- 执行器选型是一切的基础。Docker Machine 已废弃,2027 年 5 月彻底移除。有 K8s 集群就选 Kubernetes Executor,没有就选 Docker Autoscaler。
- 资源规划用数据说话。CPU = 6m × 并发数 + 10m,内存 = 2.5MB × 并发数 + 50MB。内存留 100% 余量,别省。
- 弹性伸缩三层配合。Runner concurrent → HPA → Cluster Autoscaler,缺一不可。
- 分布式缓存是构建提速的关键。S3 共享缓存 + 依赖文件 hash 做 Cache Key,命中率最高。
- 流水线编排用 include 复用模板,用动态流水线做条件编排。
rules替代only/except。 - 节点隔离防止 Manager 和 Job 资源争抢。并发超过 50 就该做。
- 监控告警用 Prometheus 替代用户投诉。Job 排队超 15 秒就该告警。
最后说一句:这些决策不是一次性做对的。在某出行项目的实际落地中,我们花了 3 个月迭代了 4 个版本才稳定下来。第一版只做了执行器迁移,缓存没配;第二版加了 S3 缓存但 Key 设计不对,缓存命中率不到 30%;第三版改了 Key 策略但忘了做节点隔离,高峰期 Manager Pod OOM。第四版才把所有决策串起来,排队时间从 30 分钟降到 30 秒。CI/CD 平台建设是个持续优化的过程,别指望一步到位。
参考资料与致谢
- GitLab Runner Executors — GitLab 官方文档,执行器类型对比与选型指南
- Install and register GitLab Runner for autoscaling with Docker Machine — GitLab 官方文档,Docker Machine 废弃时间线说明
- Optimize GitLab Runner manager pod performance — GitLab 官方文档,Manager Pod 资源规划公式与性能测试数据
- Runner fleet configuration and best practices — GitLab 官方文档,Runner 集群设计最佳实践
- Caching in GitLab CI/CD — GitLab 官方文档,Cache 与 Artifacts 的区别及缓存策略
- Advanced configuration — GitLab 官方文档,config.toml 高级配置参数说明