inFablic | Fablic, inc. Developer's Blog.

フリマアプリ フリル (FRIL) を運営する Fablic の公式開発者ブログです。Fablic のデザイナー・エンジニア・ディレクターが情報発信していきます。

フリルサーバーサイドのCI環境の現状と改善のための取り組み

f:id:kazu9su:20170914234657p:plain

こんにちは、サーバーサイドエンジニアのtommyです。

この記事では、フリルサーバーサイドのCI環境の現状と改善のための取り組みについて紹介したいと思います。

フリルサーバーサイドアプリケーション構成

フリルは5年以上運用されているサービスであり、開発者も年々増加していることから、
サーバーサイドのアプリケーションは一定の機能(役割)ごとにリポジトリを分けて構成しています。
日々活発に開発が行われているサーバーサイドのリポジトリ数は7つあります。
CIにはCircleCIを利用しており、私が入社した6月時点では、organization全体で4コンテナで運用していました。 

CIへの課題感

CIへの課題としては、やはり実行時間の増加が大部分を占めていました。
特にテストケースが多く、開発の活発なリポジトリでは実行に40分程度を要するものもあり、
エンジニアの開発効率に無視できない影響を及ぼしていました。
この問題へのアプローチとして、

  • テストの並列実行可能な環境の構築(並列化)
  • テストのリファクタリング(高速化)

などが考えられました。
長期的には、リファクタリングによる高速化も必須ではありましたが、即効性と今後のCI環境のベースを構築するという観点から、
テストの並列実行可能な環境の構築(以下テストの並列化と呼ぶ)に取り組みました。

詳細な前提条件

改善施策に取り組む前の前提条件としては、下記が挙げられました。

  • OrganizationとしてのCI対象リポジトリ数は8(Android版フリル含む)
  • 最大実行時間40分のリポジトリが1つ存在する
  • その他のリポジトリのCIの平均実行時間は5~15分程度
  • コンテナ数は4つで運用

テスト並列化への具体的な取り組み

ここからは、取り組みの紹介について紹介したいと思います。

CircleCI 2.0への移行

先日 nakamuuu からAndroid版フリルのCI環境についての紹介がありました*1 が、 同時期に、サーバーサイドの各リポジトリもCircleCI 2.0に移行しました。
テスト並列化完了後の運用を考えてもバージョンアップしておくことは必須であったことと、
弊社Android版フリルの事例のように、2.0 への移行そのものが実行時間の短縮に直接貢献するのではないかという期待もありました。
結果として、環境構築に要する時間は各リポジトリ毎に平均して30秒~1分の改善が見られました。
これは主に、依存ライブラリのインストールをキャッシュできるようになったことが大きいです。
また、CircleCI 2.0 の実行環境はdockerなので、予め必要な設定をしたdocker imageをdocker hubに登録しておくと、実行時間を短縮できます。
フリルでは、rubyとelastic searchのイメージをdocker hubに登録してCI時に利用しています。

rubyイメージ
elasticsearchイメージ

CircleCI 実行コンテナを追加

開発規模が大きくなるとコンテナを追加することは避けられないと思いますが、
1コンテナにつき、$50と決して安くないコストがかかります。
1つのリポジトリのCIを2並列で実行すると、2コンテナを消費しまい、同時に実行できるCI数がその分減ってしまいます。
もっとも開発が活発な時間帯であれば、4つフルで使用し、順番待ちが発生していることも増えてきていたので、
並列化を取り入れるのを期に、コンテナ数を2つ増やし、6にしました。(+$100)
4ではすぐにキューが詰まることが予測できたことと、かといって必要以上に増やす必要がないという理由から、スモールスタートしました。

並列化

下準備が整ったところで、並列化に取り組みました。
上記のコンテナ数の問題もあり、むやみにすべてのリポジトリに対して並列化を行うのではなく、
最も時間のかかっていたCI(40分)を並列化することにしました。

並列化を行う前の実行時間の内訳は下記のようになります。

job time
setup environment 0.50m
create database 0.59m
rake test 10.25m
rspec 28.54 m
total 40.27m

実行ファイルの指定

並列化を行う場合、どのように実行ファイルを指定するかという問題がありますが、 CircleCIでは、実行ファイルを「いい感じ」に指定してくれる便利コマンドが用意されています。*2

circleci tests glob "**/*.rb" です。
これはある規則に基づいて、実行ファイルをコンテナ毎に分離してくれます。
実行する側は「今どのコンテナにいるか」を実行コマンドを書くときに意識しなくてよく、
一度実行した実行結果があればそれを基に実行時間が均等になるようファイルを分離する。ということもやってくれます。
config.yml での指定は以下のようになるでしょう

      # run tests!
      - run:
          name: run-test
          command: |
              circleci tests glob "spec/**/*.rb" | bundle exec rspec

