Runtime-Based Splitting で CI 実行時間を33%短縮した話
GitHub Actions における並列テスト実行の最適化手法
33%
時間短縮
21%
コスト削減
83%
DB Setup 高速化
課題
CI パイプラインの完了に10分以上かかっていました。RSpec テストは7つの並列ノードで実行されていましたが、ノード間で大きな偏りがあり、6分で終わるノードもあれば10分以上かかるノードもありました。
原因は以下の3つでした:
- 1. Filesize-based splitting - テストがファイルサイズで分割されており、実行時間は考慮されていなかった
- 2. 冗長な DB setup - 各ノードが7つのDBを作成していたが、実際に使用するのは1つだけだった
- 3. 過剰なノード数 - ワークロードに対してノード数が多すぎた
Before vs After 比較
最遅: 10m 12s · 偏差: 4分以上
最遅: 7m 24s · 偏差: 1分
解決策
1. Runtime-Based Splitting の導入
ファイルサイズではなく、実行時間に基づいてテストを分散させるよう変更しました。parallel_tests(複数CPUコアでテストを並列実行するgem)のランタイムログ機能を活用しています。
仕組み:
- 1. 各ノードがテスト実行時間をランタイムログに記録
- 2. ログをアーティファクトとしてアップロード
- 3. merge ジョブで全ノードのログを結合しキャッシュに保存
- 4. 次回実行時にキャッシュから復元し最適な分散を実現
- name: Run tests
run: |
RUNTIME_LOG="tmp/parallel_runtime_rspec.log"
if [ -f "$RUNTIME_LOG" ]; then
GROUP_BY="runtime"
else
GROUP_BY="filesize"
fi
bundle exec parallel_test spec -t rspec \
--group-by "$GROUP_BY" \
--runtime-log "$RUNTIME_LOG" 2. DB Setup の最適化
各 CI ノードがセットアップ時に7つのDBを作成していることが判明しました。しかし、各ノードは独立した MySQL サービスを持っており、実際に使用するのは1つだけです。このプロジェクトでは Rails のマイグレーションではなく ridgepole(DSLベースのスキーマ管理ツール)を使用しています。
Before (110s)
parallel:create[7] + ridgepole:parallel_prepare After (19s)
db:create + ridgepole:apply 3. 並列ノード数の削減
テスト分散の改善と DB setup の高速化により、ノード数を7から5に削減しても許容範囲の wall time を維持できることがわかりました。これにより compute cost を21%削減できました。
結果
| 指標 | Before | After | 改善 |
|---|---|---|---|
| Wall Time | 10m 12s | 6m 50s | -33% |
| DB Setup | 110s | 19s | -83% |
| ノード間の偏差 | 4m 02s | 1m 07s | -72% |
| Compute/回 | ~43分 | ~34分 | -21% |
| 年間コスト(推定) | $850 | $672 | -$178 |
補足: テストコードの改善
ワークフローの最適化に加えて、テストコード自体も改善しました。RSpec の .and マッチャーチェーンを使用して it ブロックを統合し、冗長な before ブロックの再実行を削減しました。
RSpec の最適化テクニックについては、 RSpec 公式ドキュメント(aggregate_failures) や こちらの実践ガイド(英語)が参考になります。
まとめ
最適化の前に計測する
時間がどこで消費されているか(DB setup、テスト実行、ノード間の偏り)を把握することで、最も効果的な最適化を選択できます。
Runtime-based splitting は導入の価値あり
キャッシュの仕組みは多少複雑ですが、テスト分散の改善効果は大きいです。
デフォルト設定を疑う
冗長な DB setup は別の環境からコピーされた設定が原因でした。CI 設定が実際の要件に合っているか、常に確認しましょう。