2. Runner核心架构与执行模型
要治理 Runner 集群,先要看清一个 Runner 内部到底由哪些角色组成,以及一次作业从排队到结束究竟消耗了哪些资源。很多团队把“Runner”当成一个整体来扩容,结果发现加了机器仍然排队,或者单机负载忽高忽低,根源往往是没有区分 Runner 进程、Executor 执行器和底层运行环境这三层。
三层结构:Runner、Executor 与运行环境
GitLab Runner 是一个用 Go 编写的守护进程,它本身不执行作业,只负责三件事:向 GitLab 注册并领取作业、把作业交给某个 Executor、把日志和结果回传。真正决定“作业在哪里跑、怎么隔离、消耗什么资源”的是 Executor。可以把它理解为:Runner 是调度外壳,Executor 是执行策略,Shell、Docker、Kubernetes 是三种不同的落地环境。
常见的 Executor 差异可以这样理解:
- Shell Executor:直接在 Runner 所在主机的 shell 中执行,作业脚本与主机共享文件系统、环境变量和用户权限。启动最快、开销最小,但隔离性最差,一个作业写坏主机目录会影响后续所有作业。
- Docker Executor:每个作业启动一个容器,镜像由
image指定,服务由services指定。隔离性好、环境可复现,但每次作业都要拉镜像、创建容器、挂载卷,启动延迟和磁盘 IO 明显高于 Shell。 - Kubernetes Executor:Runner 把作业转成 Pod 提交给集群,由 K8s 调度到节点。弹性最好、资源按 Pod 申请,但受集群调度、镜像拉取、PVC 挂载影响,冷启动最慢,且需要处理命名空间、ServiceAccount、资源配额等集群侧约束。
选择 Executor 本质上是在“隔离性、启动速度、弹性、运维复杂度”之间做权衡。Shell 适合构建机固定、追求速度的场景;Docker 适合大多数需要环境一致性的 CI;Kubernetes 适合负载波动大、希望按需伸缩的集群。
作业生命周期与资源消耗特征
一次作业并不是“跑脚本”那么简单。它经历 pending、preparing、running、cleanup、finished 五个阶段,每个阶段的资源画像完全不同。理解这一点,才能解释为什么“CPU 不高却一直排队”或“磁盘总是满”。
pending:作业在 GitLab 侧排队,等待有空闲槽位的 Runner。此时 Runner 本机几乎不消耗资源,但队列长度直接反映并发能力是否足够。如果 concurrent 设置过小,或所有 Runner 都被长作业占满,pending 会持续堆积。
preparing:Runner 领取作业后开始准备环境。Docker Executor 会拉取镜像、创建容器、挂载缓存卷;Kubernetes Executor 会创建 Pod、拉镜像、挂载 PVC。这个阶段的主要消耗是网络带宽和磁盘 IO,CPU 反而不高。镜像越大、层越多,preparing 越久。很多“作业慢”其实慢在这里,而不是脚本本身。
running:脚本真正执行,是 CPU 和内存的峰值阶段。编译、测试、打包类作业会短时间吃满多核;如果多个作业共享同一台 Runner,running 阶段会互相争抢。Docker 和 K8s 可以通过 limits 限制单作业资源,Shell 则没有天然隔离,需要靠外部手段约束。
cleanup:上传产物、推送缓存、删除容器或 Pod、清理临时目录。这个阶段容易被忽视,但它会占用磁盘空间和网络带宽。如果容器或镜像长期不清理,磁盘会被逐步吃满,最终导致新作业无法 preparing。
下面是一份用于观察单作业资源画像的示例配置片段,参数为教学示例,实际值需按项目调整:
# config.toml 片段(示例)
concurrent = 8
[[runners]]
name = "docker-runner-01"
executor = "docker"
[runners.docker]
image = "alpine:3.19"
privileged = false
volumes = ["/cache"]
# 单作业资源上限(示例值)
memory = "2g"
cpus = "2"
[runners.cache]
Type = "s3"
Path = "runner-cache"
观察作业各阶段耗时,最直接的方式是看 GitLab 作业日志中的时间戳,或使用 Runner 的 Prometheus 指标。例如 gitlab_runner_job_duration_seconds 反映作业总时长,配合日志可以判断时间花在 preparing 还是 running。如果 preparing 占比过高,优先优化镜像体积和缓存策略;如果 running 占比高且 CPU 打满,考虑提高并发或拆分作业。
并发模型与槽位
Runner 的并发由两层控制:全局 concurrent 限制该 Runner 同时能跑多少作业,单个 [[runners]] 下的 limit 限制该注册项的最大并发。两者取较小值生效。例如全局 concurrent=8、某 runner limit=3,则该 runner 最多同时跑 3 个作业。槽位被占满后,新作业只能等待,这就是 pending 的直接来源。
需要特别注意的是,Shell Executor 的“槽位”并不等于资源隔离。8 个并发 Shell 作业会同时争抢同一主机的 CPU、内存和磁盘,一个作业的 rm -rf 可能影响其他作业。因此 Shell 更适合并发低、作业可信的场景;高并发场景应优先 Docker 或 Kubernetes,用容器或 Pod 做资源边界。
把这三层关系记牢:Runner 负责领取和回传,Executor 决定隔离方式,运行环境决定资源边界。作业生命周期的每个阶段消耗不同资源,治理时先定位瓶颈在 pending、preparing 还是 running,再决定是加并发、换 Executor,还是优化镜像与缓存。