揭示 GitHub Actions 中的 Disk I/O 瓶颈
在优化 CI 流水线时,能够进行的优化是有限的。CPU、网络、内存和 Disk I/O 的瓶颈都可能导致 CI 流水线速度变慢。让我们来看看 Disk I/O 如何成为 GitHub Actions 中的瓶颈。
在分析 CI 流水线性能时,Disk I/O 瓶颈很容易被忽视,但像 iostat
和 fio
这样的工具可以帮助我们了解哪些因素可能比我们想象的更严重地降低流水线的速度。
GitHub 提供了具有各种规格的不同 hosted-runners,但为了本次测试,我们使用私有仓库中的默认 ubuntu-22.04
runner,它确实为我们提供了额外的 2 个 vCPU,但不会改变磁盘性能。
如何监控磁盘性能
从像 fio
这样的工具中获得基准测试结果对于比较不同 runner 的相对磁盘性能非常有用。但是,要调查是否在 CI 流水线中遇到 Disk I/O 瓶颈,更有效的方法是在流水线执行期间监控磁盘性能。
我们可以使用 iostat
这样的工具来监控磁盘,同时从缓存安装依赖项,以查看我们是否使磁盘达到饱和状态。
- name: Start IOPS Monitoring
run: |
echo "Starting IOPS monitoring"
# Start iostat in the background, logging IOPS every second to iostat.log
nohup iostat -dx 1 > iostat.log 2>&1 &
echo $! > iostat_pid.txt # Save the iostat process ID to stop it later
- uses: actions/cache@v4
timeout-minutes: 5
id: cache-pnpm-store
with:
path: ${{ steps.get-store-path.outputs.STORE_PATH }}
key: pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-
pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
- name: Stop IOPS Monitoring
run: |
echo "Stopping IOPS monitoring"
kill $(cat iostat_pid.txt)
- name: Save IOPS Data
uses: actions/upload-artifact@v4
with:
name: iops-log
path: iostat.log
监控 Next.js 依赖项解压期间的磁盘活动
在上面的测试中,我们使用 iostat
来监控磁盘性能,同时缓存操作下载并解压 vercel/next.js
的依赖项:
Received 96468992 of 343934082 (28.0%), 91.1 MBs/sec
Received 281018368 of 343934082 (81.7%), 133.1 MBs/sec
Cache Size: ~328 MB (343934082 B)
/usr/bin/tar -xf /home/<path>/cache.tzst -P -C /home/<path>/gha-disk-benchmark --use-compress-program unzstd
Received 343934082 of 343934082 (100.0%), 108.8 MBs/sec
Cache restored successfully
完整的步骤花费了 12 秒完成,我们可以估计下载大约花费了 3 秒,剩下 9 秒用于解压操作。
压缩的 tarball 只有大约 328MB,但在提取后,写入磁盘的数据总量约为 1.6GB。较小的尺寸使我们的缓存通过网络传输速度很快,并且大多数 CPU 可以足够快地处理解压缩,这意味着更高的压缩通常是有利的。一旦下载和解压缩不再是瓶颈,剩下的就是写入磁盘。
从 tarball 读取是一个相当高效的过程,因为它主要是顺序读取,但是,我们需要将每个文件写入磁盘。这就是我们可能遇到 Disk I/O 瓶颈的地方,尤其是对于大量小文件。
01234567891011IOPS of Next.js dependency install from tarball0200400600800100012001400160018002000IOPStotalw/sr/s 01234567891011Throughput of a Next.js dependency install from tarball020406080100120140160180200220Throughput (MB/s)totalwMB/srMB/s
重要的是要注意,这只是单次运行,而不是平均值。随着时间的推移运行多次测试将使您更清楚地了解整体性能。运行之间的差异可能很高,因此单独的错误运行并不一定表明存在问题。
这次运行表明可能存在吞吐量瓶颈。我们看到最大总吞吐量出现峰值,其中大多数徘徊在 ~220MB/s 左右。这很可能是我们能够实现到该磁盘的最大吞吐量,我们将在下一步进行验证。我们应该继续监控并将其与其他 runner 进行比较,以查看是否可以为我们的工作流程找到理想的 runner。我们将使用 fio
来仔细检查我们是否达到了磁盘的最大吞吐量。
在我们继续之前,一个有趣的题外话是,我们可以从这个并排比较中看到相对较低的读取操作与写入操作。由于我们是从 tarball 读取,因此大多数读取是顺序的,这往往更有效。读取的数据可能进入缓冲区,然后在创建每个文件的副本时以更随机的模式写入磁盘。这就是我们看到写入 IOPS 高于读取 IOPS 的原因。
最大磁盘吞吐量
开发人员通常对他们的 CI 流水线进行的首批优化之一是缓存依赖项。即使每次运行仍会上载和下载缓存,它也可以通过将所有依赖项打包到一个压缩文件中来加快速度。这避免了解析依赖项的麻烦,避免了多次潜在的缓慢下载,并减少了网络延迟。
但正如我们上面所看到的,网络速度通常不是我们下载缓存时的瓶颈。
| 测试类型 | 块大小 | 带宽 | | ----------- | ----------- | ---------- | | 读取吞吐量 | 1024KiB | ~209MB/s | | 写入吞吐量 | 1024KiB | ~209MB/s |
使用 fio
测试我们的吞吐量,请注意“读取”和“写入”吞吐量都被限制在相同的值。这是一个相当明显的迹象,表明这里的限制实际上不是磁盘物理上的,而是 GitHub 施加的带宽限制。这是一种标准做法,可以在可能从其虚拟机访问同一物理磁盘的多个用户之间分配资源。它并不总是被记录在案,但大多数提供商对更高级别的 runner 具有更高的带宽限制。
我们在这里测量的结果与我们在 untar 测试中看到的 220MB/s 非常吻合,这再次提示我们,在依赖项安装期间,我们很可能是因磁盘而不是网络或 CPU 而变慢。
无论我们的下载速度有多快,我们都无法以快于磁盘最大吞吐量的速度写入磁盘。
| 未压缩缓存大小 | 2.0 GB | |----------------|-------| | 磁盘带宽 GitHub Runner (~210 MB/s) | | | 估计写入磁盘的时间: | ~9.52 秒 |
实际上,您的磁盘性能会因您的特定缓存大小、文件数量以及一般的构建间差异而有很大差异。这就是为什么最好监控您的 CI runner 以获得一致的基准,我们将讨论在多个 runner 上测试您的工作流程以进行比较。
最大 IOPS (每秒输入/输出操作数)
下载缓存 tarball 后,需要将其解压缩。根据压缩级别,它可能是一项占用大量 CPU 的操作,但这通常不是问题。在解压依赖项时,我们正在执行大量小的读取和写入操作,这正是我们可能遇到 Disk I/O 瓶颈的地方。
| 测试类型 | 块大小 | IOPS | | ------------- | ----------- | -------- | | 读取 IOPS | 4096B | ~51K | | 写入 IOPS | 4096B | ~57K | | 随机读取 IOPS | 4096B | ~9370 | | 随机写入 IOPS | 4096B | ~3290 |
IOPS 是一种衡量每秒可以执行多少读取/写入操作的指标。当我们有很多小文件时,尤其是在 node_modules
目录中,可能会使磁盘(或施加的限制)的 IOPS 限制饱和,并成为另一种 IO 瓶颈。
与我们无法以快于带宽限制的速度写入磁盘类似,我们可以对磁盘执行的 IOPS 数量也有限制。
在不同的 runner 上运行基准测试
如果您在 CI 流水线中看到任何类型的瓶颈,我们希望尝试通过缓存和并行化等策略来优化这些问题(如果可能)。但我们也需要知道我们是否达到了我们正在使用的 runner 的限制。在您的工作流程中添加 matrix 策略以在多个 runner 上进行测试,以便快速比较相同步骤在不同硬件上的速度非常容易。
jobs:
build:
runs-on: ${{ matrix.runner }}
strategy:
matrix:
runner: [ubuntu-22.04, depot-ubuntu-22.04]
要更详细地了解每个 runner 的特定磁盘性能,可以使用我们前面提到的 fio
基准测试工具。这将使您更好地了解每个 runner 的磁盘性能,并为检查 CI 流水线中的瓶颈提供参考点。
- name: Random Read Throughput Test
run: |
fio --ioengine=sync --bs=4k --rw=randread --name=random_read_throughput \
--direct=1 --filename=$HOME/fio_test/file --time_based --runtime=10s \
--size=250m --output=random_read_throughput_result-${{ matrix.runner }}.txt
- name: Clean up Test Directory
run: rm -rf $HOME/fio_test/*
- name: Random Write Throughput Test
run: |
fio --ioengine=sync --bs=4k --rw=randwrite --name=random_write_throughput \
--direct=1 --filename=$HOME/fio_test/file --time_based --runtime=10s \
--size=250m --output=random_write_throughput_result-${{ matrix.runner }}.txt
- name: Clean up Test Directory
run: rm -rf $HOME/fio_test/*
- name: Random Read IOPS Test
run: |
fio --name=random_read_iops --directory=$HOME/fio_test --size=5G \
--time_based --runtime=60s --ramp_time=2s --ioengine=libaio --direct=1 \
--verify=0 --bs=4K --iodepth=256 --rw=randread --group_reporting=1 \
--iodepth_batch_submit=256 --iodepth_batch_complete_max=256 \
--output=random_read_iops_result-${{ matrix.runner }}.txt
- name: Clean up Test Directory
run: rm -rf $HOME/fio_test/*
- name: Random Write IOPS Test
run: |
fio --name=random_write_iops --directory=$HOME/fio_test --size=5G \
--time_based --runtime=60s --ramp_time=2s --ioengine=libaio --direct=1 \
--verify=0 --bs=4K --iodepth=256 --rw=randwrite --group_reporting=1 \
--iodepth_batch_submit=256 --iodepth_batch_complete_max=256 \
--output=random_write_iops_result-${{ matrix.runner }}.txt
使用 Depot Ultra Runner 实现超快 Disk I/O
Depot 正在推出一种具有超快 Disk I/O 的新型 runner,即 Depot Ultra Runner。Ultra Runner 利用大型 RAM 磁盘缓存和更高性能的 CPU,最大限度地提高高 IOPS 和高吞吐量场景中的性能。
想要在 Depot Ultra Runner 可用时收到通知吗?订阅我们的 changelog 以获取所有重大更新。
尝试在 Depot runner 上比较您当前的工作流程。注册我们的 7 天免费试用,并通过 matrix job 在 Depot Runners 上比较您的 CI 流水线性能。