CIの見直しをしたら処理の中身を変えなくても実行時間を60%以上削減できた!
atama plusのyutake27です。
CIジョブのパフォーマンスチューニングをせずとも、ジョブの実行タイミングや実行時の設定を変えただけで実行時間を60%以上削減できたのでやったことと学んだことを記録します。
要約
先に本記事で言いたいことをまとめると以下です。
- ジョブの実行タイミングを減らせないか考えよう
- ジョブを実行するブランチを絞る
- ジョブを定期ジョブにする
- ジョブの設定を見直そう
- 無駄な設定がないか確認する
- 並列数を変える
- ジョブを定期的に見直して改善できないか考えよう
はじめに
atama plusではAI教材「atama+」で生徒が学習する講義動画や演習問題などの学習コンテンツを自社で制作しており、それらのデータをGitリポジトリで管理しています。
そしてコンテンツの入力内容や設定にミスがないかをCI(CircleCI)を用いてチェックしています。
コンテンツ量が非常に多いため、1回のCIの実行に1時間程度時間がかかっていました。
そのためCI実行のコストが多くかかっていました。
また、リポジトリを普段使用しているコンテンツ制作担当の方々(非エンジニア)は実行時間が長く困っていました。
ただ以下の理由で改善に手が回っていませんでした。
- エンジニアが日々使うリポジトリではないので課題感が露見しづらい
- 昔からジョブが継ぎ足されてきたので処理内容に詳しい人がいない
- コンテンツの最終チェック時に確認するものなのでCI結果を確認する頻度はそこまで多くない
改善の機運
そのリポジトリで開発する機会がありました。
その際、CIの実行時間が遅く、CI待ちで開発をブロッキングをされ困っていました。
CIの待ち時間が長いと結果が出るまでに時間がかかり、修正までのループの時間が長かったり、別の作業とのコンテキストスイッチングが頻繁に発生したりと開発効率が悪かったです。
そこで実行時間を削減できないか調査してみました。
調査
ボトルネックの解消が高速化に一番効くので、まずどのジョブが遅いのかを調べました。
1番目に遅いジョブが55min程度、2番目に遅いジョブが40min程度かかっていました。他のジョブは最大20min程度だったのでこの2つのジョブがボトルネックであることがわかりました。
次にそれらのジョブがどのような処理をしているのか、コードを読んで調べてみました。
その結果1番目、2番目に遅いジョブは必ずしも全てのブランチで実行する必要がないことが判明しました。
対策
1. ジョブを実行するブランチをfeatureブランチのみに絞る
該当リポジトリのブランチ戦略は以下の図のような流れになっていました。
ブランチごとの役割
- feature
- 開発ブランチ
- dev
- 開発ブランチを取り込んでいくブランチ
- release candidate
- リリーステスト用ブランチ
- staging
- リリース前の最終確認用ブランチ。hotfix時はここで動作確認する。
- prod
- 本番環境
dev以降のブランチは直接pushできないように保護されています。
そのため、必ずfeatureブランチを経由するのでfeatureブランチでジョブを実行すればdev以降のブランチではCIジョブが実行されていることが保証されていました。
さらにボトルネックになっていたジョブはfeatureブランチで実行されていればその後のブランチでの成功が保証できるジョブでした(複数のブランチから同一ファイルに変更が加わっても各ブランチでジョブが成功していればマージ先ブランチでのジョブが必ず成功)
そこでfeatureブランチでのみジョブを実行し、dev以降のブランチではジョブの実行がされないよう設定しました(CicleCIのブランチフィルター機能を使用しました)
これによりdevブランチ以降では実行時間を55min→20minに削減できました(約60%削減)
2. featureブランチでのジョブの毎回実行をやめて月次ジョブにする
CIジョブの中で最も実行時間が長いジョブはたとえ処理が失敗していたとしてもリリースに影響するような重大なものではありませんでした。そのためfeatureブランチで毎回実行して結果を確認する必要はありませんでした。
そこでCIで毎回ジョブを実行するのではなく、CircleCIのスケジュール実行機能を使用して1ヶ月に1度実行するようにしました。
これによりfeatureブランチでも実行時間を55min→40minに削減できました(約27%削減)
3. ジョブの設定の見直し
ここまでである程度改善したものの、featureブランチでは40minかかっているジョブがボトルネックでした。
このジョブの各ステップの処理の実行時間を確認すると、リポジトリの準備に10minほどかかっていました。
このリポジトリではGit Large File Storage(LFS)というGitHubで大容量ファイルを扱えるようにする機能を使用しています。リポジトリのサイズが大きいためLFSを有効にしてクローンをするとかなりの時間がかかる状態でした。
ジョブの処理内容を確認するとLFSが無効な状態でも実行可能な処理でした。
そこでLFSを無効にしたところ40min→35minに改善しました。
さらにこのジョブはCircleCIの並列機能を使用して2並列で実行されていました。
単純に並列数をあげれば高速化できそうな処理内容だったので課金時間とのバランスを見て並列数を4に増やしました(並列数のチューニングはちゃんとしていないので今後の更なる改善として取り組みたいです)
これにより最終的に40min→20minに改善しました。
最終結果
最終的に全てのブランチで当初55minかかっていたCI実行時間を20min程度にできました(60%以上削減!)
一般的なCIと比べると遅いですが、以前と比べて開発時やリリース時の負荷を少し減らすことができ、CIコストの削減もできました。
さらに、普段リポジトリを使用しているコンテンツ制作担当の方々からも、CI実行時間の短縮による作業効率の向上に感謝の声をいただきました。
まとめ
以下の対処で大幅にCIの実行時間を短縮できました。
- ジョブを実行するブランチを絞る
- 毎回の実行が不要なジョブは定期ジョブにする
- ジョブの設定を見直す
ジョブが実行される機会を減らすことで実行時間を大幅に短縮できました。
1番の高速化はそのジョブを実行しないことなので、ジョブの実行頻度を減らしたりジョブを廃止できないか考えてみると良いです。
ジョブの実行を廃止できるケースは少ないと思いますが、今回はたまたまボトルネックのジョブでその選択肢が取れたのでラッキーでした。
CIの処理は、時間が経つにつれて次のような問題が発生することがあります。
- 処理対象が増えることで、ジョブの実行時間が長くなる。
- 以前は必要だったが、今では不要になったジョブや処理が含まれている。
そのため、定期的にCIジョブを見直し、改善の余地がないかを考えることが重要です。これにより、無駄な処理を減らし、実行時間を短縮できます。
また、処理内容に詳しくなかったとしても臆さず調査していくことで改善につながることもあると学びました。
みなさんのCIも長年見直されていないジョブがあるかもしれないので、一度設定や見直してみると実行時間の削減ができるかもしれません!
p.s.
この記事を書いているうちにまた少しずつCIの実行時間が伸びてきたので改善を考えていきたいと思います。
CircleCIのpath-filteringを使うと変更されたファイルに基づいてジョブの実行が可能なようです。
またNxというPull Requestによって影響があったタスクのみを実行するツールがあるそうです。
これらを使用して、PRごとに実行されるジョブを絞り実行時間を削減できないか検討中です。
atama plus 株式会社では一緒に働く仲間を募集しています!
Discussion