副題:CircleCIでdocker-composeを使うのをやめた。
1年前くらいには、CircleCIでdocker-composeを動かす方法についての記事を書いてた。
patorash.hatenablog.com
しかし、これが遅い。まぁ自分のやり方が悪いというのはあったのだけれど。ライブラリのキャッシュを1コンテナでやってから、次のジョブで複数コンテナに配布するようにしたほうが無駄がなくていいかなと思って、そうしていたのだけれど、docker-volume内にあるからキャッシュするためには一旦取り出さなければならないし、キャッシュを反映するにはdocker-volume内にインポートしなければならず、これがすごく遅かった。
最近ではresource_classがmedium、parallelismが4で、parallel_testsを使っても40分くらいかかっていた。遅すぎる。
長いことプロダクトバックログに積んでいたのだけれど、いい加減耐えられなくなってきたのと、チーム内でもテストの費用が上がっていて、下げる施策を探らなければならないという状態だったので、手をつけた。うちのプロジェクトは実験台にするにはちょうどいいのだ。
何をしたか?
やりたかったのは、スタディストさんのところのブログにあったやつ。
このブログにあった通りで、docker-composeを使う限りは速くならない(金をかければ多少速くなるけれど、コストと見合わなさそう)。
executorをdockerに変更
CircleCIのexecutorをmachineからdockerに変えるところからやった。
開発ではdocker-composeを使い続けたいが、CIではやめたいので、ENV['CI']
があればという条件を加えていく。
例えば、Elasticsearchの接続条件をいじる場合はこんな感じ。
Elasticsearch::Model.client = case
when Rails.env.development?
Elasticsearch::Client.new(host: 'elasticsearch:9200/', log: true)
when Rails.env.test?
if ENV['CI']
Elasticsearch::Client.new(host: 'localhost:9200/')
else
Elasticsearch::Client.new(host: 'elasticsearch:9200/')
end
else
raise 'SEARCHBOX_URL not found.' unless ENV['SEARCHBOX_URL']
Elasticsearch::Client.new(host: ENV['SEARCHBOX_URL'], http: { port: 443, scheme: 'https' })
end
似たような感じで、どんどんif ENC['CI']
を付けていったら、テストは動くようにはなったが、Elasticsearchが落ちるようになった。
落ちているElasticsearchを動かす
Elasticsearchはエラーコード137を出した落ちてた。つまりはOut Of Memoryなので、ES_JAVA_OPTS: -Xms256m -Xmx256m
とか付けてみたり、増やしてみたりもしたが、どうにも不安定。machineの頃はCPU 2つ、メモリ7.5GBだったが、dockerになるとCPU 2つ、メモリ4GBになってるのを思い出した(デフォルトのmediumの場合)。
Configuring CircleCI - CircleCI
parallel_testsの並列数を4にしているのがマズいのかも…と思い、2に減らしたところ、Elasticsearchは安定して動くようになった。この時点でまだ落ちるテストはあったものの、24分程度に終わるようになった。
assets:precompileの結果をキャッシュ
webpackerを使っていると、env RAILS_ENV=test bin/rails webpacker:compile
をしてからでないとテストが実行できなかったのだが、こいつが遅い。2分くらいかかる。事前準備のジョブの時点で、assets:precompile
を行うようにして、./public/pack-test
ディレクトリをキャッシュするようにした。しかしJSファイルやライブラリの更新などがあった場合はキャッシュを破棄したいので、JS系ファイルのハッシュ値を集めたテキストファイルのハッシュ値を使うようにした。
やり方に関しては、この記事を参考にさせてもらった。
md5sum でディレクトリ単位のチェックサム計算等 - clock-up-blog
これをCircleCIのコマンドにしたら、こんな感じ。
commands:
restore_packs_test:
steps:
- run:
name: JavaScript Checksum
command: |
find app/javascript -type f -exec md5sum {} \; | sort -k 2 > javascript_checksums.txt
find config/webpack -type f -exec md5sum {} \; | sort -k 2 >> javascript_checksums.txt
md5sum package.json >> javascript_checksums.txt
md5sum yarn.lock >> javascript_checksums.txt
md5sum postcss.config.js >> javascript_checksums.txt
md5sum babel.config.js >> javascript_checksums.txt
md5sum .browserslistrc >> javascript_checksums.txt
md5sum config/webpacker.yml >> javascript_checksums.txt
- run:
name: cat javascript_checksums.txt
command: |
cat javascript_checksums.txt
- restore_cache:
name: Restore ./public/packs-test
key: packs_test-{{ arch }}-v{{ .Environment.YARN_CACHE_KEY }}-{{ checksum "javascript_checksums.txt" }}
save_packs_test:
steps:
- save_cache:
key: packs_test-{{ arch }}-v{{ .Environment.YARN_CACHE_KEY }}-{{ checksum "javascript_checksums.txt" }}
paths:
- ./public/packs-test
when: always
jobs:
generate_cache:
executor: default
parallelism: 1
steps:
- restore_packs_test
- run:
name: assets:precompile
command: |
if [ ! -d ./public/packs-test ]; then
bin/rails assets:precompile
fi
- save_packs_test
これにより、JS系の変更がない場合はテストが2分近く短縮されるようになった。この時点で22分(とはいえ、テストはまだ落ちてたので本来のスピードではない)
resource_classをsmallに変更
落ちる原因が掴みきれずにいたのだが、ふと思い出して対応できた。
patorash.hatenablog.com
docker-composeの頃はdocker imageの時点で対応済みだったが、今はそうではないので、対応する処理を追加したらテストも通るようになった。
最初にこれの変更の時間を測っておけばよかったのだが、なかなか気づかずにとりあえずresource_classをsmallにするのをやりたかったので先にやってしまっていた。
resource_classをsmallにすると、CPUが1つ、メモリが2GBになる。しかし、使用クレジットは5になる(mediumは10)。
最初のほうで参考にしたスタディストのブログにもあったけれど、Railsのテストは大体I/Oが遅いので、マシンパワーが貧弱でも台数が多い方が速度が上がりそうだなと思っていた。
そこで、resource_classをsmallにして、parallelismを4から倍の8に増やした。これで、1分あたりのクレジット使用量は変わらない。
Elasticsearchが落ちる
するとまたElasticsearchが落ち始めた…。メモリが2GBになったせいか…。parallel_testsの並列数を1にしてみたら、安定した。しかし、もうそれはparallelではない!でも一応parallel_tests経由でテストを実行したところ、全部通った(他にもちょこちょこ直してはいたが)。これで、22分だった。全部通るようにはなったけれど、速度はあんまり変わらず。
あと、色々とelasticsearchのイメージの環境変数を設定していたので、それを晒しておく。不要な物もあるかもしれない…。
- image: patorash/elasticsearch-kuromoji:7.9.1
environment:
node.name: es01
cluster.name: es-docker-cluster
bootstrap.system_call_filter: false
ES_JAVA_OPTS: -Xms256m -Xmx256m
TZ: /usr/share/zoneinfo/Asia/Tokyo
transport.host: localhost
network.host: 127.0.0.1
http.port: 9200
xpack.security.enabled: false
discovery.type: single-node
mem_limit: 256m
memswap_limit: 1g
parallel_testsをやめる
parallel_testsの並列数を1にしてしまったので、もう並列じゃないし、外そうと思って、直接knapsack_pro経由でrspecを呼び出すように修正した。
parallel_testsを起動するオーバーヘッドがなくなる分、多少は速くなるだろうけれど、まぁ誤差の範囲だろうなぁとタカを括っていたら、かなり速くなった。17分!🚀
knapsack_proをやめてCircleCIのsplit-by=timingに戻したら、knapsack_proの代金を浮かせることができるから、久々にtimingに戻してみるかーと思って実験してみたけれど、案の定、22分〜24分かかるようになってしまったので、knapsack_proを使うように戻した。knapsack_proよくできてんな…😇😇😇
knapsack_proをご存知ない方はこちらの過去記事をどうぞ。
patorash.hatenablog.com
まとめ
executorでmachineを使ってのdocker-composeを使ったテストはあまり速度が出なかったのだが、dockerに戻したらかなり速くなった。
また、resource_classをsmallにしてparallelismを2倍にしたほうがトータルでは高速化できた。前処理時間が全部のコンテナにかかるので、どこかで頭打ちになるとは思うが、大量にメモリを消費するような処理がない場合は、デフォルトのmediumからsmallに変えてparallelismを倍にするだけで高速化出来そう。
そして、CIにおいて高速化=節約に繋がる。40分かかっていたのが17分になったので、半分以上の高速化で、その分コストカット💵できた。CircleCIを使っているRailsプロジェクトであれば、resource_classを下げて並列数を増やすのが得策と思われる。早速社内でも横展開していく!