RailsのCIのテスト実行時間を 10分から5分に高速化した話

FindyでEMをしている栁沢(@nipe0324a)です。

今回は、FindyのとあるRailsのCIのテスト実行時間を10分から5分に高速化した話をご紹介します。

  • 「CIのテスト実行時間が遅い...」
  • 「CIの実行時間を短くしたい!!」

と感じている方はぜひご覧くださいませ。

Findyでは2024年2月現在、1人あたり1日4プルリクを平均で作っています。静的解析や自動テストなどを即時に行うCI環境がないとスピード感のある開発ができなくなるため、CIを高速で回しタスクを完了させる必要があります。機能も増え、テストケースも拡充したことでCIの高速化が求められるようになりました。

また、個人的には、CIは遅くても10分、理想は5分以内で終わるのを1つの目安にしています。これぐらいのスピード感でCIが完了すると、「プルリク作ってレビュー依頼する」、「レビューコメントもらって対応する」といったことがサクサクできます!

1. CIテスト実行時間の高速化の結果

最初にテスト実行時間の高速化の結果を紹介します。

弊社では、ソースコードをGitHubで管理しているので、CIツールももれなくGitHub Actionsを使っています。

GitHub Actionsの「並列化の促進」や「テスト実行の偏り」を減らすことで、CIのテスト実行時間を約2分の1にしました。

  • 対応前:約10分
  • 対応後:約5分

また、費用対効果もよかったと考えています。

  • 月間のGitHub Actionsのコストが2万円ぐらい増加
  • 1ヶ月あたりのCI待ち時間が約7日分減る

2. テスト実行時間の高速化の前提条件

そもそも既存でも、CI上のテストは、「5台のマシン」かつ「1マシンあたり4コア」と20並列でテストを実行していました。

テストを20並列で実行するために、GitHub Actionsのワークフローで次のようなテクニックを使っていました。

  • 5台の複数マシンでテスト実行
    • GitHub Actionsのmatrixを使い5台のマシンを動かす
    • 対象のテストファイルを5台のマシンに分散させる簡易なshellスクリプトを使う
  • 4コアでテスト実行

このような対応をしてもプロジェクト規模の拡大とともにCIのテスト実行時間が遅くなってきたので、さらなる改善を実施しました。

3. 今回のテスト実行時間の高速化で実施したこと

今回のテスト実行時間の高速化にあたり、具体的には次の3つを実施しました。

  • 不要なカバレッジレポートの作成をなくす
  • テスト実行の並列度を高める
  • テスト実行時間の偏りを減らす

それぞれ効果があったのですが、「テスト実行時間の偏りを減らす」が他の対応との相乗効果をうみだし、テスト実行時間を大きく減らすことができました。

3.1. 不要なカバレッジレポートの作成をなくす

対応前のテストのワークフローを見ると、テスト実行後に「Post coverage report」というジョブでカバレッジレポートをプルリクにコメントしていました。

これは、自動テストを拡充するときに使われていたのですが、既にテストカバレッジは90%以上になっているためほぼ必要ないためリリースプルリク以外では実行しないようにしました。

これで、テストの実行時間が30秒ぐらい早くなりました。

3.2. テスト実行の並列度を高める

次に、マシン台数を5台から10台に変更することで、並列度を20から40と2倍に高めました。

マシン台数の増加は既存で仕組みが揃っていたので、変更は次のように実施できました。

# .github/workflows/test.yml

    strategy:
      fail-fast: false
      matrix:
-      ci_node_total: [5]
-      ci_node_index: [0, 1, 2, 3, 4]
+      ci_node_total: [10]
+      ci_node_index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

    steps:
      # 省略

      - name: Run tests
        env:
          CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
          CI_NODE_INDEX: ${{ matrix.ci_node_index }}
        run: |
          TEST_FILES="$(find ./spec -type f -name "*_spec.rb" | xargs ./scripts/ci/rspec_split_files.sh)"
          bundle exec parallel_rspec -- --format progress --color -- $TEST_FILES

これでテスト実行時間が2-3分ほど早くなり、6-7分ぐらいになりました。

3.3. テスト実行時間の偏りを減らす

最後に、テスト実行時間の偏りを減らす取り組みをしました。

