大きな Rails アプリケーションをなんとかしよう。まずは計測と可視化からはじめよう。

こんにちは、技術部開発基盤グループの id:hogelog です。

RubyKaigi 2018 楽しかったですね。僕はおそらく RubyKaigi 2010 以来の久しぶりの参加でした。ああいう場の楽しさを思い出し、また今回はスポンサーブースから RubyKaigi に参加するという学生の頃は知らなかった楽しみも新たに知り、RubyKaigi を満喫させていただきました。

さて今回はそんな RubyKaigi で取り戻した Ruby に対する感情と関係あるようなないような、最近自分が取り組んでいるお台場プロジェクトとプロジェクト内で実施している計測と可視化について紹介します。

お台場プロジェクトの発足

クックパッドの開発といえば数年前までは cookpad_all という一つのリポジトリの中に詰め込まれた巨大なモノリシック Rails アプリケーションを社内のエンジニアが寄ってたかって開発するというのが典型的な開発スタイルでした。世界でも類を見ない規模の巨大な Rails アプリケーションの開発であるため、もちろん多様な技術的困難が発生していましたが様々な技術を用いてアプリケーションをメンテナンスし Rails の良さを損なわず開発が進められるように努力していました。*1

しかしその後クックパッドでも徐々にモノリシックアプリケーション構成から Microservices 構成への移行が進んでいきました。 techlife.cookpad.com

そして気づけば cookpad_all は社内に数多く存在する他のアプリケーションと比較してずいぶんと古臭い、触ることが忌避されがちなアプリケーションの代表となっていました。 https://cookpad.com/ のバックエンドの大部分を支える重要なアプリケーションであるというのは変わっていないのに。そこで始まったのがお台場プロジェクトでした。お台場プロジェクトとはなんなのか。その全貌を語るのはまた別の機会としますが、実施することは端的に言えば cookpad_all というアプリケーションの実装の改善です。

お台場プロジェクトではレガシーなシステムの削除、未使用コードの削除、システム分割など様々なことをおこなっており、 id:riseshia が取り組んだ Ruby の lazy loading の仕組みを利用して未使用の gem を探す - クックパッド開発者ブログ や RubyKaigi 2018 LT で発表した Find out potential dead codes from diff もその一環です。

以下ではお台場プロジェクトを進めるにあたって取り組んだ cookpad_all 関連メトリクスの計測について紹介します。

cookpad_all 関連メトリクスの計測

cookpad_all の開発における困難を改善するといってもどう改善されているのか記録し、可視化しなければなにもわかりません。

そこでお台場プロジェクト開始初期にまず cookpad_all に関するメトリクスを計測し、社内で稼働させている InfluxDB に記録し、Grafana でダッシュボードを作成しメトリクスを可視化できている状態を作りました。

f:id:hogelog:20180607161915p:plain

具体的には現在 cookpad_all では以下のようなメトリクスを可視化し、改善を進めながら経過を観測し続けています。

  • CI Duration
  • App Load Time (development / production)
  • Loaded File Count
  • Code Statistics
  • GemCollector Up-to-date Point
  • Dependent Gem Count

CI Duration

これは Jenkins で実行している CI にかかった時間の計測です。気をつけることとしては失敗した時の実行時間は不安定になることが多いので、成功した時の時間のみ記録していることです。上記の図で示すように 2017/7 〜 2018/6 現在に至るまで、長いものでは 10 分程かかっていたものが 7分程度まで実行時間が削減されています。

App Load Time

開発者が手元で bin/rails s した時にアプリケーションが動き出すまでの遅さはわかりやすく辛い箇所です。cookpad_all の各アプリケーションでは定期的に以下のようなスクリプトを実行しアプリケーションのロードにかかった時間を計測しています。

def profile_app_load_time
  Benchmark.measure do
    system("./bin/rails r '1;'") or raise "error"
  end
end

# Warming disk cache, ...
puts profile_app_load_time

3.times do
  result = profile_app_load_time
  puts result
  influx.write_point("cookpad_ci_app_load_time", tags: { app: app }, values: { load_time: result.real })
end

Loaded File Count

これはアプリケーションのロードが終わった時点での $LOADED_FEATURES の数です。この数字は依存 gem の追加や削除、大規模なコード削除などで大きく数字が動き、アプリケーションになにか大きな変更があったことの観測に役立っています。

Code Statistics

bundle exec rake stats *2 の数字を記録するものです。この数字も時々誰かがどこか外部で「クックパッドの巨大 Rails アプリケーション」の発表をする時に計測する程度で、定点観測はおこなわれていませんでした。

f:id:hogelog:20180607172035p:plain

大きなシステム分割などにより時々グッと下がっている以外にも、日常的なコード掃除などで地道ながらもコード削減が進んでいることがダッシュボードを見るだけでわかるようになりました。

Dependent Gem Count

依存している gem の数の記録です。数が増えれば増えるほど gem の依存関係が深くなり、新規 gem の導入や既存 gem の更新などが難しくなっていきます。

f:id:hogelog:20180607161805p:plain

このメトリクスは git のログを遡り 2011 年頃からの値を計測してみましたが、依存する gem 数はお台場プロジェクトが始まるまでは増える一方でありアプリケーションを小さくしていこうという開発の流れはほぼ存在していなかったことがわかります。

ちなみに一瞬依存 gem 数 が400個を超えたところがあるのが目を引くかもしれないので説明しておくと、これは aws-sdk を v2 -> v3 にアップグレードし、その後で必要な aws-sdk-* のみに絞るよう修正したためです。

GemCollector Up-to-date Point

これは この gem を使っているアプリケーションを探す - クックパッド開発者ブログ で紹介した GemCollector で出している gem の最新度を記録しているものです。

f:id:hogelog:20180607161828p:plain

この値は相対的なものであるため、gem のバージョンアップに追従していかないとどんどんポイントが下がっていきます。対応をおこたっていくといどんどんアプリケーションがレガシーになっていく状況を把握するのに非常に便利なグラフになっています。

まとめ

クックパッドでは現在巨大モノリシック Rails アプリケーションに頼った開発から Microservices 構成のアプリケーション群を組み合わせて使ったサービス開発への急速な移行段階にあります。その中で最後に残されている巨大 Rails アプリケーションを改善していくためのメトリクス収集と可視化ダッシュボードについて紹介しました。

私達はそういうことを一緒にやっていく仲間をもっともっと求めています。定型文じゃなくて本当に求めています。採用への応募またはどんな会社なのか聞くために遊びに来たいみたいなお声がけ、お待ちしております。

*1:どんな技術を用いていたか詳しくは Ruby on Ales 2015 で @amatsuda が発表した The Recipe for the World's Largest Rails Monolith などで詳しく説明されています

*2:実際にはちょっと特殊なディレクトリ構成に対応するため cookpad:stats という独自タスクを定義しています

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/