🏃‍♂️

CI の実行時間を大幅に削減した話

2024/05/31に公開

Thinkings 株式会社では、sonar ATS の開発で CI/CD ツールに Jenkins を使用しています。ビルドやテスト自動化などは行っていましたが、特に継続的インテグレーション(CI)について改善に手が回っておらず、開発速度のボトルネックのひとつになっていました。今回は、CI 実行時間を大幅に削減した際にやったことについてお話します。

背景

sonar ATS は バックエンドに .NET Framework/C# を使用して開発しています。何年か前まで古い歴史を持つテストコードが存在していましたが、テスト自動化は実現できていませんでした。まずは、テスト自動化がない状態からある状態にすることを目標にして、頻繁に変更されるリポジトリを対象に、Jenkins を使って CI を導入しました。この段階では CI の実行速度は考慮せず、テスト自動化の導入に専念しました。CI の導入が達成できたので、次の段階としてテスト自動化の範囲を広げ、CI 実行時間を短縮する必要がありました。

私はたまたま過去に CI/CD の導入を経験したことがあったので、上司と「じゃあ、やってみますか~」ということで着手しました。

目的

テスト自動化の範囲を広げることと CI 実行時間を短縮することの目的は主に次の2つです。

  1. 品質を可視化する範囲を広げたい
  2. CI 実行結果のフィードバックを速くしたい

1.は、製品をリリースるるために CI を使ってテスト自動化を行い、最新の製品コードがどういった品質か確認する必要があります。ビルドエラーやテストが失敗している場合、製品をリリースできる状態にはないと判断できます。

2.は、開発速度を向上させることがねらいです。CI の実行時間が短いことで時間あたりの CI 実行回数が増えて、フィードバックの速度が向上します。結果、テストが失敗していたり、ビルドエラーに速く気がつくことで修正にかかるコストを削減します。

やったこと

背景と目的を説明したところで、実際にやったことについて説明します。大きく分けると「計測」と「対処」の2ステップです。

  1. 計測: CI 実行時間を計測する仕組みを導入した
  2. 対処: ボトルネックになる箇所を調査し、対処した

実行時間を計測する

実際に着手するうえで、まず計測が必要と考えました。実際に対処したとしても、最終的にどのくらい効果があったのかを知る必要があったためです。計測するにあたって、現状の製品の開発状況を確認しました。その結果、一定期間内に変更がある8個のリポジトリを今回の実施範囲対象としました。変更頻度が一定あることで、品質を可視化するメリットが高いと考えたためです。

まずは、これら8リポジトリに対して Jenkins でビルドできる状態にしました。もともと、ビルド可能なプロジェクトはあったので、ここは苦労せずにできました。

実行時間の計測には Jenkins の REST API を使って、ビルド履歴から実行時間のログを出力するようにしました。出力したログを蓄積して、BI ツールで集計しています。すべてのテストプロジェクトが終了したときにログを蓄積することで、継続的に CI 実行時間の分析ができるようになりました。


CI 実行時間の集計を表示するダッシュボード

計測の結果、この時点で 8リポジトリの CI 実行時間の合計は3,855秒(≒64分) でした。最も時間のかかるもので約1,200秒(=20分)という状態でした。

CI まわりの状況を把握する

実行時間が計測できたので、次にどこにボトルネックがあるのか確認しました。まずは、CI を実行するための環境と内容を把握した結果、次のようなことがわかりました。

  1. CI も CD もひとつのエージェントで実行している
    • エージェントでは HDD を使用している
  2. CI で実行しているのは次のようなこと
    • Git から最新コミットを取得する
    • NuGet パッケージの復元
    • ソリューションのビルド
    • テスト実行
    • 実行結果の通知

Jenkins では最初に master と呼ばれるエージェント環境を使って Jenkins 本体とプロジェクト(ジョブ)を実行します。当時は CI も CD も master で実行されており、実行タイミングが被ってしまうとボトルネックになる可能性がありました。また、master はストレージが HDD を使用していたため、これを SSD に変えるなどで速度向上が期待できそうでした。

