並列処理でRails5アプリURIHOのRSpecの時間を短縮する
こんにちは、平尾です。URIHOの開発を担当しております。
最近RSpecの時間が長く、開発速度に影響がでていることに悩んでいたのですが、
RSpecの処理を並列化するparallel_testsというGemを導入して処理時間を短縮することができました。
この記事ではparallel_testsを導入した経緯を通常のRSpecが遅い理由を共に説明し、
parallel_testsの導入方法と実際にどれだけ短縮されたかをご紹介いたします。
通常のRSpecが遅い理由
一般に処理速度のボトルネックはCPUとI/Oに分けられます。
処理が遅いと感じたらコンピュータのCPU使用率や各種I/O使用率(ディスクI/OやネットワークI/Oなど)を見てみてください。
CPU使用率が低くディスクI/O使用率が高かったり
CPU使用率が高くディスクI/O使用率が低かったり
CPU使用率とディスクI/O使用率が両方低かったりします。
通常のRSpecが遅い理由の1つはシングルスレッドで実行していることです。
シングルスレッドではCPUの1コアしか使えないため、CPU使用率は低くなります。
この場合はCPUの全コアを効率的に使えていないことがボトルネックになっています。
他にはハードウェア、ネットワーク、データベース、テストするアプリが原因であることがあります。
原因は多岐に渡るので、高いトラブルシューティング能力やチューニング能力が求められます。
URIHOの場合、RSpecがシングルスレッドで実行されていてCPU使用率が低いことがボトルネックになっていました。
そこでRSpecを並列化するGemを探したところ、parallel_testsは利用例が多く手順も簡単だったので導入してみました。
parallel_testsを導入
parallel_testsにはRSpecを並列処理し、処理時間を短縮する機能が実装されています。
RSpec以外にもTest::UnitやCucumberなど複数のテストフレームワークをサポートしています。
前提条件
検証に使用した環境を以下に示します。
CPU | Intel Core i7-1065G7 1.30GHz(ハイパースレッディング有効) |
---|---|
論理プロセッサ数 | 8 |
Webアプリケーションフレームワーク | Ruby on Rails5 |
データベース | MySQL 5.7 |
RSpecのGemはrspec-railsを使っています。
準備
parallel_testsの実行環境を準備します。
やることは以下の通りです。
- Gemをインストール
- 複数のデータベースを動的に自動生成する設定
- テストユーザにテスト用データベースを操作する権限を与える
- parallel_testsのrakeタスクでテスト用データベースを作成
Gemfileに以下の内容を記述し、bundle install
を実行してGemをインストールします。
gem 'parallel_tests', group: [:test]
次にconfig/database.ymlを以下のように編集し、データベースの自動生成を設定します。
test:
database: yourproject_test<%= ENV['TEST_ENV_NUMBER'] %>
user: test_user
次にテスト用のデータベースを操作する権限をテスト用のユーザ(ここではtest_user)に与えます。
データベース名の末尾にワイルドカードを指定しすることでプロセス数の変更に対応します。
GRANT
ALL
ON
`yourproject\_test%`.*
TO
`test_user`
次にデータベースを作成します。
parallel_testsのrakeタスクでRails側からデータベースの破棄、作成、マイグレーションすることが可能です。
最初にデータベースを破棄しているのは、既存のテスト用データベースを破棄してクリーンな状態にするためです。
$ rake parallel:drop
$ rake parallel:create
$ rake parallel:migrate
処理時間の最適化
処理時間を最適化するためのログを取る設定をします。
アプリケーションのルートディレクトリに.rspec_parallel
ファイルを以下の内容で作成し保存してください。
--format progress
--format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log
parallel_testsは通常、各プロセスが処理するファイルサイズの総量が均等になるようにテスト用のrbファイルを振り分けます。
ログが無い場合は通常通りファイルサイズを見て振り分けますが、
ログを取る設定にしている場合は前回のテストのファイルごとの実行時間を見て振り分けます。
ログを取ることで特定のプロセスが大幅に遅れる可能性を排除できます。
計測
テストの直列実行と並列実行の処理時間を比較、検証していきます。
parallel_testsがログを使用してテストの割り振りを均一にするため事前に1回テストを実行しています。
以下のコマンドを実行します。
プロセス数を指定しない場合は自動的に論理プロセッサ数と同じ数になります。
$ rake parallel:spec[(プロセス数)]
プロセス数別に処理でかかった時間は以下の通りです。
プロセス数 | 1(直列) | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
処理時間 | 16分7秒(100%) | 8分26秒(52%) | 6分39秒(41%) | 5分40秒(35%) | 4分55秒(30%) | 4分10秒(25%) | 3分54秒(24%) | 3分50秒(23%) |
論理プロセッサごとに色分けした使用率のグラフを示します。
縦軸が各プロセッサ使用率、横軸が実行された時間です。
1プロセッサずつ増やしながら連続で実行しました。
赤矢印の箇所で30秒スリープさせています。
プロセス数の増加と共にプロセッサの使用率が上昇していることが分かります。
考察
2プロセスの場合は52%の時間で処理されました。
テストを2分割して処理したのでだいたい予想通りです。
しかし3~8プロセスの場合は3分の1~8分の1の時間にはなっていません。
理由の1つとして考えられるのはプロセスが投げる命令をCPUが一度に処理しきれなかったことです。
Intelのハイパースレッディングによって論理プロセッサ数は8ですが、物理的には4コアしかありません。
論理プロセッサ数が2倍になったところで性能が2倍になるわけではなく、
計算の種類に依りますが1.2~1.4倍程度の性能アップになることが一般に知られています。
4プロセスで並列処理した場合、グラフ上の見かけではまだ余裕があるように見えますが1コア全体で見るとフル稼働している可能性は十分あります。
parallel_testsによって生成された8プロセスを割り当てたとしてもCPUの処理能力は変わらないので、4プロセス分のタスク量あたりが限界ということです。
また3プロセスから4プロセスにした時点であまり時間短縮がされなかったことは、
CPU以外にボトルネックが発生している可能性があるので検証が必要そうです。
学び
parallel_testsを使って効率的にCPUを使ってRSpecを処理することができました。
タスクを分割して並列化するとき、CPUのコア数以上のプロセスに分割しても速度の向上はあまり見込めないこともわかりました。
加えて実行時間だけ見ていると各テストの速度が隠蔽されることに注意しなければなりません。
ログを見て遅いテストを改善することが大切ですね。
まとめ
RSpecのテスト時間をparallel_testsで高速化する方法のご紹介でした。
テストが早くなって開発が捗るようになって嬉しいです。
インフラを最大限活用する大切さを再認識することもできました。
さて、ラクーングループは一緒に働く仲間を絶賛大募集中です!
ご興味を持っていただけましたら、こちらからエントリーお待ちしています!