Drone流水线配置:从30分钟到3分钟
我们团队半年前把 CI/CD 从 Jenkins 迁移到 Drone(v2.12.0),图它轻量和原生容器化。迁移后流水线能跑了,但构建时间一直卡在 25~32 分钟,跟以前 Jenkins 差不多。老板问:“说好的10秒构建呢?”
排查发现:问题出在流水线配置上。所有人都在用官网的 .drone.yml 模板,步骤全部串行,没加缓存,依赖每次重装,Docker 镜像也是热乎的裸机层。这哪是 Drone,这是 Drone 当 Jenkis 用。
本文会拆解一个真实 PHP + Node 项目(部分前端编译)的流水线,从最初的“能跑”版本,一步步优化到“能跑快”,并提供可直接复用的配置。
1. 问题:30 分钟到底花在哪
原始流水线大概长这样(简化):
kind: pipeline
type: docker
name: default
steps:
- name: install-backend
image: composer:2.7.0
commands:
- composer install --no-interaction --prefer-dist
- name: install-frontend
image: node:20.11.0-alpine
commands:
- npm ci
- name: lint
image: node:20.11.0-alpine
commands:
- npm run lint
- name: test
image: node:20.11.0-alpine
commands:
- npm test
- name: build
image: node:20.11.0-alpine
commands:
- npm run build
- name: docker-push
image: plugins/docker:20.13.0
settings:
repo: repo/app
tags: latest
每一步都用一个独立容器,没有缓存卷,没有 layer 缓存,没有并行。实际耗时分解(某次构建日志):
| 步骤 | 耗时(秒) | 占比 |
|---|---|---|
| install-backend | 115 | 6.4% |
| install-frontend | 84 | 4.7% |
| lint | 22 | 1.2% |
| test | 180 | 10.0% |
| build (webpack) | 320 | 17.8% |
| docker-push | 1070(拉取基础镜像+构建+推送) | 59.5% |
| 总计 | 1791(≈30min) | 100% |
瓶颈很明显:Docker 镜像构建和推送占 60%;另外前后端依赖安装各 2 分钟,因为没缓存。而且所有步骤都等前一个结束了才跑。
2. 两种优化方案对比
方案 A:串行 + 局部缓存(常规优化)
- vendor/ 和 node_modules/ 用 Drone 卷(volume)持久化,避免每次重装
- Docker 层缓存(registry mirrors 或 volume 挂载 /var/lib/docker)
- 仍保持串行执行
效果:依赖安装从 199 秒降到 6 秒(纯解压),Docker 构建从 1070 秒降到 280 秒(缓存命中),总时间降到约 12 分钟。
方案 B:并行步骤 + 矩阵 + 全链路缓存(本文最终方案)
- 将测试和 lint 并行(依赖安装后立即触发)
- 后端测试和前端测试用矩阵(matrix)分别跑多个 PHP/Node 版本
- Docker 构建用“多阶段构建 + 层缓存”
- 添加 S3 或 MinIO 缓存插件加速大体积工件
效果:总时间降到 3 分 12 秒(实测 5 次均值),较原始提速 89.4%。
下面直接给最终配置,再逐段解释。
3. 完整优化后的 .drone.yml
版本环境:Drone Server 2.12.0, Runner Docker 2.12.0, Docker Engine 24.0.6, PHP 8.3.3, Node 20.11.0。
kind: pipeline
type: docker
name: backend-test
platform:
os: linux
arch: amd64
workspace:
base: /drone/src
path: app
volumes:
- name: composer-cache
host:
path: /tmp/drone/composer-cache
- name: npm-cache
host:
path: /tmp/drone/npm-cache
- name: docker-cache
host:
path: /tmp/drone/docker
services:
- name: mysql
image: mysql:8.0.35
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: test
- name: redis
image: redis:7.2.4-alpine
steps:
# 1. 安装后端依赖 (使用缓存卷)
- name: composer
image: composer:2.7.0
volumes:
- name: composer-cache
path: /tmp/cache
commands:
- cp .env.example .env
- composer install --no-interaction --prefer-dist --no-dev
- ls -la vendor/
# 2. 安装前端依赖 (并行执行)
- name: npm-install
image: node:20.11.0-alpine
depends_on: [ clone ]
volumes:
- name: npm-cache
path: /root/.npm
commands:
- npm ci --cache /root/.npm/cache --prefer-offline
# 3. 并行步骤:后端测试、前端 lint & test、构建
- name: phpunit
image: php:8.3.3-cli
depends_on: [ composer ]
environment:
DB_HOST: mysql
DB_DATABASE: test
commands:
- docker-php-ext-install pdo pdo_mysql
- ./vendor/bin/phpunit --log-junit reports/phpunit.xml
- name: frontend-lint
image: node:20.11.0-alpine
depends_on: [ npm-install ]
commands:
- npm run lint
- name: frontend-test
image: node:20.11.0-alpine
depends_on: [ npm-install ]
commands:
- npm test -- --reporters=default --reporters=junit
- name: build-frontend
image: node:20.11.0-alpine
depends_on: [ npm-install ]
commands:
- npm run build -- --mode production
# 4. Docker 构建与推送(多阶段 + 层缓存)
- name: docker
image: plugins/docker:20.13.0
depends_on: [ phpunit, frontend-lint, frontend-test, build-frontend ]
settings:
repo: registry.company.com/app
tags:
- latest
- ${DRONE_COMMIT_SHA:0:8}
dockerfile: Dockerfile
cache_from: registry.company.com/app:cache
cache_to: registry.company.com/app:cache
volumes:
- name: docker-cache
path: /var/lib/docker
when:
branch:
- main
- release/*
# 5. 矩阵构建:可选,此处用于跑多个 PHP 版本测试(略,见下文说明)
---
kind: pipeline
type: docker
name: matrix-test
matrix:
PHP_VERSION:
- "8.2"
- "8.3"
- "8.4"
steps:
- name: composer_${PHP_VERSION}
image: composer:${PHP_VERSION}
commands:
- composer install
- name: phpunit_${PHP_VERSION}
image: php:${PHP_VERSION}-cli
environment:
DB_HOST: mysql
commands:
- ./vendor/bin/phpunit
services:
- name: mysql
image: mysql:8.0.35
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
关键优化点解释:
3.1 卷挂载做缓存
Drone 支持 host 卷,把宿主机的目录挂载进容器。composer-cache 保存 Composer 的缓存文件(~/.composer/cache),npm-cache 保存 npm 缓存(/root/.npm)。首次安装后,第二次开始直接读缓存,耗时从 115s→3s。
3.2 并行执行
Drone 根据 depends_on 自动构建 DAG。我将 phpunit、frontend-lint、frontend-test、build-frontend 全部声明为只依赖对应的安装步骤,它们彼此无依赖,于是同时运行。4 个容器在 runner 所在宿主机上并发消耗 CPU 和内存,但总时间由耗时最长的那个决定(本例为 build-frontend 约 50s,前面已经优化到 50s 是因为 webpack 缓存)。
3.3 Docker 层缓存
plugins/docker 镜像内置了 cache_from 和 cache_to 参数。每次构建完成后打包缓存层 push 到一个专门 tag(例如 app:cache),下次构建时先拉取这个镜像,可以复用之前的层。配合挂载 /var/lib/docker 卷还能加速镜像拉取(但插件官方建议用 registry 缓存更稳定)。效果:Docker 构建从 1070s 降到 90s(首次 280s)。
3.4 矩阵测试(并行版本覆盖)
上面配置中增加了一个单独的 pipeline matrix-test,利用 Drone 的 matrix 机制,对 PHP 8.2/8.3/8.4 各跑一次 phpunit。三个子流水线会同时在多个 runner 或同一 runner 的多个并发槽位上执行。我们团队在 4 核心的 runner 上测试,总耗时 = 最慢子任务耗时(约 40s)+ 启动开销,比串行跑三个版本省 2/3 的时间。
4. 效果数据
在同样环境下(Docker Runner 单机 8vCPU/16GB RAM, 1Gbps 内网),对同一代码仓库(提交 f3b8a2e)各跑 5 次取中位数:
| 配置 | 平均耗时 | Docker 构建耗时 | 依赖安装耗时 | 资源峰值 |
|---|---|---|---|---|
| 原始(串行无缓存) | 1820 秒 | 1070 秒 | 199 秒 | CPU 45%, 内存 2GB |
| 方案A(串行 + 缓存) | 740 秒 | 280 秒 | 6 秒 | CPU 48%, 内存 2.5GB |
| 方案B(并行 + 矩阵 + 全缓存) | 192 秒 | 90 秒 | 3 秒 | CPU 85%, 内存 6.5GB |
速度提升 9.5 倍,代价是 CPU 和内存使用率翻倍——但 runner 空闲时也不浪费,我们接受了。
5. 避坑指南(你一定会遇到的问题)
坑1:卷权限导致 Composer 报错 'mkdir(): Permission denied'
原因:host 卷的目录默认 root 所有,但容器内运行用户(通常 uid 1000)无法写入。解决办法:在宿主机执行 chmod 777 /tmp/drone/composer-cache;或者使用 drone 官方推荐的 volumes: 中的 mode: 0777(但 host 卷不支持指定权限,需用 temp: 卷)。实际用 temp: 卷可以自动设置权限。
- name: composer-cache
temp:
medium: memory # 或 'disk'
坑2:并行步骤中 service 端口冲突
当多个并行 pipeline 同时启动(比如 matrix 中的 3 个 PHP 版本),每个都需要一个 MySQL 服务,如果宿主机端口 3306 被第一个容器的 MySQL 占用,后面容器会启动失败。解决办法:让 service 不暴露宿主机端口,只用容器网络(别名方式)。在 service 定义里不写 ports:,只写 name: mysql,然后在 step 中通过 service 名加主机名 mysql 直连。Drone 会自动使用 bridge 网络,每个 pipeline 实例有独立的网络。
坑3:Docker 层缓存爆炸
cache_to 每次 push 一个 app:cache 镜像,如果不清理,registry 上会积累大量 layer 碎片。我们设置了一个 GitLab CI 定时任务每周清理 app:cache 历史标签。也可以用 cache_from: true 配合 --cache-to type=inline 但 plugins/docker 不支持。建议改用 drone-docker-buildx 或手动写 buildx 命令。
坑4:并行步骤资源竞争导致测试不稳定
前端测试和后端测试同时运行,都尝试连接同一个 service(MySQL/Redis),如果读/写冲突或连接池耗尽可能偶发失败。解决:给每个测试步骤一个专用的数据库 schema(用环境变量传递),或限制 service 的最大连接数。我们后端测试用 phpunit --processes 1 避免并行内部的并发问题,外部则通过 service 资源限制保障。
坑5:npm ci 与缓存目录覆盖问题
npm ci 需要 node_modules 目录不存在,否则报错。但使用了卷缓存后 node_modules 会被保留。解决方案:在 npm-install 步骤开始前 rm -rf node_modules 或者使用 npm ci --prefer-offline 并指定 --cache 路径不同。我们的做法:commands: [ "rm -rf node_modules", "npm ci --cache /root/.npm/cache" ]。
6. 原理:为什么这些优化有效?
Drone 的流水线基于容器步骤,每一步都在独立容器中运行。默认情况下,步骤之间的文件系统是不共享的——除非显式挂载卷。这就是原始配置慢的核心原因:每次新容器都是干净的。
- 卷缓存:利用了 Linux 的 bind mount,让容器可以直接读写宿主机的目录。Composer 和 npm 的缓存文件不需要重新下载,而是本地读,速度从网络延迟降到磁盘延迟。
- 并行步骤:Drone 的 scheduler 根据 DAG 同时启动无依赖的步骤。单机下可并发运行数个容器,充分利用 CPU 多核。若 runner 配置
DRONE_RUNNER_CAPACITY=5,可以同时运行 5 个步骤。 - Docker 层缓存:Docker 镜像是分层存储的。当 Dockerfile 指令没变化时,缓存层可以直接复用,跳过耗时的 apt-get install、npm build 等。配合
cache_from可以跨构建复用。 - 矩阵:Drone 的 matrix 是 pipeline 级别的并行,相当于同时触发多个几乎相同的流水线,适合多版本测试。它依赖 runner 数量或 capacity,可以在不同机器上并行。
7. 总结(非结论,是你的 checklist)
如果你也在用 Drone 但构建慢,按这个顺序检查:
- 是否在步骤之间用了 volume 缓存依赖?
- Docker 构建是否用 cache_from/cache_to 或 buildx?
- 能否让测试、lint、构建并行跑?(检查 depends_on 依赖)
- 是否需要 matrix 多版本测试?
- 是否监控了资源使用?避免容器 OOM。
把我这套配置复制过去,改镜像版本、仓库地址,你的项目也能从 30 分钟降到 3 分钟。