CI 算力饥渴症:Runner 弹性伸缩的架构权衡与落地

每个 CI 平台的负责人迟早会被同一类工单轰炸:白天十点,流水线排队二十分钟,业务团队问「为什么这么慢」;晚上十点,几十台构建机器空转,财务问「这些机器是干嘛的」。

这就是 CI 基础设施的算力饥渴症:峰值要得起,谷值养不起。构建负载天然是潮汐型的------上班高峰是谷点的三到五倍,而流水线对延迟又极其敏感,排队的每一分钟都在放大研发的等待成本。

本文不谈概念,只谈架构:三种弹性方案的取舍逻辑、关键参数怎么定、以及生产环境里真正会踩的坑。

一、先把问题定义清楚

自托管 Runner 的弹性,本质是回答三个问题:

  1. 扩容信号从哪来------是等任务排队了再扩,还是按时间表预扩?
  2. 扩容粒度是什么------加一台 VM,还是加一个 Pod?
  3. 冷启动代价多大------从「需要算力」到「算力可用」要等多久?

传统方案(虚拟机池 + 定时扩缩容)的答案分别是:时间表、整台 VM、分钟级。这三个答案在今天都不及格------定时预扩需要人工维护时刻表,整台 VM 粒度太粗浪费资源,分钟级冷启动在高峰期就是雪上加霜。

二、Kubernetes 执行器:为弹性而生的架构

把 Runner 放到 Kubernetes 上,架构就变了。依据官方文档,极狐GitLab Runner 的 Kubernetes 执行器为每个 CI 作业创建一个 Pod ,Pod 内由三个容器协作:build(执行构建)、helper(克隆代码、恢复缓存、上传产物)、service 容器(数据库等依赖服务)。

作业结束,Pod 销毁,资源归还集群。这个「一作业一 Pod」的模型,把弹性粒度从「一台机器」降到了「一个容器」,扩容决策也从「运维拍脑袋」变成了「调度器自动权衡」。

基础配置并不复杂(config.toml):

toml 复制代码
concurrent = 4

[[runners]]
  name = "k8s-runner"
  url = "https://gitlab.example.com"
  token = "......"
  executor = "kubernetes"
  [runners.kubernetes]
    namespace = "gitlab-runner"
    cpu_limit = "1"
    memory_limit = "1Gi"
    helper_cpu_limit = "500m"
    helper_memory_limit = "128Mi"
    poll_interval = 5
    poll_timeout = 3600

几个参数的定值逻辑:

  • concurrent:单个 Runner 进程的并发作业上限。它不是越大越好------每个并发作业都会占用 helper 容器和 API 轮询开销,通常单实例 10-30 之间,需要更多吞吐就横向加 Runner 副本;
  • cpu_limit / memory_limit:作业 Pod 的资源上限。这里有个容易被忽视的细节:helper 容器需要单独配额,有缓存和产物上传的工作负载至少给 250MiB 内存,否则克隆大仓库时 helper 会被 OOMKill,报错信息却指向构建脚本,极具迷惑性;
  • poll_timeout:默认 180 秒。当排队的构建数超过集群处理能力时会用到它,高峰期建议放大,避免误判超时。

三、真正的杀手锏:暂停 Pod 预热集群容量

「一作业一 Pod」解决了粒度问题,但冷启动问题还在:作业 Pod 需要 2 核 4G,集群当前没余量,就得等集群自动扩缩器(Cluster Autoscaler 或云厂商的节点池)拉起新节点------这通常要一到三分钟。

极狐GitLab Runner 的解法相当优雅:用低优先级的暂停 Pod 占住容量,等真实作业来了再被抢占。工作原理分四步:

  1. Runner 创建一个由暂停 Pod 组成的 Deployment,镜像默认为 registry.k8s.io/pause:3.10,几乎不消耗 CPU;
  2. 暂停 Pod 使用低优先级类(默认自动创建 gitlab-runner-idle-capacity,优先级 -1),通过设置 cpu_request / memory_request 把节点资源「预定」下来;
  3. 作业 Pod 到来时,Kubernetes 调度器抢占暂停 Pod------作业立即占住现有节点,零冷启动;
  4. Deployment 重建被抢占的暂停 Pod,如果节点装不下,就触发集群自动扩缩器加节点,为下一波作业备货。

这相当于把「等节点」变成「先用着,后台补货」。配置示例:

toml 复制代码
[[runners]]
  executor = "kubernetes"
  [runners.kubernetes]
    namespace = "gitlab-runner"
    cpu_request = "500m"
    memory_request = "1Gi"
    [runners.kubernetes.autoscaler]
      max_pause_pods = 10          # 暂停 Pod 上限,0 为无限制
      [[runners.kubernetes.autoscaler.policy]]
        idle_count = 5
        periods = ["* 8-17 * * mon-fri"]   # 工作日 8-17 点维持 5 个
        timezone = "Asia/Shanghai"
      [[runners.kubernetes.autoscaler.policy]]
        idle_count = 0
        periods = ["* * * * *"]            # 其余时间不维持

