patorashのブログ

方向性はまだない

CircleCIでresource_classをsmall、parallelismを増やして高速化・節約

副題: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.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をご存知ない方はこちらの過去記事をどうぞ。

patorash.hatenablog.com

まとめ

executorでmachineを使ってのdocker-composeを使ったテストはあまり速度が出なかったのだが、dockerに戻したらかなり速くなった。

また、resource_classをsmallにしてparallelismを2倍にしたほうがトータルでは高速化できた。前処理時間が全部のコンテナにかかるので、どこかで頭打ちになるとは思うが、大量にメモリを消費するような処理がない場合は、デフォルトのmediumからsmallに変えてparallelismを倍にするだけで高速化出来そう。

そして、CIにおいて高速化=節約に繋がる。40分かかっていたのが17分になったので、半分以上の高速化で、その分コストカット💵できた。CircleCIを使っているRailsプロジェクトであれば、resource_classを下げて並列数を増やすのが得策と思われる。早速社内でも横展開していく!