読者です 読者をやめる 読者になる 読者になる

Thanks Driven Life

日々是感謝

parallel_tests ではなく test-queue に乗り換えようと思う

みなさま、もうすぐ今年の 2/3 が終わる今日この頃、いかが並列にお過ごしでしょうか。

私も快適に並列を生きていくために、今回 parallel_tests から test-queue に乗り換えようという気持ちになったということをご報告致します。

経緯

現在、PHP の Web アプリケーションを capybara + turnip でテストするお仕事をしており、 最近ようやく富豪な Docker ホスト環境に触れるようになったということもあって、 以前試した Selenium Grid + Docker でがりがり高速化に勤しんでいます。

そんなこんなで RSpec の同時並列実行もしないとね、ということで、以前から触っていた parallel_tests を導入しました。 これまでは全テストの完了まで 約40分 かかっていましたが、parallel_tests を導入するだけで 約10分 まで縮めることに成功しました。

数値自体は良いものだったのですが、内容には少し不満もありました。

問題点: worker 毎に "テストの重み" における偏りがある

まずは下記をご覧ください。parallel_tests で実行したテスト結果から、各 worker の実行時間を抜き出したものです。

$ bundle exec parallel_rspec -n 10 spec/
...
Finished in 4 minutes 50.6 seconds
Finished in 5 minutes 1 second
Finished in 5 minutes 5 seconds
Finished in 5 minutes 59 seconds
Finished in 6 minutes 2 seconds
Finished in 6 minutes 47 seconds
Finished in 6 minutes 47 seconds
Finished in 8 minutes 31 seconds
Finished in 8 minutes 58 seconds
Finished in 9 minutes 11 seconds

最初の worker は約5分で終わっているのに対し、最後の worker は9分を越えています。 つまり最初の worker は 約4分は暇している ということになります。 並列分散実行において worker を如何に暇を与えずギリギリまで働かせられるかが勝負のカギなので、これではもったいなさすぎます。

問題点の原因: worker に対するタスクの割り振り方

parallel_tests は、テスト開始前に全テストの数を worker の数で割って、それぞれの worker に割り振ります。 このとき「このテストはあの worker に割り振ろう」という基準に使っているのが

といったものです。

test/unit や rspec でいえばファイルサイズを基準にするのも悪くないかなとも思いますが、 cucumber や turnip のように各ステップを抽象化したファイル (.feature) でファイルサイズを使って判断するのは微妙です。 ステップの中身(参照している実装)がどれだけ複雑&大量の処理を行っていても、feature ファイルで見ればただの1行なので。

※ ちなみに cucumber や turnip を parallel_tests で実行する時に使われる gherkin runner の場合は シナリオの数が平準化するように worker に割り振られます。前述した結果はこの runner を使っていたのですが、各シナリオが持つステップが均一の処理時間になっているとは限らないので、結果あまり効果がありませんでした。

解決案(今回はボツ)

クックパッドがオープンソースとして出している rrrspec なら、上記の問題は解決できそうでした。 parallel_tests と違いあらかじめテストを worker に割り振るのではなく、全テストを一度キューに溜めておいて、worker に空きができたらそいつに投げつけていく、というスタイルになっているので、全テストが完了するまで worker が暇になることはありません。

rrrspec は試したこともある のでまずはこれを入れようかとも思いましたが、 正直なところ rrrspec を実行するまでの「MySQL 立てて Redis 立てて〜」ってのが少し億劫で、手が出しづらかったです。

test-queue との出会い

「なんか他にねーかな」と探していたところ、それっぽいのが見つかりました

tmm1/test-queue · GitHub

タスクの割り振りは rrrspec と同じで、キューに溜め込んだテストを暇してる worker に投げつける方式です。 違いは rrrspec と違って MySQL も Redis も使わず、$ gem install test-queue するだけで使えるようになるということです。

実行も簡単

$ bundle exec rspec spec/

これを

$ bundle exec rspec-queue spec/

これにするだけです。テスト側を修正する必要無し。 これで前述の parallel_tests で測定したテストと同じものを走らせてみました

$ TEST_QUEUE_WORKERS=10 bundle exec rspec-queue spec/
==> Summary (10 workers in 434.6148s)

    [ 4]                           56 examples, 0 failures, 5 pending         7 suites in 425.8578s      (pid 53184 exit 0)
    [10]                           45 examples, 0 failures, 1 pending         7 suites in 427.4674s      (pid 53190 exit 0)
    [ 5]                                       43 examples, 1 failure         7 suites in 428.1569s      (pid 53185 exit 1)
    [ 6]                           39 examples, 0 failures, 3 pending         7 suites in 428.1838s      (pid 53186 exit 0)
    [ 7]                                      43 examples, 6 failures         7 suites in 429.3289s      (pid 53187 exit 1)
    [ 2]                           25 examples, 0 failures, 1 pending         4 suites in 429.5609s      (pid 53182 exit 0)
    [ 1]                                      43 examples, 0 failures         6 suites in 429.7932s      (pid 53181 exit 0)
    [ 8]                           53 examples, 0 failures, 3 pending         8 suites in 432.0195s      (pid 53188 exit 0)
    [ 3]                            38 examples, 1 failure, 6 pending         5 suites in 433.0152s      (pid 53183 exit 1)
    [ 9]                           48 examples, 0 failures, 3 pending         7 suites in 434.5934s      (pid 53189 exit 0)

結果を見てもらうとわかる通り、各 worker がすべて 425 〜 435 秒で終わっており、テスト完了まで全 worker に暇を与えていないことがわかります。(テストは失敗していますが気にせず…)

そんなわけで parallel_tests から test-queue に乗り換えるだけで2分ぐらい短縮できました。10分切ってくると1分1分が大事。 ちなみにどの worker が暇になってるかーとかそういう情報は unix domain socket でやりとりしている模様です。

おまけ: 学習

test-queue はテストが完了すると、カレントディレクトリに .test_queue_stats というファイルを作成します(環境変数 TEST_QUEUE_STATS で変更できます)。このファイルには、さきほど完了した各テストの実行時間が記録されており、次回 test-queue 実行時にこのファイルを参考にして worker への割り振り方を変えています。

※ 具体的には、前回のテストで 実行時間が長いやつから先に worker に割り振っていく ようです

おまけ: RSpec 分散実行

parallel_tests のもう一つの問題点として、例えば parallel_tests -n 50 とかすると Ruby のプロセスが50個作られるので、テスト実行環境の負荷によって実行時間がよろしくない形になることです。

rrrspec や test-queue は、RSpec コマンドを実行する環境の分散化が可能になっており、test-queue でいえば こういう感じで 実現できるようです(ちょっとまだ試してないんで多分……)

まとめ

test-queue よかった。ツールと金の力でテストを一桁分台にしていこう!!!