メリークリスマス!🎄
Misoca開発の@kokuyouです。
Misoca Advent Calendar 2017、遂に最終日です。 昨日の記事は@eitoballの開発の小ネタ劇場でした。
本日 Ruby 2.5 が出る予定ですが、特に関係ないCIの話をしていきたいと思います。
どうでもいいんですが、CIという略語を見ると Continuous IntegrationなのかCANDY ISLANDなのか迷う今日このごろです。
⏳ ビルド時間の推移
以前の記事で紹介したとおり、MisocaではCIでのRSpec分散実行にrrrspecを使用しています。
これにより20分ほどかかっていたビルドを11分程度まで短縮することができたのですが、それでも11分は結構待たされるな―、という気持ちになります。スピーチとCIのビルド時間は短いほうが良いのです。
そんなわけで、頑張って短縮してみました。
以下は7月~9月ごろのビルド実行時間推移を表したグラフです。
青線がビルドごとの実行時間、赤線が直前10ビルドの移動平均になっています。
y軸の上限を1000秒(=16分40秒)に取っていますが、初期はこの上限を軽々と突き抜けていることがわかります。 全体的にも600秒(=10分)はまず下回らない感じです。
一方で、対処を始めた8月ごろからグラフが右肩下がりになり、終盤では400秒を下回っています。
直近(10月~12月)では以下の通り、ほとんどのビルドで400秒(6分40秒)を下回り、軽いビルドでは300秒(=5分)程度で完了しています。 なお、たまに飛び跳ねてるのもDependabotで一気にビルドキューが積まれたときのものでした。
ここまで実行時間が短縮されたことで、待たされていると感じることがだいぶ少なくなりました。
💪 やったこと
💰 金の弾丸
最初は「rrrspecの並列数増やせばスケールするでしょ」と思いmax_worker
を5から10に上げてみたのですが、ほとんど短縮せず悲しい気持ちになりました。
仕方がないので、ここからは時間がかかってる処理をひとつずつ調べて潰していく戦略にシフトしました。
🖥 MySQLのローカル同居
当初は レシピに組み込むのがめんどい MySQLの起動オーバーヘッドやメモリ量などを考慮して、rrrspec workerノードとは別にMySQLノードを立てていました。
しかしMySQLノードを固定で立ち上げているとスケールしないことや、接続数・転送量が逼迫していることから、MySQLをrrrspec workerノード内に同居させることにします。
datadirをtmpfsに向けるのに苦戦したり、AppArmorにMySQLプロセスの起動を阻まれたりしたのですが、なんとか完遂し、なぜかこれだけで1分程度実行時間が短くなりました。
帯域とか転送速度の問題でこれまでの戦略がイケてなかったという話なんですが、まぁ結果オーライです。
(この時点でjob全体10min、うちrrrspecが8min)
🔗 rsyncの設定見直し
さらにrrrspecの実行ログを確認したところ、実行対象をrrrspec master -> rrrspec workerへ同期するrsyncが1min以上かかっていました。
以下の設定を見直し、rsyncにかかる時間を5sec程度まで短縮しました。
- Jenkinsではjobごとにworkspaceを持ちタイムスタンプが異なるため、
--times
ではなく--checksum
を使う node_modules
やtmp/cache
など不要なディレクトリが同期されていたため、--exclude
指定に追加する
(この時点でjob全体9min、うちrrrspecが7min)
🔪 specの分割
MySQLがスケールするようになったためmax_worker
を再度上げていったのですが、やはりあまり実行時間は短縮されませんでした。
個別に実行時間を見てみると、なんと単独ファイルで7minかかっているfeature specが……
rrrspecはファイル単位で分散実行するため、そりゃ7minは下回れないなぁ…… という気持ちになり、そのspecを幾つかのファイルに分割しました。
その後もボトルネックになったspecを分割したり不要なテストを消したりと地道に潰していった結果、10workerでのrrrspec実行時間を5minまで短縮することができました。
(この時点でjob全体7min、うちrrrspecが5min)
🏗 package install & build
オーバーヘッドの比率が上がってきたため、そちらで時間がかかっている処理に目を向けていきます。
オーバーヘッドのうちbundle install
が20sec、yarn install
が30sec、yarn build
が1min程度で、依存パッケージ解決と静的ファイルビルドに殆どの時間を使っていました。
ここで依存パッケージは Gemfile.lock
や yarn.lock
が一致していれば同じものを使いまわせますし、yarn build
もJavaScriptのソースなどに変更がなければ同じものを使えます。
そこでCircleCI 2.0のキャッシュ機構を参考に、Jenkinsfileでキャッシュを行う関数を用意しました。
def yarn_install() { cache( ['node_modules'], get_cache_key(['package.json', 'yarn.lock']), { bash "yarn install --no-progress --pure-lockfile" } ) } def cache(paths, key, callback) { if (!fileExists(cachedir(key))) { callback() for(def path: paths) { // node_modulesのrequireの仕様上、ディレクトリ構造の保持が必要なので path/to/file ような場合に path/to までを作成したい sh("dirname ${cachepath(path, key)} | xargs mkdir -p") sh("mv ${path} ${cachepath(path, key)}") } } for(def path: paths) { sh("rm -rf ${fullpath(path)}; ln -s ${cachepath(path, key)} ${path}") } }
cache
関数では依存ファイルのパスを元にキーを生成しており、そのキーを持つキャッシュが存在する場合はsymlinkを貼るだけで処理を終わります。
これにより、Gemfileなどに変更がない場合はこれらの処理を合わせて10secちょっとで終わるようになりました。
なおキャッシュを利用するのはfalse positiveが怖いのですが、古いキャッシュはfalse negativeのほうが起こりやすいはずですし、定期的に削除して再生成させているため大きな問題はないだろうと思っています。
✅ 結果
これらの改善処理によって、全体で5min、オーバーヘッドは10sec程度まで短縮することができました🎉
あとは不要なspecを削ったり統合したり、あるいはfixtureを減らしたりcreate
からbuild
に変えたり、といった地道な改善を続けて少しずつ実行時間を削ることになります。
こういった地道な改善を進める上でも、ひとまずそれ以外の部分で実行時間を大幅に削れたのは効率が良かったと思います。
🎅 Misoca Advent Calendar 2017
最終日がだいぶギリギリになってしまいましたが、無事Misoca Advent Calendar 2017を完走することができました。
個人的には社長の記事が一番バズってるという事実に微妙な気持ちを抱いていますが、全員でいろいろな記事を書くことができて楽しかったです。
来年も気が向いたらアドベントカレンダーを立てたいなーと思います。
📣 宣伝
Misocaでは2018年のアドベントカレンダー記事を一緒に書く仲間を募集しています!!!