CI で実行しているタスクは基本的なものだけでした。それぞれの実行時間を確認すると、特に時間がかかっているのは「ソリューションのビルド」と「テスト実行」でした。優先度としては、これら2つのタスクを改善することで、より実行時間の短縮が期待できると考えました。中でも着目したのは以下の2つです。

  1. テスティングフレームワークが古い
  2. テストプロジェクトが複数ある

1.のテスティングフレームワークが古いことにより、テスト実行時間のボトルネックになっている可能性があると考えました。2.テストプロジェクトが複数あることにより、余計にビルド時間がかかっている可能性があると考えました。他にもテストケースを修正することで改善する見込みはありましたが、どのくらい時間をかけるとどのくらい効果が出るのか全く予想できなかったため、一旦後回しにしました。

エージェントの移行

ストレージが HDD で Jenkins 本体と CD が実行されるため、別のエージェントを作成してストレージを SSD にすることで速度向上が期待できそうです。

エージェントは Azure Virtual Machine(VM)を利用していたので、VM の作成と環境構築、各種設定を実施しました。この時、選択したのは Standard SSD です。Premium SSD だと目的に対してコストが高くなる可能性があったためです。CPU やメモリ量についてはランニングコストが発生するため、上司と相談しながら「このあたりならコストをかけても問題ない」というボーダーを決め、それを超えない範囲で設定しました。

テスティングフレームワークの移行

この時点で使用していたテスティングフレームワークは MSTestV1 でした。MSTest は .NET Framework の主要なテスティングフレームワークのひとつです。MSTest は V1 と後継の V2 があり、sonar ATS では V1 を使用していました。歴史が古いこともあり、開発当初はこれでも良かったのですが、現代では実行速度のボトルネックになってしまっていました。

というのも、MSTestV1 はシングルスレッドでしか実行できませんでした。CPU の性能を活かすにはマルチスレッドでテストを実行する必要があります。CPU リソースを効率よく利用することで CI 実行時間を短縮できるため、xUnit.netを導入することにしました。

xUnit.net は .NET や .NET Framework のテスティングフレームワークとしては、ほぼデファクトスタンダードになっているのと、MSTest と大きく書き味が変わらないこと、MSTestV1 ではできなかったパラメタライズドテストができることからこちらを選択しました。

テストプロジェクトを集約

いくつかのリポジトリでは、テストコードが書かれているテストプロジェクトが複数ありました。.NET Framework を使用したアプリケーション開発では複数のプロジェクトを作成するのは珍しくありません。過去の経緯までは追うことができなかったのですが、事実として製品コードをまとめたプロジェクトに対して、1対1でテストプロジェクトがありました。

これだと各プロジェクトをビルドする必要があるため、CI 実行時間が長くなる可能性がありました。他にも、新規プロジェクトが追加された際にテストプロジェクトの追加漏れや、CI でテスト実行対象に含めるのを忘れてしまう可能性もあったため、これらのテストプロジェクトをひとつに集約しました。

テストプロジェクトがひとつに集約されることで、テストケースをどのプロジェクトに書けばよいかは明白になりますし、ビルドするプロジェクトも減るので CI 実行時間の短縮が期待できます。

効果

8リポジトリの CI 実行時間の合計が 3,855秒(≒64分)が1,634秒(≒27分)まで短縮できました。
特にもともと CI 実行時間の長いリポジトリほど効果が高く、全体の半分近くのリポジトリが実行時間を60%程度削減できました。最も実行時間が短縮できたもので1,136秒 → 488秒という結果でした。

それぞれ対策ごとの変化は次の通りです。

  • エージェントの移行: 3,855秒 → 2,162秒(≒36分)
  • テスティングフレームワークの移行 + テストプロジェクトを集約: 2,162秒 → 1,634秒(≒27分)

まとめ

ビルド時間は、最終的に半分未満にまで短縮することができました。

やっていることは実行時間の計測とボトルネックの分析、その対処という基本的なことだけでしたが、大きく改善できたので安心しました。とはいえ、やってみるとまだまだ速度改善の余地があることが分かったので、継続して監視と改善を実施していきたいです。

Thinkingsテックブログ

Discussion