見出し画像

君はCircleCIを使い倒しているか!CircleCIの実行時間を半分にした話

こんにちは
noteでArchitectureチームに所属しています、GENDOSUです。

CircleCI遅い!!(突然)

というのも
noteでは今までnoteではCircleCIが遅いという課題があり問題があって、CircleCIが遅くなっていました。しかし、〇〇したことで、解決することができました。今回はその方法について書いていきたいと思います。

noteの構成

まず、単に遅いと言っても改善できるのかどうか調べないといけないので
現状と環境の把握をしていきます。

簡単にnoteの構成を説明すると以下のようになります。

  • フロントエンド

    • Vue / Nuxt

    • React / Next(一部)

  • サーバサイド

    • Ruby on Rails

    • テストRSpec

  • CIツール

    • CircleCI

CircleCIはサーバサイドに限らずテスト自動化の部分で使われてます。

今回「遅い!」と言っているのはサーバサイドが対象です。

CircleCIが遅い原因の調査

CircleCIの実行時間を表した図
青い部分がRSpecの実行時間

では、ネックになっている部分の調査です。

noteのサーバサイドは現在モノリシックな構成で
全体のテストを実行すると
CircleCIのランナー6本並列で実行して14分ほどかかっているのが図から分かると思います。

CPU usageを見ると、実行時間の割にはCPUがほぼ1/3しか使われていないことが分かる

CircleCIではテストを実行するインスタンスをランナーと呼んでおり
このランナーのスペックは設定ファイルの中で指定することが出来ます。
noteで現在使っているスペックはmedium+(CPU: 3、メモリ: 8GBのインスタンス)を指定しています。

しかし、CPUは3つ使えるはずなのに、CPU usageのグラフを見ると1/3だけしか使われていないことが分かります。

実際、RSpecはそのまま起動すると1プロセスでの実行になるので、ほぼ解釈として間違えていないはず。

CPUの使用率100%を実現するためにランナー内で並列処理をさせる

CPU usageの理想の形

で、理想は・・・

現在のCPUが1/3しか使われていない状態から、CPUの使用率100%という状態に持って行きたい。

そこで・・・

ランナーの中で並列処理をさせようと考えました。

noteのCircleCIのランナーを再確認

実装をする前に、noteのCircleCIのランナーを再確認
  • CircleCIのランナーを6本立てて並列実行

  • ランナーの中でRSpecテストが1本動いている

  • 合計6本の並列性

これを以下のようにカイゼンしてみます。

そして、これから実装するランナーの理想がこちら

CircleCIのランナーを6本立てて並列実行(ここは変わらず)

  • CircleCIのランナーを6本立てる

  • ランナーの中でコア数分テストを並列実行

  • 合計24本の並列性

CircleCIで並列化するための実装の仕様を確認します。

  • 並列で動かすということで、データベースも並列数分必要になり、実行結果も並列数分作成されます。

  • CIrcleCIから見た場合、現状と同等のテスト結果が受け取れる必要があります。

並列処理の実装をしてみる

ということで、実際にやってみる。

parallel_testを使用

並列実行には、parallel_testsを使用
これは、コア数分RSpecをパラレルに実行してくれるものです。

DBの並列化

データベースを並列数分作成するのは
parallel_testsのコマンドで実行出来る
並列のそれぞれで、違うデータベースを参照するのは、parallel_testsが実行時環境変数を作ってくれていて個々のプロセス事にプロセス番号が入るので
それをもとにテーブルを切り替える

出力ファイルの並列化

実行結果のファイルもデータベース同様環境変数を使ってプロセス事に分けて出力するようにする

上記を実行した結果

どれだけ早くなった?

左が実装前:14分17秒、右が実装後:6分38秒

並列処理を疾走した結果、実行時間が14分から6分と、半分以下になりました。

計算上はもっと早くなるはず・・

倍速にはなったのですが、もう少しはやくなる想定でした。

並列性が4倍なら1/4になるのでは??

単純計算だと、 1/4になるのですが、ランナーの中では・・・

  • 実行イメージの取得&展開

  • キャッシュ展開

  • テーブル作成

などなど、これらの実行は並列になりません。
加えて、ランナーのIOなどの性能の上限というのも考慮しないといけないので、単純に1/4にはなりにくいです。

改修後のCPU利用率
100%使われていることが分かる

並列実行は出来たが
性能すべてを使い切れてはいない
が、それでも一番
時間がかかるフェーズが
並列実行出来ているので
よしとする。

やったことおさらい

  • CircleCIのランナーが6並列なのをここの設定はいじらず

    • ランナーの中をさらに並列にする

  • データベースを並列数分作成

  • CircleCIのランナーのリソースタイプをmedium+からlargeに変更

    • プロセスが増える分メモリが消費されるため

  • その他、最適化した内容

    • Dockerイメージにあらかじめ必要なパッケージをインストール

    • apt install [パッケージ]

    • Dockerイメージはus-east-1に配置

    • プロジェクトのチェックアウトで出来る.gitディレクトリをキャッシュ

    • assets:precompileはRSpec実行の前に事前に実行

コスト計算(単純計算)

resource_typeを一つあげた割にはコスト削減になっている(はず)

CircleCIの速度改善に取り組んでみて

今回、CircleCIの速度改善という対応をして見て
CircleCIのドキュメントにある通りのparallelismという並列化をする設定だけでは無く、ランナーのスペックに応じた対応をすることで、飛躍的に高速化が出来るということが分かりました。

また、CircleCIによるテスト実行が高速化されることで
DevOps のトップレベルパフォーマンス指標といわれる4つの指標の中の3つ

  • リードタイム

  • デプロイの頻度

  • 平均修復時間

が改善されそうです。
(DevOpsに関する指標についてはまだ取れていないので今後の課題です。)

最後に

noteではいろんな人を募集してます。興味ある方はぜひぜひ来てください!


この記事が気に入ったらサポートをしてみませんか?