Jenkinsとrrrspecと私

Misoca開発チームの黒曜(@kokuyouwind)です。
最近PS VRを買いました。画像は夏にSony StoreのPS VR体験会へ行った際、スタッフの方が撮ってくださった写真です。

f:id:kokuyouwind:20161130184820j:plain

OculusやViveと比べると解像度は低めですが十分な没入感がありますし、なによりアイマスVOCALOIDなどのキャラクターコンテンツが色々あるのは強いですね。
PS VRはいいぞ。

rspec-queueからrrrspecへの移行

MisocaではJenkinsを使ってCIを回しています。
またrspecでテストを書いており、Jenkins上では時間短縮のためにrspec-queueを使って並列実行していました。
しかし、テストが増えるにつれてrspecの実行時間が長くなってしまい、CPUコア数やメモリの制約で1ノード内での並列数も限界になっていました。

このため、ビルド時間の短縮を目的にrrrspecへの移行を行いました。 今回はこの移行についての話を紹介したいと思います。

rrrspecについて

rrrspecは、クックパッド社製のrspec分散実行ライブラリです。
複数のworkerがそれぞれジョブキューからジョブを取得して実行する構成になっており、workerのマシンやプロセスが落ちた際の自動復帰、無反応プロセスのタイムアウトなどの機能が特徴となっています。

技術的な詳細についてはクックパッド開発者ブログの記事がまとまっています。
また、Software Design 2016年8月号に特集記事が掲載されており、今回のrrrspec移行でも、こちらの特集記事を参考に作業を進めました。

サーバ構成

rrrspec移行前

rrrspec移行前は下図のような構成になっており、Jenkins Slaveノード上でrspec-queueプロセスを立ち上げて実行していました。
またMySQLはプロセスのメモリオーバーヘッドを考慮し、2つのJenkins Slaveノードのうち片方でのみ立ち上げて、それを両方から見る構成にしていました。

f:id:kokuyouwind:20161202112726p:plain

この構成だと、Jenkins SlaveノードのCPUやメモリ構成を簡単には変えられないため、rspec_queueで上げられる並列度には限界がありました。
またJenkins Slaveノードにはベアメタルサーバーを利用していたのですが、普段はかなりのリソースを余らせているものの、ジョブが集中するタイミングではリソースが全く足りない、という状況になっていました。

rrrspec移行後

rrrspec移行後は下図のような構成になっています。

f:id:kokuyouwind:20161202112746p:plain

移行前と比べるとかなり複雑な構成に見えますが、Jenkinsクラスタとrrrspecクラスタはrrrspec masterノードを間に挟んでいるだけで、クラスタ間では参照がない構成になっています。
こうしてみると、それぞれのクラスタについては比較的シンプルな構成となっているのが分かるかと思います。

rrrspec-workerノードについて、図中には1ノードのみ記載していますが、実際には複数台を起動しtaskが分散実行されるようにしています。
この際、rrrspec masterノードからはrrrspec workerノードを知る必要がないため、適切な設定でrrrspec workerノードを立ち上げるだけでスケールアウトすることができる、というのがrrrspecの素晴らしい点だと思います。

JenkinsのJob定義

rrrspecの移行と同時期にJenkinsのバージョンを2系に上げたため、rrrspecを実行するテストはPipelineを利用してJenkinsfileに記述しています。
以前のジョブ定義に比べると、Jenkinsfile化されたことで履歴管理ができレビューもしやすくなり、stage viewを見ることで失敗したstageを手早く把握できるようになりました。

stage viewでの表示

stage viewでの表示は以下のようになっています。

f:id:kokuyouwind:20161201180829p:plain

このうち、rrrspecに関連しているstageはRspec StartとRspec Waitです。

Jenkinsfileの内容

Rspec Start stageでは、以下のように rrrspec-client start を実行し、 taskId を取得します。

stage 'Rspec Start'
taskId = sh(returnStdout: true, script: '''#!/bin/bash -e
bundle exec rrrspec-client start \
  --rsync-name=jenkins-${NODE_NAME}-${EXECUTOR_NUMBER}
''').trim().split("\n").last()

一方、Rspec Wait stageでは取得した taskId を指定してrrrspecのタスクセット完了を待ち、完了したら結果を表示します。

stage 'Rspec Wait'
withEnv(["TASKID=${taskId}"]) {
  bash '''
bundle exec rrrspec-client waitfor $TASKID
bundle exec rrrspec-client show $TASKID
'''
}

