Rails
capistrano
sprockets
Asset
precompile

Railsアセットの激遅Precompileを解決するまでの軌跡

More than 1 year has passed since last update.

これは Railsデプロイ時の rake assets:precompile が激遅い問題を解決するまでの軌跡を記した文章です。

環境周りのトラブルに際して「そういうのって、どうやって原因を突き止めるの?」とよく言われるので、実際に起こった事例を題材に、その解決に至る具体的な発想と調査手順をご紹介したいと思います。 (๑•̀ㅂ•́)و✧

(╭☞•́⍛•̀)╭☞ 結論だけ知りたい方は TL;DR をどうぞ。

TL;DR

事象

Rails 4 で tmp/cache/assetsshared へ symlink しているのに、Capistrano でデプロイするとアセットの Precompile が毎回激遅い。

原因

Sprockets の不備でキャッシュが使われていなかった。
バージョンが 3.1.0 〜 3.2.0 の場合にのみ発生する。

https://github.com/rails/sprockets/issues/59

解決策

Sprockets を 3.3.0以上にバージョンアップする。

取り組みの経緯

とある案件にて、既存デプロイ環境の抜本的整備を実施することになりました。

アセットの Precompile が激遅でデプロイが億劫。
なんとか小細工して高速化を試みたものの、どうも正しくデプロイできていないようで切り戻した。
サーバをスケールアップする他に手立てはないものか?

という状況でした。

環境

  • Rails 4系
  • Capistrano 3系
  • Sprockets 3.1.0
  • therubyracer (V8)
  • Amazon Linux (本番) / Mac (開発)

解決までの軌跡

1. 事前知識のインプット

まずは全体感を把握するために、実際に手を動かすのに先立って、アセット・パイプラインの仕様、評判、問題点をざっと調査しました。

その結果、なんとなく以下の事実がわかりました。


Rails 3系

遅い。

高速化には turbo-sprockets-rails3capistrano-faster-assets のような gem で小細工する必要あり。

Rails 4系

小細工なしで速い。

tmp/cache/assets があれば、キャッシュを使って差分のみコンパイルするので高速。

ただし、リリースを跨ってキャッシュを共有するために、Capistrano を採用している場合は linked_dirs'tmp/cache' が含まれている必要がある。


我々が使っている Rails は 4系であるので、ならば小細工は要らないはず。

まずは正攻法を試してみることにしました。

2. 普通にデプロイしてみる

Capistrano が <RAILS_ROOT>/tmp/cache  ->  <DEPLOY_TO>/shared/tmp/cache という symlink を張るように設定されているかを確認のうえ、普通にデプロイ。

...何度か繰り返しデプロイしてみましたが、確かに遅い。
間違いなく <RAILS_ROOT>/tmp/cache は存在しているのですが、rake assets:precompile に時間がかかっています。

手元の Mac では 2回目以降の Precompile は数秒で完了するので、何かがおかしいようです。

3. ファイルのタイムスタンプかも

もしかしてアセット・ファイルのタイムスタンプもキャッシュの有効要件に含まれているのかもと思い、app/assets配下のファイルを touch してみました。

が、Precompile は変わらず秒速で完了する...。

タイムスタンプは関係ないみたい。

4. サーバ上で直接 Precompile してみる

Capistrano が悪さをしているのかと思い、サーバ上で直接 rake assets:precompile を実行してみました。
(capコマンド実行時のログを見て、環境変数の指定など同一のコマンドを実行しました)

すると...秒速で完了。

犯人は Capistrano または SSH...??? (๑•﹏•)

5. JSランタイムか?

Capistrano/SSH が影響する処理の目星をつけるため、アセット・パイプラインの仕組みを考え直してみました。

Sprockets ... CoffeeScript ... Sass ... ExecJS ... あ、もしかして。

Capistrano (= SSH) で実行するとダメ、でもサーバ上で直接 (= インタラクティブ・シェル) 実行すると OK。

ということは、.bash_profile.bashrc 周りの設定に不備があり、SSH経由とインタラクティブ・シェルでは異なる JSランタイムが使われている (SSH の場合は超遅いランタイムが選択される) のではなかろうかと思い至りました。
(ランタイム間でそんな劇的に速度が違うのか? という疑問もありますが...)