しかし、このコマンドを使うことはできませんでした。
その理由はズバリ、ファイル間のテストの依存です。
もちろん、理想の状態はそのような依存は存在しないことではありますが、この問題を回避するために単純にディレクトリを直で指定することにしました。
その場合は、コンテナ1でどのディレクトリを指定し、コンテナ2でどのディレクトリを指定する。ということを明示する必要があります。
具体的な config.yml 上での指定は下記のようにします。

      # run tests!
      - run:
          name: run-test-1
          command: |
            if [[ $CIRCLE_NODE_INDEX = 0 ]]; then
              bundle exec rspec spec/models ...
            fi
      - run:
          name: run-test-2
          command: |
            if [[ $CIRCLE_NODE_INDEX = 1 ]]; then
              bundle exec rspec spec/controller ...
            fi

$CIRCLE_NODE_INDEX という環境変数でどのコンテナにいるのかを取得できるので、それぞれでディレクトリを指定しています。

並列化により短縮される実行時間

さて、並列化によってどのくらい実行時間が変化したかを見ていきたいと思います。
以下に、フリルで最も実行時間の長かったCIを2並列で実行した場合と3並列で実行した場合の実行結果を示します。

  • 2並列
job time(コンテナ1) time(コンテナ2)
setup environment 0.45m 0.30m
create database 1.12m 0.51m
rake test 10.01m 0m
rspec 7.14m 17.58m
total 19.12m 18.38m
  • 3並列
job time(コンテナ1) time(コンテナ2) time(コンテナ3)
setup environment 1.00m 0.45m 0.32m
create database 1.11m 0.35m 1.16m
rake test 10.57m 0m 0m
rspec 0 m 12.42m 12.29m
total 13.08m 13.22m 14.17m

ここでCIの実行時間は、並列実行したコンテナのうち、最も遅いコンテナの実行時間になります。
以上から言えることは、並列化によって実行時間を半分にすることはできますが、むやみに並列実行数を増やしても、実行時間が半分になっていくわけではないということです。
今回は費用対効果を考えた結果、2もしくは3並列にすることが最もコスパがよいであろうと判断しました。

最適な並列実行数の検証

先程も述べましたが、並列化には並列化した分だけコンテナを消費するので、最適な並列数を見つける必要があります。
最も実行時間が長いCIに関して言えば、3並列にすることが望ましいですが、その場合、6コンテナのうち3コンテナを消費してしまいます。
そこで、1週間ずつ試験運用しながら、3つのリポジトリの平均待ち時間にどの程度差が出るかを検証しました。
今回は各リポジトリの、開発が活発な(同時に6コンテナをすべて消費する)時間帯のビルドから無作為に10個を選び、その平均の待ち時間を計算しました。 結果は以下になります。

  • 3並列
リポジトリA リポジトリB リポジトリC total平均
6.2m 4.0m 10.1m 6.7m
  • 2並列
リポジトリA リポジトリB リポジトリC total平均
3.1m 2.1m 2.3m 2.5m

3並列にした場合にはやはり全体としての待ち時間が長くなっていることがわかりました。
この結果(定量的データ)と、開発者からの意見(定性的データ)を総合して、最も実行時間の長いCIを2並列で運用するのが適切であろうと判断しました。
今後の運用状況を見ながら、必要に応じてコンテナ数の追加と並列実行数を増やしていければと考えています。

まとめ

今回はフリルサーバーサイドのCI環境の現状と改善のための取り組みについて、この1ヶ月ほどでやったことについて紹介しました。
CircleCI2.0への移行、コンテナ数の追加、そして並列化を行うことで大幅に全体のCI時間を短縮することができました。
CIを改善するにあたっては、コンテナ数の増加に顕著なように、お金で殴ることも必要になります。
余談ですが、入社後すぐのタイミングでその価値をチームとして十分に認識し、必要であれば投資をするという弊社の姿勢を感じることができました。
また、CIの改善を通じて、フリルの現状と問題点の認識を深めることができたことも大きな収穫でした。
今後の課題としては、

  • 最も実行時間が長いCIの高速化(20分でもまだまだ遅い!)
  • テストの依存性の解消

などが挙げられ、より本質的に問題を解決していくことが求められます。
FablicではCIの改善に興味のあるサーバーサイドエンジニアを募集しております。
興味のある方はぜひ一緒に取り組みませんか?