RailsアプリケーションのCIを爆速にしたくいろいろやってみたメモ。
前提など
- 概ね checkout - build - test で15分くらい。
- 使っているのはCircleCI
カバレッジ収集をしない
各ブランチでテストカバレッジを収集していたのを止めた。別にPR上に通知するわけでもなく、活用しているわけでもなく、新しく見えるようになったところで特に効果がない、と判断したため。とはいえ、メインブランチでの収集は引き続きしている。これは全体の動向を掴むため。 実際にはSimpleCovを使ってカバレッジを収集していたところ、CI上でメインブランチのときだけ有効になる環境変数を作って、それ以外のブランチでのカバレッジ収集を止めた。
こういう感じ。
when:
condition:
or:
- equal: [ main, << pipeline.git.branch >> ]
- equal: [ foo-bar-analyze, << pipeline.git.branch >> ] # 別途必要だったブランチ
steps:
- run:
name: Set COVERAGE="1"
command: echo "export COVERAGE=true" >> ~/.bashrc
結果: 1-2分程度速くなった(メインブランチは変わらず)
Shallow cloneを使う
Shallow Repository Cloning – CircleCI Support Center
結構歴史が溜まったリポジトリだったため、常に数十秒かかっていた。
結果: 数十秒速くなった(checkoutが1秒まで短縮した)
assetsのgzip圧縮をやめる
config/environments/test.rb にこんなものを書く。
if ENV['CI']
config.assets.compile = false
config.assets.gzip = false
end
なんだかんだアセットの量がありここに時間がかかっていた。
結果: 数十秒速くなった
assets:precompileをキャッシュしてスキップする
単なるキャッシュではなく、assetsなファイルの更新があるときだけprecompileをするようにした。
- restore_cache:
keys:
- asset-cache-v1-{{ .Environment.CIRCLE_BRANCH }}-{{ checksum "ASSETS_REVISIONS" }}
- asset-cache-v1
- run:
command: git log --oneline -n 1 app/assets vendor/assets Gemfile.lock | awk '{{print $1}}') > ASSETS_REVISION
- run:
command: |
current_revision=ASSETS_REVISION
previous_revision=public/assets/ASSETS_REVISION
if [ ! -e $previous_revision ] || ! diff $previous_revision $current_revision; then
source ~/.bashrc && bundle exec rake assets:precompile assets:clean[1]
cp -f $current_revision $previous_revision
else
echo "Skipped."
fi
- save_cache:
key: asset-cache-v1-{{ .Environment.CIRCLE_BRANCH }}-{{ checksum "ASSETS_REVISION" }}
paths:
- ./public/assets
- ./tmp/cache/assets
- when:
condition:
equal: [ main, << pipeline.git.branch >> ]
steps:
- save_cache:
key: asset-cache-v1
paths:
- ./public/assets
- ./tmp/cache/assets
結果: 1分速くなった(assets更新がない場合)
parallelismを調節
通常、rspecは順番にテストを実行して1コアしか使わないので、あんまり高スペックなマシンは不要。1コアなマシンをたくさん並べたほうが速い。この改善をしたプロジェクトではまあまあメモリが必要だったので、Mediumで。
もともと parallelism: 24 だったところ、 20,30,40,50 と変えて試した。結果、40,50にしても、ランダムフェイルするテストが一定あり、失敗時にリトライしているのでトータルの時間はあまりよくならなかった。いまのところ30がバランスよく終わりそうなのでこの値にした。
結果: 1-2分速くなった
ランダムテストを改善する
Circle CI Insightからランダムに失敗しているテスト(Flaky test)が確認できるので、これを見て、失敗したテストを一個ずつ確認した。
[A,B,C].sample
のようなランダムに値を決める箇所があり、これは決め打ちできるような仕掛けを入れた。let(:aaa) {}
の遅延評価のタイミングなのか、データ順がおかしいケースがあり、let!
を使うようにした。(これはsortやorderをつけるほうが正しいかもしれない) 他にもいくつか…
これ自体はCIの速度改善にはあまりつながっていないが、ランダムで失敗する確率が下がれば下がるほど前述の並列数を上げて速度アップできそうな気がしている。
不要なコンテナを立ち上げない
テスト時にmysqlやモック用のサーバが必要だが、なぜかcheckout時にも立ち上がるようになっていた。これを整理し、各ステップで必要なコンテナだけを立ち上げるようにした。
結果: 数十秒の改善
persist_workspaceに不要なファイルを入れない
checkoutからtestに移る際、persist_workspaceを使って解決された依存関係やコンパイル結果を含むリポジトリ全体のファイルを渡していた。
例えばvendor/bundle以下には spec
なファイルや *.gem
なファイルがあり、これらはテスト時には不要だった。これらのファイルを削除した後、persist_workspaceを呼び出すことで、ファイルサイズが削減され、処理時間が短くなった。
結果: 数十秒の改善
bundle install で –deployment を使う
Bundler: bundle install Railsのbundle install –deploymentとは何なのか|TechRacho by BPS株式会社
公式で使えとあったので使った。速度という面では変化はあまり見られなかった。
一旦の区切り
ここまでやって10分を切るか切らないか、くらいまで改善された。まだまだできそうなところはあるが、一旦ここで区切りをつける。
他にもできそうなところ
- テストの分割・実行をもっといい感じにする
- Speed up your tests with optimal test suite parallelisation
- skroutz/rspecq: Distribute and run RSpec suites among parallel workers; for faster CI builds
circleci tests split --split-by=timings
を現状使っているが、もっといい感じにするツールもある。導入コストはややかかるが…- 最遅ケースの量が減って、中央ケースや最短ケースに近い時間での完了件数が大きく増えるだろうというイメージがある
- CPUをフル稼働させる
- Mediumだと2コアあり、1コアしか使わないrspecでは少し勿体ない。フル稼働できれば単純計算で2倍速
- 単純に
&
でrspecを並列起動すればいいが、それではDBの変更が相互に影響しあってテストがうまく行かない気がする - いまはアイデアなし
- 遅いテストを速くする
- 例えば
101件のデータがあるとき ページ2が存在する
みたいなテスト - 101件データを作らなくても、ページングの基準を定数にし、stub_const で上書きすれば、最低で2件のデータを用意するだけで十分になる
- 例えば