Housmartの宮永です。
Ruby on Rails 5.2 betaからデフォルトGemとしてRailsの起動を高速化するBootsnapが新たに採用されました。Railsでサービス運用をしているとGem数が増加し起動時間もどんどん遅くなっていくと経験をされている方も多いのではないでしょうか。
今回はBootsnapに関して、起動時間に関する検証を行いましたので簡単に紹介したいと思います。
なお、今回はQiita Ruby on Rails Advent Calendar 201712日目の記事になります。
Bootsnapとは
概要
BootsnapはRails減らすことでにアプリケーションの起動時間を高速化するgemで、そのパフォーマンスは公式ドキュメントでも堂々と記載しています。(Discourseの報告ではBootsnap導入前後で6秒から3秒と50%削減とあります。)
Discourse reports a boot time reduction of approximately 50%, from roughly 6 to 3 seconds on one machine; One of our smaller internal apps also sees a reduction of 50%, from 3.6 to 1.8 seconds; The core Shopify platform – a rather large monolithic application – boots about 75% faster, dropping from around 25s to 6.5s. 公式ドキュメント
これはどういった仕組みで実現されているのでしょうか。
基本的には2つの機能で持って起動時間削減を実現しています。1つはPath Pre-Scanning、もう1つはコンパイルされたコード(Ruby, YAML)のキャッシュです。
Path Pre-Scanningはファイルパス自体をキャッシュし、無駄なRubyのシステムコール数を減らすことで効率化しています。
例えば$LOAD_PATH = [x, y, z, ...]
となっているようなRailsアプリケーションの場合、 require 'sample'
を実行すると$LOAD_PATH内の全ディレクトリを対象に下記のようにopenが実行されます。下記のような状態です。
open x/sample.rb # (fail)
open y/sample.rb # (fail)
open z/sample.rb # (success)
close z/sample.rb
Bootsnapの場合、パスを事前にスキャンしキャッシュすることで最短ルートでファイルにたどり着くということになります。
open z/sample.rb # (success) use cache
close z/sample.rb
キャッシュ時間についてはstableな領域と可変的な領域とに分類して、可変的な領域については30秒のみ有効とのことです。
(stable: /usr/local/ruby
, Gem.path
配下, Bundler.bundle_path
など)
この記述を見る限り、$LOAD_PATHのボリュームに応じてパフォーマンス差が出るようです。 「大規模なRails appで効果が大きい」としてあるのもこの辺りが影響しているのかもしれません。
今回の検証でも検証環境の欄に$LOAD_PATH
を記載しましたので、もし試されたい方は参考になれば幸いです。
Springとの関係性
Rails x 起動高速化というとSpringを連想しますが、Springは事前にバックグラウンドでライブラリをロードし、2回目以降のRailsコマンド実行時にRailsアプリケーションの初期処理(ライブラリのロードなど)を省略することでコマンド実行を高速化します。 一方、Bootsnapは先にも述べたようにキャッシュを利用して、個々のファイルロードの効率化にフォーカスしています。両者の関係は「直行的」なものとして公式ドキュメントでも表現されています。
Note: Bootsnap and Spring are orthogonal tools. While Bootsnap speeds up… 公式ドキュメント
Bootsnap検証
それではBootsnapを検証していきます。
検証内容
- Boosnap導入前後でRailsコマンド実行速度はどの程度改善されるか
検証環境
検証環境は自身のMacBook Pro(Docker For Mac)を使って行いました。
項目名 | 概要 |
---|---|
OS | Linux (Docker For Mac) |
memory | 7.8G |
Ruby version | 2.4.1 |
Rails | 5.0.1 |
$LOAD_PATH.count | 291 |
※$LOAD_PATH
はBootsnapの性質上パフォーマンスにかなり影響しているので、記載しています。
下記コマンドを実行し、Bootsnap有無で起動時間がどの程度変わるか見てみました。 (今回はSpringも組み合わせて検証しています。)
time bin/rails runner "puts 'boot test'"
Bootsnap導入手順
gemをインストールして、boot.rbに1行追加するだけです。
gem 'bootsnap', require: false
require 'bundler/setup'
require 'bootsnap/setup' # 1行追加
検証結果
Bootsnap導入前後でRailsコマンド実行速度はどの程度改善されるか
純粋にBootsnapの効果を見たいのでSpringは起動せず検証しました。 結果はこちらです。
Bootsnap | 起動時間(2回目以降の平均) |
---|---|
なし | 8.18秒 |
あり | 4.20秒 |
5回ほどコマンド実行した上で2回目から5回目の平均で算出しています。
絵に描いたような展開ですが、起動時間は約50%削減と公式ドキュメントにあった結果になりました。
確かにキャッシュの効果が大きく出ているようです。
正直ここまで効果が出るとは思いませんでした。
まとめ
今回は開発用のコマンドでの検証だったので、一部Springと並べて説明したのですが、貢献するシーンが多少異なると思います。Bootsnapはrequireの高速化という意味ではproductionでも貢献するGemです。
確かにBootsnapによりアプリ起動が高速化されることがわかりました。
導入もかなりシンプルなので、5.2を待たずして、早速弊社でも採用していこうかなと思っています。
Housmartでは不動産業界を変えるカウルを支えるエンジニアを募集しています。
今話題のReTech!業界を変えるカウルを支えるエンジニアをWanted!