CI上のRailsのRSpecテストが30分もかかっていてストレスフルだったので、テストを並列化する等で30分かかっていた時間を12分まで削減しました。
そのときのメモ。
環境
- AWS CodeBuild
- Ruby
- 2.7.2
- Rails
- 6.0.3.6
- Parallel_tests
- 4.4.0
- MySQL
- 8
前提
docker compose
でテストは閉じている。そのため、RDS等のアクセスは発生していない。
対応
parallel_testsライブラリを入れる
並列化ライブラリのparallel_tests
を導入します。
group :development, :test do gem 'parallel_tests' end
テストのDB名を修正する
並列化したプロセスごとに独立したスキーマを割り当てないとデッドロックが頻発するのでプロセスごとにスキーマを作成します。
parallel_tests
ライブラリが実行しているプロセスごとに環境変数TEST_ENV_NUMBER
を用意してくれるので、suffix
に環境変数を指定します。
test: database: sample<%= ENV.fetch('TEST_ENV_NUMBER', '') %> # sample # sample2 # sample3 # といった具合に、プロセス数に応じたスキーマ名が振り当てられます。 # 1は割り振られないので、どうしても1と割り振りたい場合は自作rakeが必要です
テスト実行時のRakeを自作する
私の環境の場合docker compose
を使用して閉じた環境でテストしていることもあり、商用環境テーブルは不要でした。そのため、parallel_tests
のライブラリをそのまま使用するとマイグレーションチェックが走ってエラーが発生してしまったため、その過程を飛ばしています。
また、自作Rakeがdevelop test
groupに依存しているのもあり、商用でエラーになってしまうので、LoadError
をスキップするようにしています。
# frozen_string_literal: true # NOTE: テスト時にしか使用しないものなので、商用ビルドするとインポートエラーが発生してしまう begin require 'parallel_tests/tasks' rescue LoadError puts 'Unable to load parallel_tests/tasks, skipping' end namespace :my_parallel do # NOTE: 必要な個所のみ抜き出す # https://github.com/grosser/parallel_tests/blob/8129c4c6feb6db7f94de144b995ea2a42aae0af5/lib/parallel_tests/tasks.rb#L258C1-L266C8 task :spec, [:count, :pattern, :options, :pass_through] do |_t, args| # 商用環境テーブルのマイグレーションはチェックしない # ParallelTests::Tasks.check_for_pending_migrations ParallelTests::Tasks.load_lib command = ParallelTests::Tasks.build_run_command("spec", args) abort unless system(*command) # allow to chain tasks e.g. rake parallel:spec parallel:features end end
CIを修正する(buildspec-ci.yml)
AWS CodeBuild
を使用しているので、次のようなyml
を用意しました。
phases: pre_build: commands: # MySQL 8 だと_utf8mb4 というsuffixがついてしまうので、それを削除する - sed -i 's#_utf8mb4\\\\'\''_\\\\'\''#'\''_'\''#g' db/schema.rb # スキーマの削除・作成・db/schema.rbの反映 - docker-compose run --rm api rails parallel:drop parallel:create parallel:load_schema build: commands: # 自作rakeでのテスト実行 - docker-compose run --rm api rails my_parallel:spec
プロセス数は指定したイメージのCPU数に依存するので、もし指定したCPU数よりも少ないプロセスで動かしたい場合は環境変数で指定してください。ローカルPCで挙動確認すると、20並列で動いてしまって期待の速度が出なかったので制御していました。
env: variables: PARALLEL_TEST_PROCESSORS: 4
結果
もともと、30分の内訳としては次のとおりでした。
- 起動準備
- 2分
- PRE_BUILD
- 5分弱
- BUILD
- 23分
テストを並列化した結果、25分になり、内訳は次のとおりでした。
- 起動準備
- 2分
- PRE_BUILD
- 5分弱
- BUILD
- 18分
ここまではコンピューティングが3GBメモリ2vCPUでしたが、7GBメモリ4vCPUに変更した結果。16分になり、内訳は次のとおりでした。
- 起動準備
- 2分
- PRE_BUILD
- 5分強
- BUILD
- 9分
最後に15GBメモリ8vCPUまで性能を上げて、12分になり、内訳は次のとおりでした。
- 起動準備
- 1分40秒
- PRE_BUILD
- 5分強
- BUILD
- 5分弱
AWSが用意するコンピューティングとのバランスを考えたときに、並列化によるこれ以上のメリットはないと考えてここで終了しています。
ソースコード
なし
終わりに
イメージ作成したり、DBを用意したりするPRE_BUILD
が測定前に想像していなかったほど重く、ここを改善しないとこれ以上の高速化は見込めませんでした。
ビルドをDocker Build Cloud
に任せたり、DBを作ったイメージごとsave
して流用する等々の考えもありましたが…。複雑になる上、高速化に貢献するか微妙なラインだったので、検証はしていません。
EC2だけでなく、Lambda
をうまく組み合わせたら早いとかそういう話があればぜひ聞きたいです。
参考情報
https://www.publickey1.jp/blog/24/docker40docker_build_cloudappleaws_graviton.html