GitHub Actions 中 PHP 流水线:并行测试与缓存优化
大家好,今天我们来聊聊如何在 GitHub Actions 中构建高效的 PHP 持续集成(CI)流水线,重点关注并行执行测试和缓存优化这两个关键方面。
为什么并行测试和缓存优化很重要?
在任何软件开发项目中,快速的反馈周期至关重要。持续集成旨在尽早发现问题,而缓慢的 CI 流水线会阻碍这一目标。并行测试通过同时运行多个测试套件来缩短测试时间,而缓存优化则通过重用先前构建的结果来减少重复工作。两者结合,可以显著提升 CI 的效率,让开发者更快地获得反馈,从而更快地迭代代码。
1. 构建基础的 PHP CI 流水线
首先,我们从一个简单的 PHP 项目开始,假设它具有以下目录结构:
my-php-project/
├── src/
│ └── MyClass.php
├── tests/
│ └── MyClassTest.php
├── composer.json
├── composer.lock
└── phpunit.xml.dist
composer.json 定义了项目的依赖,phpunit.xml.dist 是 PHPUnit 的配置文件。我们先创建一个基本的 .github/workflows/ci.yml 文件,定义我们的 CI 流水线:
name: PHP CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mbstring, xml, ctype, json, gd
- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Install Dependencies
run: composer install --no-interaction --no-progress --prefer-dist
- name: Run Tests
run: vendor/bin/phpunit
这个流水线做了以下几件事:
- 触发条件: 在
main分支的 push 和 pull request 上触发。 - 运行环境: 使用
ubuntu-latest作为运行环境。 - 检出代码: 使用
actions/checkout@v3检出代码。 - 配置 PHP: 使用
shivammathur/setup-php@v2设置 PHP 版本和扩展。 - 缓存依赖: 使用
actions/cache@v3缓存 Composer 依赖,加速安装过程。 - 安装依赖: 使用
composer install安装依赖。 - 运行测试: 使用
phpunit运行测试。
这个流水线虽然可以工作,但它存在两个主要问题:测试是串行执行的,并且缓存可能不够高效。
2. 并行执行测试
PHPUnit 支持并行执行测试,我们可以利用这一点来显著缩短测试时间。 我们可以使用 paratest 这个工具来实现。
首先,我们需要安装 paratest:
composer require --dev brianium/paratest
然后,修改 .github/workflows/ci.yml 文件,添加并行测试步骤:
name: PHP CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mbstring, xml, ctype, json, gd
- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Install Dependencies
run: composer install --no-interaction --no-progress --prefer-dist
- name: Run Tests (Parallel)
run: vendor/bin/paratest -p 8 # -p 指定并行进程数
我们使用 vendor/bin/paratest 命令替代了 vendor/bin/phpunit 命令,并添加了 -p 8 参数,指定使用 8 个进程并行执行测试。 进程数量需要根据你的服务器配置以及测试套件的复杂度来调整。
并行测试的原理: Paratest 将测试用例分割成多个块,然后将这些块分配给多个进程同时执行。这可以显著减少测试总时间,特别是对于大型测试套件。
需要注意的事项:
- 数据库连接: 并行测试可能会导致数据库连接冲突。你需要确保你的测试用例能够正确处理并发数据库访问。通常,需要为每个进程配置独立的数据库连接。
- 文件系统访问: 并行测试也可能导致文件系统访问冲突。确保你的测试用例不会同时写入相同的文件。
- 全局状态: 避免在测试用例中使用全局状态,因为这可能会导致并行测试产生不可预测的结果。
- 资源限制: 并行测试会消耗更多的 CPU 和内存资源。你需要根据你的服务器配置调整并行进程数,以避免资源耗尽。
- 测试隔离: 确保你的测试用例是相互隔离的,不会相互影响。
3. 缓存优化
除了缓存 Composer 依赖,我们还可以缓存其他内容来进一步优化 CI 流水线。例如,我们可以缓存 PHPUnit 的结果缓存,以避免重复运行相同的测试用例。
首先,在 phpunit.xml.dist 文件中配置结果缓存:
<phpunit>
<!-- ... 其他配置 ... -->
<extensions>
<extension class="PHPUnitRunnerExtensionResultCacheExtension"/>
</extensions>
</phpunit>
然后,修改 .github/workflows/ci.yml 文件,添加结果缓存步骤:
name: PHP CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mbstring, xml, ctype, json, gd
- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Install Dependencies
run: composer install --no-interaction --no-progress --prefer-dist
- name: Cache PHPUnit Result
uses: actions/cache@v3
with:
path: .phpunit.result.cache
key: ${{ runner.os }}-phpunit-result-${{ hashFiles('phpunit.xml.dist', 'tests/**/*Test.php') }}
restore-keys: |
${{ runner.os }}-phpunit-result-
- name: Run Tests (Parallel)
run: vendor/bin/paratest -p 8
我们添加了一个新的缓存步骤,用于缓存 .phpunit.result.cache 文件。这个文件包含了 PHPUnit 的结果缓存,可以避免重复运行相同的测试用例。
更进一步的缓存优化策略:
- 自定义缓存键: 你可以使用更精确的缓存键来提高缓存命中率。例如,你可以将 PHP 版本、操作系统版本、甚至 PHP 扩展列表包含在缓存键中。
- 分层缓存: 你可以使用分层缓存策略来进一步优化缓存。例如,你可以将 Composer 依赖分成两层:一层包含核心依赖,另一层包含可选依赖。这样,当只需要更新可选依赖时,可以避免重新安装核心依赖。
- 缓存构建产物: 如果你的项目需要构建产物(例如,编译后的代码),你可以将构建产物缓存起来,以便在后续的构建中使用。
- 利用 GitHub Actions 缓存作用域: GitHub Actions 提供了不同的缓存作用域,包括 workflow、job 和 runner。你可以根据你的需求选择合适的缓存作用域。Workflow 级别的缓存可以在多个 job 之间共享,而 runner 级别的缓存只能在同一个 runner 上共享。
4. 使用矩阵策略 (Matrix Strategy) 实现多版本测试
如果你的项目需要支持多个 PHP 版本,你可以使用 GitHub Actions 的矩阵策略来并行执行多个版本的测试。
修改 .github/workflows/ci.yml 文件,添加矩阵配置:
name: PHP CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['8.1', '8.2', '8.3']
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: mbstring, xml, ctype, json, gd
- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}-${{ matrix.php-version }}
restore-keys: |
${{ runner.os }}-composer-${{ matrix.php-version }}-
- name: Install Dependencies
run: composer install --no-interaction --no-progress --prefer-dist
- name: Cache PHPUnit Result
uses: actions/cache@v3
with:
path: .phpunit.result.cache
key: ${{ runner.os }}-phpunit-result-${{ hashFiles('phpunit.xml.dist', 'tests/**/*Test.php') }}-${{ matrix.php-version }}
restore-keys: |
${{ runner.os }}-phpunit-result-${{ matrix.php-version }}-
- name: Run Tests (Parallel)
run: vendor/bin/paratest -p 8
我们添加了 strategy.matrix 配置,指定了要测试的 PHP 版本列表。GitHub Actions 会为每个 PHP 版本创建一个独立的 job,并行执行测试。 注意,我们也在 cache 的 key 中加入了 php-version,保证了每个 PHP 版本使用独立的缓存。
矩阵策略的优点:
- 并行测试: 可以并行测试多个 PHP 版本,显著缩短测试时间。
- 代码兼容性: 可以确保代码在多个 PHP 版本上兼容。
- 简化配置: 可以使用简单的配置来定义多个测试环境。
5. 性能监控和优化
仅仅配置并行测试和缓存是不够的,还需要定期监控 CI 流水线的性能,并根据监控结果进行优化。
监控指标:
- 构建时间: 监控每次构建的总时间,以及每个步骤的耗时。
- 缓存命中率: 监控缓存的命中率,以便评估缓存策略的有效性。
- 资源使用率: 监控 CPU、内存和磁盘的使用率,以便识别资源瓶颈。
- 错误率: 监控测试的错误率,以便及时发现问题。
优化策略:
- 优化测试用例: 优化测试用例的性能,减少测试时间。
- 调整并行进程数: 根据资源使用率调整并行进程数,以达到最佳性能。
- 优化缓存策略: 根据缓存命中率优化缓存策略,提高缓存命中率。
- 升级硬件: 如果资源瓶颈严重,可以考虑升级硬件。
- 使用更快的运行环境: 不同的运行环境的性能可能不同,可以尝试使用更快的运行环境。
表格总结: 常见优化点及其影响
| 优化点 | 影响 |
|---|---|
| 并行测试 | 显著缩短测试时间,特别是对于大型测试套件。 |
| 缓存依赖 | 加速依赖安装过程,减少构建时间。 |
| 缓存测试结果 | 避免重复运行相同的测试用例,减少构建时间。 |
| 矩阵策略 | 并行测试多个 PHP 版本,确保代码兼容性,减少构建时间。 |
| 优化测试用例 | 减少测试时间,提高 CI 效率。 |
| 调整并行进程数 | 平衡资源使用率和测试速度,达到最佳性能。 |
| 优化缓存策略 | 提高缓存命中率,减少构建时间。 |
| 升级硬件 | 解决资源瓶颈,提高 CI 效率。 |
| 使用更快的运行环境 | 提高 CI 效率。 |
代码示例: 动态调整 Paratest 进程数
可以根据 runner 的核心数量动态调整 Paratest 的进程数。
name: PHP CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mbstring, xml, ctype, json, gd
- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Install Dependencies
run: composer install --no-interaction --no-progress --prefer-dist
- name: Determine Paratest Processes
id: paratest-processes
run: |
CORES=$(nproc)
echo "Number of cores: $CORES"
PROCESSES=$((CORES / 2)) # 假设使用一半的 CPU 核心
echo "processes=$PROCESSES" >> $GITHUB_OUTPUT
- name: Run Tests (Parallel)
run: vendor/bin/paratest -p ${{ steps.paratest-processes.outputs.processes }}
这个例子中,我们使用 nproc 命令获取 runner 的核心数量,然后将其除以 2,作为 Paratest 的进程数。这样可以确保 Paratest 不会过度占用 CPU 资源。
6. 调试 CI 流水线
CI 流水线出现问题是不可避免的,我们需要掌握一些调试技巧。
- 查看日志: 仔细查看 CI 流水线的日志,特别是错误日志。
- 本地调试: 尝试在本地复现 CI 流水线中的问题。
- 使用 GitHub Actions 的调试功能: GitHub Actions 提供了调试功能,可以让你在 CI 流水线中设置断点,并逐步执行代码。
- 简化问题: 尝试简化问题,例如,只运行一个测试用例,或者禁用缓存,以便更容易找到问题的根源。
- 寻求帮助: 如果无法解决问题,可以寻求社区的帮助。
高效 CI 的一些思考
构建高效的 PHP CI 流水线是一个持续迭代的过程。通过并行测试、缓存优化、矩阵策略和性能监控,我们可以显著提高 CI 效率,让开发者更快地获得反馈,从而更快地迭代代码。记住,没有银弹,需要根据你的项目特点和资源限制,选择合适的策略。
持续集成持续改进
构建高效的 CI 流水线是一个持续改进的过程。监控、分析和调整是关键。