まずは Mac上で、

$ bundle exec ruby -e 'require "execjs"; puts ExecJS.runtime.name'
therubyracer (V8)

次にサーバ上で、

$ bundle exec ruby -e 'require "execjs"; puts ExecJS.runtime.name'
therubyracer (V8)

そして SSH経由で、

$ ssh user@server "cd /path/to/<RAILS_ROOT>; bundle exec ruby -e 'require \"execjs\"; puts ExecJS.runtime.name'"
therubyracer (V8)

...全部 therubyracer でした。

JSランタイムが原因ではなかった。

6. SSH経由の Precompile を精査する

さっぱり糸口が掴めないので、もう一度、Precompile処理のみに焦点を絞って挙動を調べてみることにしました。

SSH経由で rake assets:precompile してみると...おぉ、秒速で完了しました。
であるならば、やはり Capistrano が余計なことをしているのか?

続いて Capistrano の deploy:assets:precompileタスク (rake assets:precompile のラッパ) を実行してみました。

すると...速い。
こちらも秒速で完了する。

Capistrano や SSH が原因ではないことがわかりました。

糸口が掴めました。 ヽ(•̀ω•́ )ゝ✧

7. 通常デプロイでやっていること

通常デプロイ (deployタスク) でやっていて、deploy:assets:precompileタスクでやっていないこと。
そこに原因がありそうです。

通常デプロイでやっていて、アセットの Precompile に影響しそうな処理は... <DEPLOY_TO>/releases/<VERSION> へのコード・セット配備に違いない。

それに起因して変化するアセット・ファイルの属性は以下2つ。


inode

新たにデプロイされたアセット・ファイルは中身が同じでも以前のリリース・バージョンとは異なるファイルなので、inode も異なる。

絶対パス

VERSION はデプロイのたびに変動するので、アセット・ファイルの絶対パスも変化する。


8. 原因特定

inode か絶対パスのどちらかが原因と予想がたったので、原因特定のため以下の 2パターンで挙動を見てみました。

  1. コード・セットをコピーして、元の名前にリネーム
  2. コード・セットをコピーして、別名のまま

どちらのパターンでも遅ければ原因は inode。
パターン1 は速く、パターン2 だけ遅ければ絶対パスが原因と判断できます。
(逆にパターン1 が遅くてパターン2 が速いと...謎の挙動なので振り出しに戻る...)

結果、パターン1 が速くてパターン2 は遅かったので、絶対パスが変わることが原因であるとの仮説がたちました。


仮説

キャッシュの有効要件にアセット・ファイルの絶対パスが含まれていて、「デプロイすると絶対パスが変わる = 失効したキャッシュ」と判断され、毎回フル・コンパイルされているのではないか。


9. 仮説をググる

仮説に基づき、それっぽいキーワードでググってみたところ、

Sprockets にそのものズバリな Issue が見つかりました。
https://github.com/rails/sprockets/issues/59

どうやら、

  • 3.1.0 〜 3.2.0 で発生している。
  • 3.0.3 (3.0系最終バージョン) では発生していない。
  • 3.1.0 でキャッシュのキーを絶対パスに変えた。

とのことなので、仮説は正しかったようです。
(この時点で我々が利用しているバージョンは 3.1.0)

すでに解決済みのようなので、Sprockets をバージョンアップすればよさそう。

10. 解決策

Sprockets をバージョンアップしたところ、正常にキャッシュが効くようになりました。

お疲れ様でした。

まとめ

実際に起こった事例について、どのように調査を進め、原因特定&解決できたのかをご紹介しました。

この手のトラブル対応の巧拙はサーバ管理者としてのバックグラウンドに依るところが大きいのかもしれませんが、今回ご紹介したように、どれほど手慣れたスタッフでも勘と経験で一撃解決できるわけではありません。

事象の観察と仮説&検証による問題の切り分けを泥臭く繰り返した結果やっと解決できたというようなことばかりですので、環境トラブル対応に不慣れな方もどうぞ臆せず取り組んでみてください。