要約
Perlのテストを複数台のサーバに分散させて実行させるためのモジュール、App::Ikarosを用いて拡張可能な高速CI環境を作れたのでハッピーというお話です。
さらにApp::IkarosはJenkinsで読み込ませる用の出力を吐いてくれるので結果がきれいに可視化できる点もグッドです。
実際に導入した経験をハマりどころなど含めて簡単に紹介します。
フルテストが遅い? 我々はPerl使いなので
App::Ikarosがあるじゃないか!
App::Ikarosはどういうときに使うのか?
本モジュールが適しているのは以下のような場合です
- テストの数がとにかく多すぎて、一台のテスト実行サーバだと限界がある
- forkproveを使って高速化したいのだけれど、一部どうしても失敗するテストがあって、全部に対しては適用できない
基本的な使い方については上記の開発者のドキュメントの通りです。
forkproveはモジュールを先に読み込むことでテストごとに同じモジュールをなんども読み込むオーバーヘッドを無くしてくれるproveですね。
弊社でもプロダクトの成長に伴いテストが増加していたので、フルテストには20分(!)くらいかかっていました。またforkproveを使うと45%くらいテストが失敗するのでforkproveはなかなか活用できないでいました。
実例: 20分かかっていたテストをサーバのスペックアップにより8分へ、さらに実行時間をApp::Ikarosで約5分にしました
App::Ikaros導入の前に、クラウド化で空いたサーバがあったのでサーバのスペックアップを行いました。これで20分から8分と半分以下の所要時間になりました!やはり札束で殴るのは大事
しかし、依然として8分は長いという意見がちらほらありました。
それに最近の急激なテスト時間の増加のことを考えるとまたすぐに10分以上テストにかかる時代がきてしまうのは火を見るよりも明らかでした。
1台のサーバで遅いなら2台のサーバに実行させよう
今回は、スケールがしょぼいですが、スペックの高いサーバが2台あったのでこれを繋げることにしました。
App::Ikarosの処理の概要
App::Ikarosを使うとテストをn台のサーバに分散して実行させるので、サーバを追加すればするほど短い時間でフルテストを回せるようになります。またテスト結果は保存され、次のテストで実行時間の長いもの同士が同じテストクラスタに入らないように配慮されているそうです。
- JenkinsサーバにBuildリクエストが投げられる
- JenkinsサーバからApp::Ikarosがinstallされているノード(Masterノードとする)にテスト実行要求を投げる
- Masterノードから、n台にテスト実行要求を投げる
- n台でbuild
- 結果をMasterに集約
- MasterからJenkinsに結果を渡す
- Jenkins上で結果表示
各ホストでの実行結果は非同期にMasterノードへ送られる。各ホストの最終結果を受け取った後、失敗したテストについてはMasterノードで再テスト(並列度1のproveで)する。
一般に、並列度をあげるとDBリソースの競合のためにテストが失敗しやすくなる他、特にforkproveでテストしたものについては失敗しやすいので、App::Ikarosが並列度1のproveで再テストしてくれるのは嬉しい仕様です。
config/hosts.yaml
# 全ホストのdefaultパラメータを指定する
default:
user: jenkins
private_key: /var/lib/jenkins/.ssh/id_rsa # passphraseはなし
coverage: false # trueにするとcpanmとDevel::Coverを勝手にインストールしてくれてカバレッジを計測しつつテストしてくれるようになる
perlbrew: false
runner: forkprove
workdir: /var/lib/jenkins/ikaros_workspace
# ホスト毎の設定を個別に設定する
hosts: # hostsに書いた設定がdefaultよりも優先される。
- master # ホスト名
runner: forkprove
workdir: /var/lib/jenkins/ikaros_workspace2
- master:
runner: prove
- remote:
runner: prove
注意点としては、
- hosts.yamlではhostsに書いた設定がdefaultよりも優先される点
- forkproveとproveが両方指定されなければならない点
があげられるかと思います。
config/ikaros.conf
- テスト用のファイル一覧として$all_testsを用意します。
"t/"を渡せばprove -r
で再帰的にテストを実行してくれそうですが、最後の結果の生成がうまくいかないので、File::Find#findなどを使ってテストファイルの一覧を渡しましょう。 - forkproveを指定したくなければ、$forkprove_testsに空のArrayRefを渡せば、ここでforkprove抜きのテストが実現できます。
- before_commandsでリポジトリのcloneなりpullなりをしておくと良いでしょう。この時、cd hoge;などとしても、次のコマンドに移ったらまたhosts.yamlで指定したworkdirに戻るようです。
App::Ikaros実行結果
フルテストの実行時間が短縮できたよ
8分だったテスト実行時間が約5分になった!またテストが増えて伸びてきたらsshできるマシンを増やせばいい!
他の嬉しいこと
- テストの実行結果の可視化がログしかなかったところにJenkinsの結果可視化機能が導入されたので、例えば各テストの実行時間でソートが行えるようになり遅いテストのリファクタリングが捗るようになった。
- テスト実行順が固定されてしまっていたところ、テストケースを賢く振り分ける関係上、自然とランダムにテストが実行されるようになり、実行順序依存のテストが入ってしまっても検出できるようになった。
少しはまったこと
ブランチ戦略
App::Ikarosのbefore_commands内でgit pull
を各ホストで実行させた上でテスト対象ブランチに移動してテストさせてましたが、これ、ローカルブランチを作成してそこでテストを実行させるとコンフリクトが発生することがあって、その場合は-f
なしのgit checkout
ではブランチ移動できなくなり正常なテストができません...
そもそもローカルブランチを利用する必要はなく、Jenkins Pipelineのcheckout
を参考にしつつ、リモート追跡ブランチの先頭コミットの一時ブランチをテスト実行場所としました。
mysqldが残る件
特にJenkins上でフルテストを中断した時にmysqldプロセスが残ってしまうことがあり、テストごとにmysqldを立ち上げるようにしていたので、次のフルテストでmysqldのプロセス数制限にかかり、各テストがDBに接続できないエラーを吐いてとにかく失敗してしまう場合がありました。
解決策
どうしてmysqld(やperlのテストのプロセスも時々)がゾンビ化してしまうのか、原因はよくわかっていないのですが、pkill mysqldをビルド終了後のフェーズで実行させること(Jenkins側のpipelineならpost{})で解決し、App::Ikarosが実運用に耐えられると判断しました。
所感
まだ本格的に運用し始めて2週間くらいですが、特に問題なく動かせています。
EC2とかDockerとか大流行りの中で、物理サーバでJenkinsを動かす話でしたが、自社サーバで使用料はかからないし、遊ばせていても勿体ないので物理サーバを使ったのは悪くない選択肢だったかなと思います。
フルテストの待ち時間が短くなったことでフルテストをどんどん回してもらって、変更に対するフィードバックを得るサイクルをがんがん回して、エンジニアが書くコードの品質がますます高まるといいですね!
明日は@mattakさんのUniRxのお話です!