さようならrrrspec、いままで速度をありがとう

Misoca開発チームの黒曜(@kokuyouwind)です。

今回の記事タイトルは銀河ヒッチハイクガイドからの引用です。*1

その水棲動物を見よ。

🤖 MisocaのCIとrrrspec

MisocaではCIが短時間で終わるよう、rrrspecを利用してRSpecを分散実行していました。関連記事もいくつか書いています。

tech.misoca.jp

tech.misoca.jp

自分の入社時に20分ほどかかっていたビルドが5分ほどで収まるようになり、またメンバーが増えても並列性を担保できたのはrrrspecの力による部分が大きいといえるでしょう。

…が、残念ながら昨年2月にrrrspecのActive Maintenanceが終了してしまいました

その後もMisoca社内でforkしたリポジトリを保守しながら運用していたのですが、次第に以下のような問題が顕在化してきました。

  • RubyやGemのバージョンアップにどこまで追従できるかわからない
  • 問題が発生したときに、関与するミドルウェアが多く調査に時間がかかる
  • rrrspecの仕組みに詳しいメンバーが少なく、保守や問題対応でそのメンバーに負荷が集中する

上記にはActive Maintenance終了と直接関係ないものもありますが、いずれにせよこのままrrrspecを使い続けるのは厳しいだろうと判断し、仕組みを載せ替えることになりました。

🧭 載せ替え先の検討

結構な力技でrspecを回していたため、載せ替え先でもある程度並列実行できるものにしないと現実的な時間で終わらないということはわかっていました。

このため以下のようにいくつかの案を作り、それぞれの速度や運用コストを見積もった上でどれにするかを判断しました。

  • 並列実行をサポートしているCircleCIに載せ替える
  • knapsackを使ってテストスイートを分割し、Jenkinsfileのparallelでノード分散実行する
  • parallel_testsを使い、スペックの高いJenkins Slaveノード1台の内部で並列実行する

それぞれを試験実装してみたところ、CircleCIは運用費用が現状から大きく跳ね上がってしまうこと、knapsackでの分散はparallel_testsに比べてノードの性能を引き出しづらいことがわかりました。

またCircleCIに載せ替える場合は現状のJenkins + parallel_testsから一本化する必要があり乖離が大きいこと、他のJenkins Jobが残っているためJenkins自体は止められずCI環境が複数になること、などの懸念もありました。

これらの調査結果を踏まえて、実行速度と運用コストのいずれも許容範囲内であったparallel_testsへ載せ替える方針としました。

🏎 parallel_testsへの載せ替え

rrrspecからparallel_testsに載せ替えるにあたり、もともと使っているJenkins Slaveではspec実行環境が整っていないという問題がありました。

頑張ってMySQLなどの依存ミドルウェアを立ち上げ、Rubyもバージョンアップに追従させて… というのはしんどそうだったため、Dockerを使ってspecを実行することにしました。

細かいところを端折ったJenkinsfileは以下のようなものです。

node('parallel_worker) {
  withEnv([
    "COMPOSE_FILE=docker-compose.test.yml"
  ]) {
    stage('checkout') {
      checkout(scm)
    }
    stage('build') {
      sh 'docker-compose build'
    }
    stage('test') {
      sh 'docker-compose run --rm app rake parallel:rake[db:reset,18] parallel:spec[18]'
    }
  }
}

Pipeline組み込みのDocker文法を使う方法もありましたがサイドカーコンテナの扱いがややこしそうだったため、単純にdocker-composeを使うだけにとどめています。

また18並列という数字は並列数を変えながら検証して得られた数字で、これ以上に並列数を上げてもほとんど実行時間が変化しませんでした。

spec実行にはc5.12xlargeインスタンスを利用しています。このレベルのインスタンスだと1台でも結構なコストなので、JenkinsのAmazon EC2 Pluginを利用して必要なときだけspotインスタンスが立ち上がるようにしました。

🏁 結果

さすがにDocker立ち上げやRails起動などのオーバーヘッドが大きく5分には収まりませんでしたが、6分ほどでrspecの実行が終わるようになりました。

f:id:kokuyouwind:20200317190135p:plain

🛠工夫が必要だったところ

作業中にいろいろと罠を踏んだり工夫したことがあるのですが、それぞれ細かく書くと長くなるのでざっくり箇条書きしておきます。

  • parallel_testsのparallel_runtime_rspec.logはJenkins Artifactに格納して、次以降のジョブでcopyArtifactを使って引っ張ってくるようにしました
  • Docker内で出力するファイルのオーナーがホストとずれてしまい消せなくなってしまったため、user namespaceを使って揃えるようにしました
  • EC2 Pluginremoting.jarに実行権限が付与されておらずslave立ち上げに失敗していたため、Slave command prefixchmod a+rx /tmp/remoting.jarを挟んでいます
  • 同じくEC2 Pluginで、Ubuntuを使う場合はRemote userubuntuの指定が必要でした。Usageに書いてありますが、ちょろっとしか書いてないので見落としやすいです

💬 感想

今回の置き換えでCIにかかる時間は少し長くなってしまいましたが、以下のように得られたメリットも多くありました。

  • rrrspecクラスタがなくなったことで管理する対象が格段に減った
  • Jenkinsローカルでテストを実行することで、失敗時スクリーンショットなどの収集を簡単に行えるようになった
  • Dockerベースにしたことで環境の切り替えが楽になり、レイヤーキャッシュが効くため複雑な自前キャッシュを減らせた

rspec実行に時間がかかるのも、テストが多すぎる・重すぎるという問題の比重のほうが多いため、ここからは仕組みの改善ではなく既存テストの棚卸しといったことが必要になってくるのではないかと思っています。

最後に、rrrspecには導入してからの3年半ほど、MisocaのCIを支えていただきました。コミッターの皆様、これまでありがとうございました。

📢 宣伝

Misocaでは「生命、宇宙、そして万物についての究極の疑問の答え」を探求するエンジニアを募集しています。

あとCIの仕組みを改善したいエンジニアも募集しています。

*1:前回タイトルはRe:ゼロ引用でしたが1ミリも伝わらなかったので、今回は自ら言っていくことにしました