2. GitLab Runner架构与选型

在 GitLab CI/CD 体系中,Pipeline 的 YAML 只描述“要做什么”,真正把作业跑起来的是 GitLab Runner。Runner 是安装在目标机器上的独立进程,它主动向 GitLab 实例发起长轮询,领取属于自己的 Job,在本地创建执行环境、拉取代码、运行脚本,再把日志和产物回传。理解 Runner 的架构与选型,本质上是在回答三个问题:作业在哪里跑、用什么隔离方式跑、如何把作业精确地送到合适的机器上。

一、Runner 的两种类型:项目级与共享级

从归属关系看,Runner 分为三类。项目级 Runner(Specific Runner)只服务某一个项目,注册时绑定 project token,适合对构建环境有强定制要求的项目,例如需要特定硬件、特定内网依赖或固定 IP 白名单。群组级 Runner(Group Runner)绑定 group token,服务该群组下所有项目,是中型团队最常用的形态,既避免每个项目重复维护机器,又能按业务域隔离资源。共享级 Runner(Shared Runner)绑定实例级 token,服务整个 GitLab 实例的所有项目,适合通用型、轻量级作业,但通常不承载敏感或重资源任务。

选型的第一条原则是:越靠近项目的 Runner,控制力越强、复用度越低;越靠近实例的 Runner,复用度越高、隔离性越弱。实践中常见的组合是“共享 Runner 跑通用测试 + 群组 Runner 跑业务构建 + 项目 Runner 跑特殊部署”。

二、执行器差异:决定隔离性与并发能力

执行器(Executor)是 Runner 真正创建作业环境的机制,它比 Runner 类型更影响实际行为。常见执行器对比如下:

执行器隔离方式典型场景关键限制
shell无隔离,直接复用宿主环境单机固定环境、内网部署机并发作业互相污染,缓存与依赖易冲突
docker容器隔离,每 Job 一个容器绝大多数构建、测试任务需处理镜像拉取、DinD 或挂载 socket
docker-machine容器 + 自动创建云主机弹性伸缩的云上构建依赖云驱动,机器创建有延迟
kubernetesPod 隔离,按需调度大规模、弹性、多租户集群需集群与 RBAC 配套,冷启动有开销
docker-windowsWindows 容器隔离.NET、Windows 构建镜像体积大,节点资源占用高

选择执行器的核心判断是“作业之间是否需要强隔离”。如果多个 Job 会并发修改同一份工作目录、同一套全局依赖,shell 执行器几乎必然出问题;而 docker 执行器通过 image 指定基础镜像,天然做到每个 Job 环境独立。对于需要构建镜像的作业,通常采用 Docker-in-Docker(DinD)或在宿主机挂载 /var/run/docker.sock,前者隔离更彻底但需要 privileged 权限,后者性能更好但存在安全边界问题。

三、注册机制:token 与配置的绑定过程

Runner 注册的本质是把一个本地进程与 GitLab 上的某个“Runner 记录”绑定,并协商出认证 token。注册时 Runner 会向 GitLab 的 /api/v4/runners 提交注册 token、标签、执行器、描述等信息,GitLab 返回一个 runner token,Runner 将其写入本地 config.toml。此后每次领取 Job,Runner 都携带该 token 轮询 /api/v4/jobs/request。

# 注册一个群组级 Runner(示例参数)
gitlab-runner register \
  --url "https://gitlab.example.com/" \
  --registration-token "GR1348941xxxxxx" \
  --executor "docker" \
  --docker-image "alpine:3.19" \
  --description "group-build-runner-01" \
  --tag-list "build,linux,docker" \
  --run-untagged="false" \
  --locked="false"

注册完成后,config.toml 中的 [[runners]] 段落记录了 url、token、executor 和标签。需要特别注意:标签在注册时写入,但后续可以在 GitLab 界面修改并同步到 Runner;而 run_untagged 决定该 Runner 是否接受没有标签的 Job,默认 false 意味着只有带匹配标签的 Job 才会被领取。

四、标签路由:Job 如何找到正确的 Runner

