4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Railsの起動を高速化するbootsnapをコードリーディングで理解する

Posted at

Rails 5.2 から標準 gem として入っている bootsnap
私自身は **install しておくだけで何かをキャッシュしてくれて起動が高速化する・・**ぐらいの理解で使っていたので、改めてコードリーディングを通して bootsnap がどういう仕組で何を解決しているのかを理解してみました。

🙇🙇肝心のコードリーディング部分はWIPです 🙇🙇

bootsnap とは

bootsnap は Shopify 社によって開発された Rails/Ruby アプリケーションの起動時間を短縮するためのライブラリです。
Shopify 社によって開発される Shopify は、巨大なモノリシックの Rails アプリケーションであり、 アプリケーションの起動時間が長いことは開発体験の大きな毀損でした。
そこで開発されたのが bootsnap です。
Shopify 社曰く、 bootsnap を利用することで、Rails アプリケーションの起動時間を 50% ほど短縮することが見込めます。

[参考リンク] Bootsnap: Optimizing Ruby App Boot Time

bootsnap の仕組み

bootsnap は、大きく分けて2つの機能で構成されています。

  1. Path Pre-Scanning(パスの事前解決)
  2. Compilation caching(コンパイルキャッシュ)

コードリーディングの前に、それぞれの機能を詳しく見てみましょう。

Path Pre-Scanning(パスの事前解決)

ruby では、 require に相対パスが指定された時、 $LOAD_PATH(別名 $:) に示されるパスを順番に探し、最初に見つかったファイルをロードします。

[参考リンク] module function Kernel.#require

ここで、 $LOAD_PATH のについて見てみます。
例えば以下の Gemfile が置かれている環境で $LOAD_PATH の中身を見てみましょう。
なお、 gem は vendor/bundle 配下にインストールしています。

Gemfile
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem "pry"
$ bundle exec ruby -e 'puts $LOAD_PATH'
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/bundler-2.0.1/lib
/usr/local/Cellar/rbenv/1.1.1/rbenv.d/exec/gem-rehash
/Users/kekekenta/www/bootsnap-test/vendor/bundle/ruby/2.5.0/gems/pry-0.12.2/lib
/Users/kekekenta/www/bootsnap-test/vendor/bundle/ruby/2.5.0/gems/method_source-0.9.2/lib
/Users/kekekenta/www/bootsnap-test/vendor/bundle/ruby/2.5.0/gems/coderay-1.1.2/lib
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/bundler-2.0.1/lib/gems/bundler-2.0.1/lib
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/site_ruby/2.5.0
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/site_ruby/2.5.0/x86_64-darwin18
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/site_ruby
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/vendor_ruby/2.5.0
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/vendor_ruby/2.5.0/x86_64-darwin18
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/vendor_ruby
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/2.5.0
/Users/kekekenta/.rbenv/versions/2.5.1/lib/ruby/2.5.0/x86_64-darwin18

上記のように、 vendor/bundle 配下にインストールされた gem などが $LOAD_PATH に設定されていることがわかります。

例えば require 'pry' を実行した場合、 上記の $LOAD_PATH のパスを上から順にチェックしていき、マッチするファイルがあったらそれを読み込み、なければ次のパスをチェックしていく形になります。
今回のケースだと、上から3番目の vendor/bundle/ruby/2.5.0/gems/pry-0.12.2/lib でファイルが見つかるはずです。
見つかるまで順に実行するため、require の度にフルスキャンが走るということになります。

$LOAD_PATH を見るとわかるように、インストールした gem 毎に $LOAD_PATH が設定されるため、gem が増えるとそれに応じて $LOAD_PATH も増えていきます。
このことから、 Rails/Ruby アプリケーションが巨大になってきて、多数の gem に依存するようになると、 require の際のパスの解決に時間がかかってしまうことが理解できると思います。

これを解決するのが、 Path Pre-Scanning(パスの事前解決) です。

Path Pre-Scannningでは、それぞれの require に対して対象のパスを初回の解決時にキャッシュしておきます。
これにより、次回起動時に require する際にキャッシュからパスを引っ張ることで、フルスキャンのコストを抑えることができ、高速に起動することができます。

bootsnap では、 既存の require に対してモンキーパッチを当てていて、毎回 $LOAD_PATH からフルスキャンするのではなくキャッシュを参照する仕組みになっています。

Compilation caching(コンパイルキャッシュ)

Compilation caching については、さらに以下の2種類に分類されます。

  1. Ruby Compilation Caching(Rubyコンパイル結果のキャッシュ)
  2. YAML Compilation Caching(YAMLコンパイル結果のキャッシュ)

それぞれについて見ていきましょう。

Ruby Compilation Caching(Rubyコンパイル結果のキャッシュ)

Ruby では、コードが実行される際に、まずは YARV 命令列 という 中間コード に変換されます。
そして、 YARV 命令列を Ruby VM が読み込み実行することで、みなさんが書いた Ruby のコードが実行されるという流れです。

YARV や Ruby VM については、下の記事などを参考にしてみて下さい。
RubyでRubyVMを実装してRubyVMの気持ちになってみる

さて、この 中間コードへの変換 つまり、 コンパイル は当然コストが掛かります。
Rails/Ruby アプリケーションを起動するたびに同じコードのコンパイルをしていては、無駄なわけです。
そこで、この コンパイル 結果をキャッシュしてしまおう、というのが Ruby Compilation Caching(Rubyコンパイル結果のキャッシュ) です。

Ruby では中間コードへのコンパイル処理をフックすることができ、これを利用して実現しています。(RubyVM::InstructionSequence.load_iseq

bootsnap は、コンパイル処理をフックし、すでにコンパイル済みのものがある場合にはキャッシュから利用することで、コンパイルの処理をスキップすることができるわけです。

[参考リンク] Ebisu.rb#18 で、RubyVM::ISeqの話をしました

YAML Compilation Caching(YAMLコンパイル結果のキャッシュ)

Rails のアプリケーションを開発していると、設定値等を YAML で管理する機会も多いと思います。
初回起動時に読み込む YAML ファイルが多くなると、 YAML の読み込みが無視できないくらいのコストになってしまいます。
そこで、 MessagePackMarshal など、 YAML に比べて読み込みコストの低い形式に変換してキャッシュすることで、読み込み時間を抑える事ができます。

コードリーディング

image.png

🙇🙇コードリーディングのまとめが終わらなかったのでWIPです🙇🙇

まとめ

今回は bootsnap の仕組みをコードリーディングを通してお伝えしました。
次回は Docker x bootsnap に関する記事を書こうと思います。
bootsnap の各種キャッシュをビルド時に生成して Docker イメージに含めることで、immutable な Docker コンテナの起動高速化を図る方法について解説します。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?