こんにちは! スタメンでRailsアプリの開発を担当しているシュール(@shule517)です!
Railsアプリケーションを安全に開発するために、自動テストはとても大切ですよね! スタメンではCircleCIでRSpecやRuboCopの実行をし、安全を確認して本番機へデプロイしています。
CircleCIの高速化を行った結果、倍速(約20分→10分)にすることができました!その時に対応したポイントを解説します。
CircleCIのキャッシュを有効活用!
① アセットプリコンパイル結果をキャッシュする
CI全体の処理時間のうち4分の1はアセットプリコンパイルに時間がかかっていました。アセットプリコンパイルの結果をキャッシュすることで5分ほど高速化できました。
# プリコンパイルのキーを作成 - run: name: create assets precompile cache key command: | # プリコンパイル対象のファイルの最新コミット番号をキーにする git rev-parse $(git log --oneline -n 1 app/assets lib/assets vendor/assets Gemfile.lock | awk '{{print $1}}') > VERSION # キャッシュを復帰 - restore_cache: keys: # 同じバーションからキャッシュを復帰。無ければ、直近のキャッシュを復帰。 - assets-precompile-cache-{{ checksum "VERSION" }} - assets-precompile-cache- # プリコンパイルを実行 - run: name: assets:precompile command: | current_revision=VERSION previous_revision=public/assets/VERSION # プリコンパイル対象のファイルが変更されていなければスキップ if [ ! -e $previous_revision ] || ! diff $previous_revision $current_revision; then bundle exec rake assets:precompile cp -f $current_revision $previous_revision else echo "Skipped." fi # キャッシュを保存 - save_cache: key: assets-precompile-cache-{{ checksum "VERSION" }} paths: # プリコンパイル結果の保存先を指定 - public/assets - tmp/cache/assets
※実際に使用している設定ファイルとは異なります。
アセット関連のファイルが変更されていない場合はプリコンパイルをスキップしたいため、キャッシュのキーをプリコンパイル対象のファイルの最新コミット番号にしました。また、アセット関連のファイルを修正している場合も、前回との差分だけをプリコンパイルするようになるためかなり時間が節約できます。
プリコンパイルの対象ファイルとしてGemfile.lock
を入れている理由は、Gemをインストールした時にアセット関連のファイルが変更されることがあるためです。
アセットの変更がない場合、assets:precompileをスキップすることでCIの速度を改善した を参考にしました。
② Gemのインストールをキャッシュする
CircleCI上で使うGemをGemfileに記載し、インストールしたGemをキャッシュします。RuboCopの実行に必要なGemをキャッシュし、3分ほど速くなりました!
group :test do # CircleCIでRuboCopを実行するために必要 # 修正前はCircleCIのタスクでgem installしていた gem 'rubocop' gem 'rubocop-select' gem 'rubocop-checkstyle_formatter' end
# キャッシュを復帰 - restore_cache: key: rails-gemfile-{{ checksum "Gemfile.lock" }} # Gemをインストール - run: bundle install --path vendor/bundle # キャッシュを保存 - save_cache: key: rails-gemfile-{{ checksum "Gemfile.lock" }} paths: - vendor/bundle
restore_cache / save_cache の keyをchecksum "Gemfile.lock"
とすることで、Gemfile.lock
の内容が一致した時だけキャッシュを復帰します。つまり、Gemfile
を変更した時だけGemのインストールが実行されます。
CircleCI - キャッシュ設定の例 を参考にしました。
③ ソースコードのチェックアウトをキャッシュする
ソースコードをチェックアウトした後の.gitフォルダをキャッシュします。これで前回との差分だけをチェックアウトすればよくなり、30秒ほど速くなりました!
# キャッシュを復帰 - restore_cache: keys: - source-v1-{{ .Branch }}-{{ .Revision }} - source-v1-{{ .Branch }}- - source-v1- # ソースコードをチェックアウト - checkout # キャッシュを保存 - save_cache: key: source-v1-{{ .Branch }}-{{ .Revision }} paths: - ".git"
restore_cache
に指定する keys
を複数指定しておくと、その中から一番近いキャッシュデータを復帰してくれます。今回の設定方法だと、同じブランチのキャッシュを優先し、無ければ直近のキャッシュを使用します。
CircleCI - ソースコードのキャッシュ を参考にしました。
RSpecをバランスよく並列実行する!
① ボトルネックになっているコンテナを見つける
CircleCIのTimingを見ると、分散している各コンテナの実行時間を見ることができます。16分台に完了しているコンテナもありますが、1番遅いコンテナに足を引っ張られてしまい全てが完了するのは22分です。処理を均等に分割できれば、20分以内に減らせそうですね!遅いコンテナから順に分散を検討しましょう。
② ボトルネックになっているspecファイルを見つける
RSpecの実行ログを保存することで、コンテナごとの各specファイルにかかった詳細な時間を見ることができます。どのspecファイルがボトルネックになっているかを見つけます。
- run: name: bundle exec rspec command: bundle exec rspec --format RspecJunitFormatter --out test-results/rspec.xml - store_artifacts: path: test-results/rspec.xml
③ RSpecを高速化し、ボトルネックを解消する
ボトルネックのコンテナ、テストファイルを特定したら、RSpecの高速化をしていきましょう!高速化で対応した内容がこちらです。
不要なテストデータを作らないようにする
- 不要なテストデータ作成で、テストの実行が遅くなっていた
- 未使用なテストデータを作っているletを削除した
- 使い回さないテストデータの場合は、`let!`→`let`に変更した
E2Eテストのitをまとめる
- 1つのitで1つのexpecにするのはRSpecとして正しいが、E2Eテストでは時間がかかりすぎる
- 同じ画面を確認する場合は、itをまとめることで高速化した
- itの中で複数のexpectを実行する場合は、`aggregate_failures`を使う RSpecでテストをまとめて検証する方法を参考にしてください。
E2Eテストのsleepを削除し、have_xxxで待つ
- 画面描画を待つためにsleepをしていたため、テストが遅くなっていた
- テストケースの数 × sleepの時間だけ無駄な待ち時間が発生してしまう
- sleepの代わりにhave_xxxマッチャを使い、待ち時間を最小限に抑えた
巨大なspecファイルを分割する
- RSpecの分散はテストファイル(*_spec.rb)ごとに行っている
- 高速化を行っても、テストケースが多いテストファイルはボトルネックになってしまう
- そのため、巨大なspecファイルを分割し、均等に分散できるようにした
おわりに
CircleCIの高速化を行い、待ち時間が減ることで開発のストレスがかなり減りました!みなさんがCircleCIを高速化する時に、参考になればとても嬉しいです。待ち時間を減らして、より楽しく効率的に開発をしていきましょう!
熱い仲間と一緒に、より良いサービスを一緒に作りませんか? 仲間を募集しています! まずは、スタメンのエンジニアサイト stmn, inc. Engineers を見てみてください!