概述

凌晨 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、中等并发活跃
KubernetesPod 隔离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。原因:

  1. 原生弹性伸缩:配合 HPA + Cluster Autoscaler,Pod 级别的扩缩容比 VM 快 10 倍以上(秒级 vs 分钟级)
  2. 资源利用率高:Job Pod 执行完即销毁,不占用闲置资源。Docker Machine 方式下,EC2 实例即使空闲也要计费直到被回收
  3. 运维统一:Runner 管理面和 Job 执行面都在 K8s 体系内,用 kubectl 就能排查问题,不需要额外维护 Docker Machine 工具链
  4. 调度能力强:支持 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峰值内存
25160m112MB
50308m261MB
75460m237MB
100657m369MB

从数据可以推导出资源消耗公式:

  • 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 增加管理开销。

我的经验值:

场景concurrentManager Pod 数总并发适用规模
小团队20240日构建 <100 次
中型团队503150日构建 100-500 次
大型团队1005500日构建 >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 + EC2Kubernetes Executor + HPA提升幅度
Job 启动延迟(冷启动)180-240s8-15s15-20 倍
Job 启动延迟(热启动)30-60s3-5s10 倍
高峰期最大排队时间30-40min<30s60 倍
闲置资源成本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.jardist/),在同一个 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),原因:

  1. 大多数项目的依赖在不同分支间是相同的,按分支缓存会导致每个分支都重新下载一遍,浪费
  2. 依赖文件 hash 变化时自动失效,不需要手动管理缓存版本
  3. Shared = true 配合下,所有分支共享同一份缓存,缓存利用率最高

实测缓存效果

以一个 Go 项目为例(200+ 依赖,go.sum 约 50KB):

场景无缓存有缓存(首次)有缓存(命中)
go mod download45s45s2s
go build12s12s4s
总构建时间82s82s23s

缓存命中后构建时间从 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/exceptrules 更灵活,支持 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 节点。这会带来两个问题:

  1. 资源争抢:Job Pod(构建任务)CPU/内存消耗大,会影响 Manager Pod 的调度稳定性
  2. 日志处理延迟: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)45s12s73%↓
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_totalRunner 错误总数5 分钟内 >0
container_cpu_usage_seconds_totalManager Pod CPU 使用率持续 >70%
container_memory_working_set_bytesManager 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 个决策的优先级:

  1. 执行器选型是一切的基础。Docker Machine 已废弃,2027 年 5 月彻底移除。有 K8s 集群就选 Kubernetes Executor,没有就选 Docker Autoscaler。
  2. 资源规划用数据说话。CPU = 6m × 并发数 + 10m,内存 = 2.5MB × 并发数 + 50MB。内存留 100% 余量,别省。
  3. 弹性伸缩三层配合。Runner concurrent → HPA → Cluster Autoscaler,缺一不可。
  4. 分布式缓存是构建提速的关键。S3 共享缓存 + 依赖文件 hash 做 Cache Key,命中率最高。
  5. 流水线编排用 include 复用模板,用动态流水线做条件编排。rules 替代 only/except
  6. 节点隔离防止 Manager 和 Job 资源争抢。并发超过 50 就该做。
  7. 监控告警用 Prometheus 替代用户投诉。Job 排队超 15 秒就该告警。

最后说一句:这些决策不是一次性做对的。在某出行项目的实际落地中,我们花了 3 个月迭代了 4 个版本才稳定下来。第一版只做了执行器迁移,缓存没配;第二版加了 S3 缓存但 Key 设计不对,缓存命中率不到 30%;第三版改了 Key 策略但忘了做节点隔离,高峰期 Manager Pod OOM。第四版才把所有决策串起来,排队时间从 30 分钟降到 30 秒。CI/CD 平台建设是个持续优化的过程,别指望一步到位。

参考资料与致谢

  1. GitLab Runner Executors — GitLab 官方文档,执行器类型对比与选型指南
  2. Install and register GitLab Runner for autoscaling with Docker Machine — GitLab 官方文档,Docker Machine 废弃时间线说明
  3. Optimize GitLab Runner manager pod performance — GitLab 官方文档,Manager Pod 资源规划公式与性能测试数据
  4. Runner fleet configuration and best practices — GitLab 官方文档,Runner 集群设计最佳实践
  5. Caching in GitLab CI/CD — GitLab 官方文档,Cache 与 Artifacts 的区别及缓存策略
  6. Advanced configuration — GitLab 官方文档,config.toml 高级配置参数说明