副題:CircleCIでdocker-composeを使うのをやめた。
1年前くらいには、CircleCIでdocker-composeを動かす方法についての記事を書いてた。
しかし、これが遅い。まぁ自分のやり方が悪いというのはあったのだけれど。ライブラリのキャッシュを1コンテナでやってから、次のジョブで複数コンテナに配布するようにしたほうが無駄がなくていいかなと思って、そうしていたのだけれど、docker-volume内にあるからキャッシュするためには一旦取り出さなければならないし、キャッシュを反映するにはdocker-volume内にインポートしなければならず、これがすごく遅かった。
最近ではresource_classがmedium、parallelismが4で、parallel_testsを使っても40分くらいかかっていた。遅すぎる。
長いことプロダクトバックログに積んでいたのだけれど、いい加減耐えられなくなってきたのと、チーム内でもテストの費用が上がっていて、下げる施策を探らなければならないという状態だったので、手をつけた。うちのプロジェクトは実験台にするにはちょうどいいのだ。
何をしたか?
やりたかったのは、スタディストさんのところのブログにあったやつ。
- CircleCIの消費クレジットとRSpecの実行時間を半減させるために行った9の手順 | スタディスト開発ブログ
- CIの本格化その2. 自動テストの実行時間10倍近く速くしたお話 | by ERDOS Balint | スタディスト開発ブログ | Medium
このブログにあった通りで、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に変更
落ちる原因が掴みきれずにいたのだが、ふと思い出して対応できた。
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.memory_lock: true 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をご存知ない方はこちらの過去記事をどうぞ。
まとめ
executorでmachineを使ってのdocker-composeを使ったテストはあまり速度が出なかったのだが、dockerに戻したらかなり速くなった。
また、resource_classをsmallにしてparallelismを2倍にしたほうがトータルでは高速化できた。前処理時間が全部のコンテナにかかるので、どこかで頭打ちになるとは思うが、大量にメモリを消費するような処理がない場合は、デフォルトのmediumからsmallに変えてparallelismを倍にするだけで高速化出来そう。
そして、CIにおいて高速化=節約に繋がる。40分かかっていたのが17分になったので、半分以上の高速化で、その分コストカット💵できた。CircleCIを使っているRailsプロジェクトであれば、resource_classを下げて並列数を増やすのが得策と思われる。早速社内でも横展開していく!