1. 问题现象
团队并行提交多个 MR 后,GitLab Runner 队列开始变长。业务测试本身没有明显变慢,但 job 卡在环境准备阶段:
text
Preparing environment
Using Docker executor with image node:20 ...
Pulling docker image node:20 ...
context deadline exceeded
这类问题不要一上来就改测试脚本。先确认到底是测试执行慢,还是 runner 在拉镜像时已经被拖住。
本文记录一套排查顺序:从 runner executor、基础镜像、service 镜像、pull policy、镜像源和本地缓存逐层检查。
2. 环境示例
text
GitLab Runner: Docker executor
Runner 节点:Linux / Docker Engine
并发场景:多个分支同时跑单测和集成测试
常见镜像:node:20、python:3.12、postgres:16-alpine、redis:7、ghcr.io/.../test-runner
如果是 GitHub Actions 自建 runner,也可以参考同样的排查思路。核心都是:job 开始前要先把容器镜像拉到 runner 节点。
3. 先区分 pending、preparing 和 script
看 job 卡在哪一段:
| 阶段 | 现象 | 方向 |
|---|---|---|
| pending | 没有 runner 接任务 | runner 数量、tag、并发数 |
| preparing | 已分配 runner,正在拉镜像 | Docker、镜像源、DNS、缓存 |
| script | 脚本已经执行 | 测试耗时、依赖缓存、数据库初始化 |
如果日志里出现 Pulling docker image,说明至少有一部分时间花在镜像拉取。
4. 在 runner 节点手工验证
进入 runner 节点,不通过 CI,直接拉关键镜像:
bash
docker pull node:20
docker pull python:3.12
docker pull postgres:16-alpine
docker pull redis:7-alpine
如果团队使用 GHCR、MCR、Quay:
bash
docker pull ghcr.io/your-org/test-runner:latest
docker pull mcr.microsoft.com/playwright:v1.44.0-jammy
docker pull quay.io/skopeo/stable:latest
常见错误包括:
text
context deadline exceeded
i/o timeout
connection reset by peer
TLS handshake timeout
这时问题优先看 runner 节点的网络、DNS、镜像源和代理配置。
5. 配置 Docker 镜像源
可以先用 1ms-helper 处理 Docker 场景:
bash
curl -sSL https://static.1ms.run/1ms-helper/install.sh | bash
sudo 1ms-helper config:docker
Linux Docker 的基础配置可以参考:
json
{
"registry-mirrors": ["https://docker.1ms.run"],
"dns": ["8.8.8.8", "114.114.114.114"]
}
然后重启 Docker:
bash
systemctl daemon-reload
systemctl restart docker
systemctl status docker
再验证:
bash
docker pull docker.1ms.run/library/node:20
docker pull docker.1ms.run/library/postgres:16-alpine
docker pull ghcr.1ms.run/your-org/test-runner:latest
说明:毫秒镜像在这里解决的是多源镜像入口和拉取稳定性这一层,不替代 runner 并发、测试拆分和依赖缓存。
6. 检查 GitLab Runner pull policy
GitLab Runner Docker executor 支持 pull_policy。如果每次 job 都强制拉镜像,runner 本地缓存就很难发挥作用。
示例:
toml
[[runners]]
executor = "docker"
[runners.docker]
image = "docker.1ms.run/library/node:20"
pull_policy = ["if-not-present", "always"]
实际生产里不要简单照抄。建议按镜像类型区分:
| 镜像类型 | 建议 |
|---|---|
| 高频基础镜像 | 固定 tag,配合本地缓存 |
| 安全扫描镜像 | 固定版本,关键场景考虑 digest |
| 数据库 service 镜像 | 固定小版本,减少重复拉取 |
| 团队自建镜像 | 使用内部版本号,不长期依赖 latest |
7. 检查 .gitlab-ci.yml
一个常见配置:
yaml
test:
image: docker.1ms.run/library/node:20
services:
- name: docker.1ms.run/library/postgres:16-alpine
alias: postgres
script:
- npm ci
- npm test
如果每个 job 都启动独立数据库 service,且并发很多,镜像拉取、容器启动、数据库初始化都会叠加。需要结合缓存、测试拆分和服务复用一起看。
8. 固定关键镜像版本
近期 Docker 官方复盘过 Trivy 和 KICS 相关的供应链攻击事件。CI 里常见的扫描器、构建器、测试工具镜像,不建议长期用模糊的 latest。
至少做到:
- 生产发布链路用明确 tag。
- 关键扫描和构建镜像记录 digest。
- 基础镜像升级走 MR,而不是静默变化。
- runner 节点定期清理,但不要把高频镜像缓存清得太干净。
Docker 支持按 digest 拉取镜像,例如:
bash
docker pull alpine@sha256:<digest>
Digest 不是为了让镜像拉得更快,而是为了让 CI 结果更可复现。
9. 排查清单
| 检查项 | 命令/位置 |
|---|---|
| runner 是否接任务 | GitLab job 状态、runner tag |
| 是否卡在镜像 | job 日志里的 Pulling docker image |
| Docker 是否正常 | systemctl status docker |
| DNS 是否异常 | 1ms-helper check:dns |
| 基础镜像能否拉取 | docker pull node:20 |
| 多源镜像能否拉取 | docker pull ghcr.io/... |
| pull policy 是否合适 | config.toml |
| service 镜像是否过多 | .gitlab-ci.yml |
总结
CI 并行构建变慢时,不要只看测试脚本。runner 的镜像拉取阶段经常是隐藏瓶颈。
我的处理顺序是:
- 看 job 卡在 pending、preparing 还是 script。
- 在 runner 节点手工拉基础镜像和 service 镜像。
- 配置 Docker 镜像源并验证多源镜像入口。
- 检查
pull_policy和本地缓存。 - 固定关键工具镜像版本,必要时记录 digest。
这样才能把"测试慢"和"runner 拉镜像慢"分开处理。