最近遇到一个很有迷惑性的 CI 问题:小团队自建了两台 runner,平时单个分支跑测试没问题,但一到几个人同时提 MR,队列就越来越长。
一开始大家以为是测试变慢了。看完日志才发现,很多 job 的脚本还没开始执行,时间先花在这里:
text
Preparing environment
Pulling docker image node:20 ...
Pulling docker image postgres:16-alpine ...
context deadline exceeded
这篇不讲大而全的 CI 优化,只记录我会怎么排"runner 排队"和"镜像拉取慢"这两件事。
先拆流水线阶段
我先把 job 耗时拆成三段:
| 阶段 | 表现 | 常见原因 |
|---|---|---|
| pending | 等 runner 接任务 | runner 少、tag 不匹配、并发限制 |
| preparing | runner 接了,但在准备环境 | 拉基础镜像、service 镜像、DNS、网络 |
| script | 脚本开始执行 | 测试慢、缓存没命中、数据库初始化慢 |
这次真正慢的是 preparing,不是 script。
一个典型 .gitlab-ci.yml
示例配置大概这样:
yaml
test:
image: node:20
services:
- name: postgres:16-alpine
alias: postgres
- name: redis:7-alpine
alias: redis
script:
- npm ci
- npm test
并发一上来,runner 要同时拉 node、postgres、redis。如果还有 e2e、扫描、构建 job,镜像来源会更多:
- Docker Hub:基础镜像、数据库镜像。
- GHCR:团队工具镜像。
- MCR:Playwright / 浏览器测试镜像。
- Quay:安全扫描或云原生工具镜像。
单个 job 慢 30 秒不明显,6 个 job 一起慢,队列就会明显变长。
先在 runner 节点复现
不要只盯着 CI 页面。我会直接到 runner 节点跑:
bash
docker pull node:20
docker pull postgres:16-alpine
docker pull redis:7-alpine
docker pull mcr.microsoft.com/playwright:v1.44.0-jammy
如果这里就卡住,说明不是 GitLab 本身的问题,而是 runner 节点拉镜像这层不稳定。
配镜像源和多源入口
runner 是 Docker executor 时,可以先处理 Docker 镜像源:
bash
curl -sSL https://static.1ms.run/1ms-helper/install.sh | bash
sudo 1ms-helper config:docker
然后验证常用镜像:
bash
docker pull docker.1ms.run/library/node:20
docker pull docker.1ms.run/library/postgres:16-alpine
docker pull docker.1ms.run/library/redis:7-alpine
如果有 GHCR:
bash
docker pull ghcr.1ms.run/your-org/e2e-runner:2026.05
我会把这组验证命令放进 runner 节点初始化文档。这样新加 runner 时,不用等第一条流水线失败才发现镜像入口没跑通。
pull policy 和缓存别乱配
GitLab Runner Docker executor 支持 pull_policy。但这不是一个"越不拉越好"的选项。
我一般这样分:
| 镜像 | 策略 |
|---|---|
| 高频基础镜像 | 固定 tag,允许本地缓存复用 |
| 团队自建测试镜像 | 使用版本号,不长期用 latest |
| 安全扫描镜像 | 固定版本,关键链路记录 digest |
| 临时实验镜像 | 可以每次拉,但不要进主干发布链路 |
示例:
toml
[[runners]]
executor = "docker"
[runners.docker]
image = "docker.1ms.run/library/node:20"
pull_policy = ["if-not-present", "always"]
这个配置不是万能答案,重点是让团队知道:哪些镜像需要新鲜,哪些镜像应该稳定。
安全扫描镜像别随手 latest
最近几个月,安全扫描、IaC 扫描这类工具镜像也出现过供应链事件。CI 里这些镜像权限往往不低,还能接触代码和构建产物,所以不要把它们当普通工具随便拉。
我现在会对扫描镜像做三件事:
- 固定版本 tag。
- 升级走 MR。
- 发布链路记录 digest。
这和镜像源加速不是同一件事:前者解决可控性,后者解决可达性。runner 想跑得稳,两层都要看。
最后复盘
这次真正的瓶颈不是测试脚本,而是 runner 公共入口太脆弱。每个人都在提 MR,但每条流水线都要重复拉镜像,镜像入口不稳定时,就会把团队并行效率拖下来。
我会把 runner 初始化分成四步:
- 配 Docker 镜像源。
- 预拉高频基础镜像。
- 固定工具镜像版本。
- 用一组
docker pull验证多源入口。
这个动作不花哨,但对小团队很实用。测试优化之前,先让 runner 稳定把环境拉起来。