标签(tag)是 GitLab 调度 Job 的唯一路由依据。Job 在 .gitlab-ci.yml 中通过 tags 声明需求,Runner 在注册时声明自己拥有的标签,GitLab 只把 Job 派发给“标签集合是 Job 标签超集”的 Runner。例如 Job 声明 tags: [build, linux],那么拥有 build,linux,docker 的 Runner 可以领取,只拥有 build 的 Runner 则不行。

路由失败最常见的现象是 Job 一直处于 pending,日志提示 “This job is stuck because you don't have any active runners online or available with the tags”。排查顺序是:确认 Runner 在线、确认标签拼写与大小写一致、确认 run_untagged 与 Job 是否带标签匹配。大规模场景下,标签设计应遵循“维度化”原则,例如用 os:linux、arch:arm64、cap:gpu、env:prod 这类语义化标签,而不是随意命名,否则标签会迅速膨胀成难以维护的集合。

GitLab Runner 注册与标签路由架构 GitLab 实例 Job 调度 / 标签匹配 / token 校验 ① 注册请求 token + tags + executor ② 返回 runner token Runner 进程(config.toml) url / token / executor / tags executor docker / k8s tags build,linux ③ 轮询 .gitlab-ci.yml Job tags: [build, linux] image: node:20 script: npm ci && npm test ④ 提交 标签匹配判断 Runner.tags ⊇ Job.tags ? 且 Runner 在线、未达并发上限 调度 匹配成功 → 领取 Job 创建容器 / Pod 执行脚本 匹配失败 → Job pending 提示无可用 Runner / 标签不符

五、大规模场景下的选型策略

当项目数量从几十增长到几百、每日 Pipeline 从数百增长到数万时,Runner 选型不再是单机问题,而是容量与治理问题。可以从四个维度展开。

第一,按作业特征分层。把作业分为“轻量通用型”(lint、单元测试)、“重资源型”(编译、镜像构建)、“特殊环境型”(GPU、ARM、Windows、内网部署)。轻量作业交给共享 Runner 或 Kubernetes 执行器,重资源作业交给带标签的专用 Runner 池,特殊环境作业用项目级 Runner 固定绑定。这样既避免重作业挤占通用池,也避免为每个项目单独维护机器。

第二,并发与资源配比。Runner 的 concurrent 控制全局并发,[[runners]] 下的 limit 控制单个 Runner 的并发。经验上,CPU 密集型作业的并发数不宜超过“可用核数 / 单作业核数”,否则会出现大量上下文切换导致整体变慢。例如一台 16 核 64GB 的构建机,单作业需要 4 核 8GB,则并发上限设为 4 比较稳妥;若设为 8,虽然队列变短,但单作业耗时可能上升 30% 以上。

第三,缓存与产物策略。大规模下缓存命中率直接决定构建时长。Docker 执行器应配置 volumes 挂载宿主机缓存目录,或使用分布式缓存(如 S3、MinIO)配合 cache: 关键字。Kubernetes 执行器则需借助 PVC 或对象存储,否则每次 Pod 重建都会丢失缓存。

第四,可观测与弹性。应监控 Runner 的在线数、pending Job 数、平均排队时长、Job 失败率。当 pending 持续升高时,优先考虑增加 Runner 实例或提高并发,而不是盲目扩大单机规格。Kubernetes 执行器可结合 Cluster Autoscaler 实现按需扩容,但要注意节点冷启动时间通常为 30~90 秒,对短作业不友好,可保留一个小规模常驻节点池承接突发流量。

标签设计建议采用“能力维度 + 环境维度”组合,例如 cap:docker、cap:gpu、os:linux、env:staging。避免使用项目名作为标签,否则 Runner 会退化成项目专属,失去复用价值。
使用 shell 执行器时,多个 Job 共享同一工作目录和全局依赖,极易出现“本地能跑、CI 失败”或“两个 Job 互相覆盖文件”的问题。除非作业之间严格串行且环境完全固定,否则不建议在大规模场景使用 shell 执行器。

综合来看,Runner 选型的落脚点是一张“作业特征—执行器—Runner 类型—标签”的映射表。先明确作业需要什么环境、是否需要隔离、是否重资源,再决定用哪种执行器和哪一级 Runner,最后用标签把路由规则固化下来。这样在项目规模扩张时,只需增加对应标签的 Runner 容量,而不必重构整条流水线。