テストの並列度を2倍にしたのに、いまいちテスト実行時間が早くなりませんでした。そのため、どこがボトルネックになっていそうか調べてみると、テスト実行時間の偏りがボトルネックになっていそうでした。

以下の例ですと、「Test (10, 8)」が2m25sなのに比べて、「Test (10,3)」が5m30sとなっており、テスト実行時間の差が最大3分ほどあります。

さらに、詳細をみていくと、「マシンごとのテスト実行時間の偏り」と「マシン内のコアごとのテスト実行時間の偏り」の両方が発生していました。そのため、次の対応をしてテスト実行時間の偏りを減らしました。

  • マシンごとのテスト実行時間の偏りの対応
    • マシンへのテストファイルの振り分けをファイル名からテストファイルのサイズ順に変更
    • 「ファイルサイズ ≒ テスト実行時間」と考えて、時間がかかるテストファイルを偏らないように変更
  • コアごとのテスト実行時間の偏りの対応
    • マシン内でコアごとにテストファイルが割り当てられて実行されていたので、時間がかかるテストファイルを割り当てられていたコアの実行時間がかかっていた
    • ファイル単位からチャンク単位でテストを実行するために、 parallel_testsparallel_split_test に置き換え(※parallel_testsと作者同じ)
# .github/workflows/test.yml

      - name: Run tests
        env:
          CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
          CI_NODE_INDEX: ${{ matrix.ci_node_index }}
        run: |
-         TEST_FILES="$(find ./spec -type f -name "*_spec.rb" | xargs ./scripts/ci/rspec_split_files.sh)"
+         TEST_FILES="$(find ./spec -type f -name "*_spec.rb" -exec ls -l {} + | sort -n -k 5 | awk '{print $9}' | xargs ./scripts/ci/rspec_split_files.sh)"
          echo $TEST_FILES
-         bundle exec parallel_rspec -- --format progress --color -- $TEST_FILES
+         bundle exec parallel_split_test $TEST_FILES --format progress --color

これでマシンとコアのそれぞれでテスト実行時間の偏りが減少し、5分以内にテストが完了するようになりました。

4. 最後にGitHub Actionsのコストについて

並列度を2倍に増やしましたが、コストはそこまで高くなっていません。

GitHub Actionsの料金は「OS x vCPU数 x 分あたりの料金」で課金されます(参考:GitHub Actionsの課金について)。今回は、並列度を2倍に増やしましたが、Billable timeは30分前半から40分ぐらいと1.2〜1.3倍ぐらいなので、コストも1.2〜1.3倍ぐらいの増加になっています。

1ヶ月単位だと、「ざっくり2万円ぐらいのコスト増加(※1)になりますが、CI待ち時間が7日分ぐらい削減(※2)」できました。必ずしもCI待ち時間がムダではないですが、エンジニアの人件費を考えると費用対効果は高いのではと考えています。

※1. 4coreの分あたりの料金 $0.016 x (変更後のBillable time 40分 - 変更前のBillable time30分) x 月間CIテスト実行回数 700回 x 為替150円
※2. 待ち時間の削減時間 5分 x 月間CIテスト実行回数 700回 ÷ (60分 x 営業時間 8時間)

5. CIテスト実行時間の高速化のまとめ

最後にまとめです。

今回は、FindyのRailsプロジェクトで、CIのテスト実行時間を10分から5分と2分の1に高速化した話をご紹介しました。

具体的には、次のテクニックで高速化を実現しました。

  • 不要なカバレッジレポートの作成をなくす
  • テスト実行の並列度を高める
  • テスト実行時間の偏りを減らす

また、費用対効果もよかったと考えています。

  • 月間のGitHub Actionsのコストが2万円ぐらい増加
  • 1ヶ月あたりCI待ち時間が約7日分減る

なにか、参考になる内容があれば嬉しいです。

最後に、弊社のエンジニアが次のようなスライドも公開しているので興味があればご覧くださいませ。

speakerdeck.com

また、Findyは積極的にエンジニアを採用しています。CI/CDを始め、Four Keys、開発生産性、技術トレンド、転職市場など興味のある方は、お気軽にカジュアル面談を受けてみてください :)

Findyの採用情報はこちら↓

herp.careers