一、一个Runner,成了全村的希望
小区门口只有一个快递柜,双11来了,快递小哥排着队往那个柜子里塞,塞不进去就堆在架子上。你心里那个急:为什么不多配几个柜子?为什么柜子大小不一样?为什么有的柜子只能装小包裹,有的能装大件?其实公司里的CI流水线也一样。一开始项目不多,一个Runner(也就是帮你跑构建、测试、部署的小机器)绰绰有余。后来项目多了,大家都把Pipeline扔到这个Runner上,问题来了:任务开始排队,一个构建卡住,后面全是红点。这不是Runner不够努力,而是它的能力上限就摆在那里。
大家有没有想过,为什么有的任务必须等?因为单个Runner在任何时间点能处理的job数量有限,而且不同类型的任务挤在一起,互相干扰。比如一个跑测试的CPU密集型任务,和一个拉代码、打包的I/O密集型任务,同时挤在一个Runner上,谁也快不了。这就引出了两个核心思路:一是给任务打标签,让它们分门别类地找合适的Runner;二是搭建分布式架构,让多个Runner各司其职,甚至按需扩容。
二、为什么会排队?先看看Runner的脾气
Runner本身没什么脾气,但它有配置。GitLab Runner有一个“并发数”的概念,就是你允许它在同一时间最多跑多少个任务。
# 查看当前Runner的并发配置
# 配置文件默认在 /etc/gitlab-runner/config.toml
cat /etc/gitlab-runner/config.toml
一般安装后默认并发数是1,也就是说,同一时刻只能执行一个job。后面来的任务只能进队列等。如果任务队列又长又重,那体验就像春运排火车票。
即便你把并发数调到4,也不是万事大吉。因为你的Runner可能是一台只有2核4G内存的小机器,同时跑4个构建任务,CPU直接跑满,内存不够用,结果每个任务都变慢,比排队更煎熬。这就像一个收银员同时给4个顾客结账,结果每个订单都算错,还不如一个一个来。
所以,单靠调并发数解决不了本质问题。我们需要的是“多柜子”和“分门别类”。
三、多标签分配:让合适的Runner干合适的活
3.1 标签是什么?
标签(Tags)是GitLab Runner上的一组标识符。你可以把一个Runner上的标签理解为它的“技能牌”。比如有一个Runner装了Docker环境,你给它贴上docker标签;另一个Runner内存很大,专门跑耗时测试,你给它贴上test和memory标签。然后在Pipeline配置里,给任务指定这些标签,GitLab调度器看到后,就会把任务只派发给“会这些技能”的Runner。
3.2 给Runner打标签
注册Runner时,用--tag-list就能指定标签。看下面这个例子:
# 注册一个Runner,标签设为 backend 和 docker
# 这样它只会接带 backend 或 docker 标签的活
gitlab-runner register \
--url https://gitlab.example.com \
--token glrt-xxxxxxxx \
--non-interactive \
--executor docker \
--docker-image alpine:3.18 \
--tag-list "backend,docker" \
--description "后端构建专用"
注意到没有?--tag-list后面跟的是一个逗号分隔的列表。注册成功后,这个Runner就被打上了两个标签。如果哪天你想改标签,去/etc/gitlab-runner/config.toml里把tags数组改一下,再重启Runner就好。
3.3 在流水线里用标签挑Runner
接下来,我们在项目的.gitlab-ci.yml里写任务,并指名要哪些标签:
# 这是一个简单的GitLab CI配置示例
# 技术栈:GitLab Runner + Shell
stages:
- build
- test
# 后端构建任务,只找带 backend 标签的 Runner
build-backend:
stage: build
tags:
- backend
script:
- echo "开始编译后端代码"
- mkdir -p build
- touch build/app.jar
# 前端构建任务,只找带 frontend 标签的 Runner
build-frontend:
stage: build
tags:
- frontend
script:
- echo "开始编译前端代码"
- mkdir -p dist
- touch dist/index.html
# 测试任务,需要 backend 且同时有 docker 标签的 Runner
test-backend:
stage: test
tags:
- backend
- docker
script:
- echo "运行后端测试"
- sleep 5
上面配置里,test-backend这个任务要求Runner同时拥有backend和docker两个标签。如果一个Runner只有backend没有docker,它是不会被选中的。这就像点外卖时选了“麻辣香锅”和“不要香菜”,商家必须同时满足两个条件才接单。
多标签分配还有一个好处:它天然地把不同类型的负载分散开了。前端构建任务不会跟后端构建任务挤在同一台机器上,因为它们各自有专属的Runner。这样即使某个Runner挂了,其他标签的Runner不受影响,排队现象也会减轻。
3.4 标签分配容易踩的坑
第一,不写tags的任务,只会交给那些没有标签的Runner。如果你所有Runner都有标签,这些任务就会变成“无家可归”,一直卡在队列里。所以要么给Runner留一个“通吃”的角色,要么在流水线里每个任务都写清标签。
第二,标签别设太多,也不好。标签多了,每个Runner能接的任务范围就被限制得死死的。比如你给一个Runner打了backend、frontend、test、deploy四个标签,那它跟没打标签也没区别,什么活都接,最后又成了全村的希望。
第三,标签和实际环境要匹配。别给一个没装Docker的Runner贴上docker标签,到时候任务派过去了,跑起来报错,比排队还闹心。
四、分布式架构:从“一个人扛”到“一群人干”
多标签分配解决了“谁来做”的问题,但没解决“做不过来”的问题。比如所有标签都是同一个Runner,那标签再多也是分身乏术。真正要治本,得把Runner拆成多个,部署在不同的机器上,甚至按需动态伸缩。
4.1 分布式不是多买几台机器就完事
分布式架构听起来高级,说白了就是:别让一个Runner扛所有活,让多个Runner各管一摊。比如你有三台机器,一台只跑后端构建,一台只跑前端构建,一台专门做发布。这三台机器相当于三个独立的小店,各卖各的,互不抢生意。
从运维角度看,每个Runner还是用GitLab Runner这个工具,只是它们注册到同一个GitLab实例,各自有不同的标签,并发数也可以分别设置。这样总体的吞吐量就上去了。
4.2 用Docker Executor让Runner随用随走
前面示例里的Runner用的是docker executor。这是什么意思呢?就是说每个任务在运行时,Runner会从镜像里拉一个容器,然后在容器里执行脚本。任务结束,容器就销毁。这样做的好处是环境干净,不会因为上一个任务残留东西影响下一个任务。
来看一个完整的Docker executor配置示例:
# GitLab Runner配置示例,位于 /etc/gitlab-runner/config.toml
concurrent = 4 # 这个Runner最多同时跑4个任务
[[runners]]
name = "shared-docker-runner"
url = "https://gitlab.example.com"
token = "glrt-xxxxxxxx"
executor = "docker"
tags = ["build", "common"] # 这个Runner接 build 和 common 标签
[runners.docker]
image = "alpine:3.18" # 默认镜像
volumes = ["/var/run/docker.sock:/var/run/docker.sock"] # 允许容器内使用docker命令
privileged = false # 不用特权模式,更安全
这个Runner就像一个“云餐厅”:每个顾客来了,后厨单独开一桌做菜,做完收拾干净,再接待下一拨。由于每个任务都是隔离的,就算某个任务把环境搞得乌烟瘴气,也不会影响其他人。
4.3 更高阶:利用Kubernetes动态扩容
如果你想再进一步,可以设置支持Kubernetes executor的Runner。用Kubernetes做底层,Runner接到任务时,会临时创建一个Pod来执行任务,任务结束Pod就被销毁。这样整个构建集群可以动态伸缩,任务多的时候多开几个Pod,空闲的时候缩回来,资源利用率极高。
不过需要注意,Kubernetes executor需要提前准备好集群和相关权限,配置也会复杂一些。对于大多数中小团队来说,先把多标签分配和几台固定Runner做好,已经能解决90%的排队问题。
五、应用场景和优缺点
5.1 这种方案适合谁?
- 团队里有多个项目,且构建环境差异大(比如一个Java一个Node)。
- 某个核心项目频繁跑Pipeline,占用了Runner的大量时间,导致其他项目排队。
- 有部分构建任务要求特殊硬件,比如GPU、大内存或者专用网络。
5.2 优点
- 隔离性好:不同标签的任务用不同Runner,环境互相不干扰。
- 扩展方便:新增一个Runner,注册后打个标签就能用。
- 调度灵活:任务按标签精准选择执行环境,避免“用牛刀杀鸡”或者“小马拉大车”。
- 故障域小:某个Runner挂了,只影响对应标签的任务,其他任务照常跑。
5.3 缺点
- 资源成本高了:需要维护多台Runner机器,而不是一台。
- 配置变得复杂:标签规划、并发调优、安全配置都要花心思。
- 标签设计不合理时,反而会导致Runner闲置或任务无人接。
六、注意事项
- 标签命名要统一。建议用
build、test、deploy这种业务含义清晰的词,不要用机器名。 - 监控Runner的负载。可以用GitLab的Runner管理页面,或者自己写脚本看任务队列长度。
- 保持Runner镜像和依赖同步。多台Runner的镜像版本不一致,可能会导致同一个任务在不同Runner上行为不一致。
- 注意Runner的注册Token安全。泄露Token等于让别人随便往你的集群里塞Runner。
- 如果用了Docker executor,不要在容器里直接跑Docker daemon,而是挂载宿主机的
/var/run/docker.sock。当然这也带来了安全风险,需要谨慎使用。
七、总结
单个Runner忙不过来,Pipeline就开始排队,本质上是因为“所有鸡蛋放在一个篮子里”。通过多标签分配,我们可以让任务精准地找到合适的Runner;通过分布式架构,我们可以把负载摊到多台机器上,甚至按弹性伸缩。两者结合,就是一套既灵活又稳定的CI/CD基础设施。别再让一个Runner独自扛着全村的希望了,学会“多柜子、分标签、分布式”,Pipeline排队的问题也就迎刃而解。
评论
围绕“单一Runner负载不均导致Pipeline排队?设计多标签分配与分布式架构”参与讨论