この2つのstageの間に、lintやフロントエンドのテストを行うstageを挟んでいます。
これにより、pipelineとしては直列のまま先にrrrspecを回す、というちょっとした並列処理を行ってジョブ実行時間を短縮しています。

なお、Jenkinsではparallelを使って並列処理を定義することもできるのですが、parallel内部ではstageを定義することができません。
このためstage viewでrrrspecとlint・フロントエンドテストが1つのstageとして表示されてしまい、どこで落ちたか分かりにくくなってしまうという問題があったため、上述のような実装になっています。

rrrspec workerのスケーリング

rrrspec workerはインスタンスが突然停止されても問題ないことから、コストパフォーマンスの高いAmazon EC2 スポットインスタンスを利用しています。
前述の通り、適切な設定でAMIを作成しておけばインスタンスを起動しただけでrrrspec masterに接続してtaskの実行を開始することができます。
このため他ノードの設定などを行う必要がなく、簡単にAuto Scalingの仕組みに載せることができます。
CIでの自動テストでは、Jobがない状態ではリソースが不要な一方で、Jobが集中しているときには大量のリソースを必要とします。
このためAuto Scalingに載せやすいというのはコスト削減の点でもJobのスループット向上の点でも非常に重要です。

スケーリング設定

現在はrrrspec workerインスタンスのAuto Scalingを以下のように設定しています。

  • スケジュールされたアクション
    • 平日5時から、最小5, 最大5
    • 平日9時から、最小5, 最大15
    • 毎日20時から、最小1, 最大5
  • スケーリングポリシー

考慮したこと

Misocaでは、.rrrspecconfig.max_workers = 5と設定しているため、1つのTasksetにつきrrrspec workerを最大で5ノード利用します。
このため5ノード単位でworkerをスケールさせることで、各Tasksetを最大効率で実行することができます。

また、基本的には日中の決まった時間にのみ勤務しているため日中にリソースを集中させたいのですが、フレックス勤務を採用しており夜間に少し作業を行うこともあるため、夜間でもCIが実行できるようにしておく必要がありました。
さらにallnightジョブのプラクティスを取り入れており、作業の行われていない早朝に数回テストを実行して安定性を確認するJobを実行しているため、この時間帯はリソースを確保しておく必要があります。

設定の意図

スケジュールについては、最小数指定で最低限のインスタンスを常に確保しつつ、負荷があがったタイミングでは最大数までスケールするように意図しています。
5インスタンスのまとまりで考えると、allnightジョブを走らせる平日5時〜9時は1並列固定、平日日中は1-3並列です。
夜間はやや特殊で、常時確保するのは1インスタンスのみで、ビルドが始まったタイミングで5台までスケールするようにしています。
このため夜間は少しビルドに時間がかかりますが、5インスタンスを常に立ち上げておくのに比べるとほぼ1/5のコストに抑えられます。
夜間はたまにしかジョブが実行されないことを考えると妥当なトレードオフではないかと思います。

スケーリングポリシーについては、CPU負荷が高い場合はリソースが枯渇しているのでインスタンスを増やし、CPU負荷が下がったときには余っているリソースを開放する、という動作を意図しています。
インスタンス追加する基準が60%となっているのは、10台ある状態で5台だけが動いているときにはリソースが余っているため、インスタンスが追加されないようにするためです。

スケーリングの可視化

インスタンス数やCPU UtilizationなどはCloudwatchのダッシュボードを使って一覧できるようにしています。
左上のインスタンス数グラフとその下のCPU Utilizationをみると、CPU負荷に応じてインスタンス数がスケールしているのが分かるかと思います。

f:id:kokuyouwind:20161201191229p:plain

まとめ

Misocaではrrrspecを利用してテストを分散実行するようにすることで、以前は20分前後かかっていたビルド時間を10分程度まで短縮することができました。
またスポットインスタンスを利用し必要なタイミングでスケールさせるようにすることで、以前に比べてコストも削減できました。

今後の課題として、以下のようなことを考えています。

  • スポットインスタンスをm4.large以外も使えるようにして、よりコストパフォーマンスの高いものを自動で使うようにできないか
  • 常にm4.largeを確保しているMySQLノードについてもスケールさせられないか
  • CPU Utilizationではなくrrrspecのtasksetの状態に応じてスケールさせられないか

宣伝

Misocaは継続的にCIを改善したいエンジニアを募集しています!

あと島根で働きたいエンジニアも募集しています!