问题背景
公司研发团队使用 GitLab CI/CD 进行代码检查和镜像构建,所有 Runner 以 Docker executor 方式运行在一台 64 核 256GB 的物理机上。配置了 8 个 GitLab Runner,单个 Runner 最大并发 concurrent=8,理论最大并发任务数 64 个。日均 Pipeline 执行约 2000 次。
故障现象
上午 10:30 开始,GitLab CI/CD 流水线大面积失败,错误信息集中在:
|
|
Runner 宿主机负载飙升:
|
|
64 核 CPU 全部打满到 100%,load average 高达 85。Docker daemon 也因 CPU 争抢而响应缓慢,导致新容器无法及时创建——CI 任务全部超时。
排查过程
第一步:定位 CPU 消耗来源
|
|
多个构建容器各占用 600%+ CPU,远超预期。进入容器内部查看:
|
|
cc1plus 是 GCC 的 C++ 编译器内部进程。有人在构建大型 C++ 项目,且开了 18 个并行编译任务。
第二步:检查 Runner 并发设置
|
|
此时 Runner 上有 28 个正在执行的任务,其中 8 个是多线程 C++ 编译,每个使用了 -j$(nproc) 启动 64 个编译线程。但 Docker 默认不限制 CPU,容器内 nproc 看到的是宿主机的 64 核,实际每个构建容器都在尝试跑满 64 核。
第三步:分析 Docker CPU 隔离问题
|
|
所有构建容器均无任何 CPU 限制,每个容器都可以使用的全部 64 核。
解决方案
紧急处理:暂停非关键 Pipeline
通过 GitLab API 批量取消正在排队的非关键构建任务,将并发恢复到可控水平。
修复 Runner 配置
|
|
优化构建策略
- 容器内限制编译并行度:在 CI 脚本中显式设置
make -j4而非make -j$(nproc) - 调整 Docker daemon 配置:
|
|
- 引入构建缓存:使用 Docker layer cache + 分布式缓存(如 GCS/S3 as cache backend),减少重复编译。
根因分析
直接原因:多个 C++ 构建任务在无 CPU 限制的容器中启动 64 线程并行编译,64 核宿主机被完全打满。Docker daemon 的 API 调用因 CPU 争抢而超时,新构建任务无法启动,形成恶性循环。
深层原因:
- Runner 配置中
concurrent和limit设置过高,未与宿主机物理资源匹配 - Docker 容器未设置 CPU 资源限制,单容器可消耗全部核心
- CI 脚本中使用了
nproc自动探测核心数,在 Docker 容器中返回的是宿主机核心数 - 缺少 CI Runner 资源使用监控和限流机制
预防措施
- 强制 CPU 限制:所有 CI Runner 容器默认分配
--cpus=4,编译任务使用make -j4 - 并发上限策略:全局并发上限 ≤ 物理核心数 / 单容器核心数(64/4 = 16),而非盲目设置 64
- CI 资源监控:Prometheus + Grafana 监控 Runner 宿主机 CPU、任务排队深度、构建时长分布
- 构建优先级队列:区分关键服务构建和普通构建,高峰期优先调度发布相关 Pipeline
- 构建规范文档:要求各团队在
.gitlab-ci.yml中显式指定并行度,禁止使用nproc
总结
Docker 提供了资源隔离,但默认是"无限"模式——容器可以看到宿主机的全部 CPU,也能用满。这在 CI/CD 场景下尤其危险,因为构建任务天然容易触发 CPU 密集型编译。正确的做法是:宿主机总核心数 / 每容器限制核心数 = 最大安全并发数。同时,CI 脚本中永远不要用 nproc 来决定并行度,这是给自己埋坑。