这套配置的含义:工作日白天维持 5 个暂停 Pod 的热备容量(按上面的 request 算即 2.5 核 5G),下班后和周末全部释放,资源账单跟着业务节奏走。

对于负载波动更剧烈的场景,还有 scale_factor 参数:设置为 0.5 时,维持的暂停 Pod 数等于 max(idle_count, 活跃作业数 × 0.5)------作业越多,预备容量越多,扩容曲线自动跟随真实负载,scale_factor_limit 兜底上限。

四、生产落地的三个坑

坑一:RBAC 权限不足,扩缩器静默失效。 自动扩缩器需要操作 deployments 和集群级的 priorityclasses 资源,普通 namespace 级 Role 授权不够。正确做法是通过 Helm Chart 的 rbac.create=true 让 Runner 服务账户获得所需权限,或在 ClusterRole 中显式加入:

yaml 复制代码
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "create", "update", "delete"]
- apiGroups: ["scheduling.k8s.io"]
  resources: ["priorityclasses"]
  verbs: ["get", "create"]

部署后第一时间验证暂停 Pod 是否真的建起来了(kubectl get pods -n gitlab-runner),权限缺失时它只会记录警告,不会报错。

坑二:抢占不是免费的。 暂停 Pod 被抢占会触发重建,如果 idle_count 设得激进而节点池弹性差,会出现「暂停 Pod 与作业 Pod 互相挤兑」的抖动。建议 idle_time(缩容冷却,默认 5 分钟)保持默认,初期 idle_count 从小值起步,用一周的真实排队数据再回调。

坑三:别忘了缓存层。 Pod 是临时的,作业每次冷启动都要重新拉依赖,弹性做得再快也会被「mvn 下载三分钟」拖垮。把 Runner 缓存指向对象存储(S3/MinIO),或配合依赖代理预热镜像,才能让「秒级扩容」真正兑现为「秒级反馈」。

五、架构选型:一张对比表收尾

维度 虚拟机池定时扩缩 Kubernetes + 节点自动扩缩 K8s + 暂停 Pod 预热
弹性粒度 整台 VM 单 Pod 单 Pod
冷启动 分钟级(扩节点) 分钟级(扩节点) 秒级(抢占现有节点)
扩容信号 时间表 队列积压 时间表 + 负载因子 + 抢占联动
资源利用率 低(谷期空转) 高(暂停 Pod 几乎零耗)
运维复杂度 低但维护成本高 中(多一层策略配置)

结论很直接:如果团队已有 Kubernetes 基础设施,「K8s 执行器 + 暂停 Pod 预热」是当前弹性与成本的最优平衡点;如果暂时没有集群,先从虚拟机池的定时策略过渡,但扩容信号一定要尽早从「时间表」迁移到「真实队列长度」。

CI 算力饥渴症的根治,从来不是买更多机器,而是让每一份算力在最需要的时候刚好在场。Runner 弹性伸缩的完整参数说明,可查阅极狐GitLab Runner 官方文档。如果你的团队正在规划大规模 CI 集群,也欢迎在评论区聊聊你们峰谷比是多少。

相关推荐
Thneonl5 小时前
SLO 燃烧率公式漏了个 1-,告警要全停才触发
安全·kubernetes
羑悻的小杀马特7 小时前
从写 YAML 到集群自愈:KES-Operator 把 KES 集群接进 Kubernetes 原生体系
运维·数据库·容器·kubernetes
智能运维指南7 小时前
持续集成平台怎么选?从Jenkins迁移、信创适配到质量门禁的全维评估
运维·devops·嘉为蓝鲸
Thneonl1 天前
照着 CKS 教材做实验,把 apiserver 干瘫了 7 分钟
安全·kubernetes
名字还没想好☜1 天前
Kubernetes 用 kubectl port-forward 和 proxy 调试集群内服务:两者区别、断连自动重连与为什么别用在生产
运维·docker·kubernetes
武汉唯众智创2 天前
云计算实训室建设指南(2026版):从技能大赛赛项标准反推架构、课程与落地路径
云原生·kubernetes·云计算·云计算实训室·云计算教学平台·职业技能大赛
名字还没想好☜2 天前
Docker 网络实战:bridge/host/none/自定义网络、容器互通与端口映射踩坑
运维·docker·kubernetes
秋风点枝2 天前
gogogo!go语言感悟大观--打卡day01
go·devops
探索云原生2 天前
KubeClipper 1.7.0 发布:Operation 优化与 Kubernetes 1.37 支持
linux·docker·云原生·kubernetes·go