Drone流水线配置:从30分钟到3分钟
发布日期: 2026/07/31 阅读总量: 0
Drone流水线配置:从30分钟到3分钟

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-backend1156.4%
install-frontend844.7%
lint221.2%
test18010.0%
build (webpack)32017.8%
docker-push1070(拉取基础镜像+构建+推送)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。我将 phpunitfrontend-lintfrontend-testbuild-frontend 全部声明为只依赖对应的安装步骤,它们彼此无依赖,于是同时运行。4 个容器在 runner 所在宿主机上并发消耗 CPU 和内存,但总时间由耗时最长的那个决定(本例为 build-frontend 约 50s,前面已经优化到 50s 是因为 webpack 缓存)。

3.3 Docker 层缓存

plugins/docker 镜像内置了 cache_fromcache_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 但构建慢,按这个顺序检查:

  1. 是否在步骤之间用了 volume 缓存依赖?
  2. Docker 构建是否用 cache_from/cache_to 或 buildx?
  3. 能否让测试、lint、构建并行跑?(检查 depends_on 依赖)
  4. 是否需要 matrix 多版本测试?
  5. 是否监控了资源使用?避免容器 OOM。

把我这套配置复制过去,改镜像版本、仓库地址,你的项目也能从 30 分钟降到 3 分钟。