8

快就是帥,加速你的 Rails 專案啟動時間

 2 years ago
source link: https://blog.niclin.tw/2018/07/21/%E5%BF%AB%E5%B0%B1%E6%98%AF%E5%B8%A5%E5%8A%A0%E9%80%9F%E4%BD%A0%E7%9A%84-rails-%E5%B0%88%E6%A1%88%E5%95%9F%E5%8B%95%E6%99%82%E9%96%93/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Nic Lin's Blog

喜歡在地上滾的工程師

速度快就像雷神降臨一樣帥

當 Rails 專案逐漸成長之後,最令人頭痛的就是 Rails 的 boot time 實在是很久。

有些沒辦法 reload on change 的部分(例如修改 config、安裝 gem)都需要將 server 重啟,然後發現跑一次竟然要十幾秒甚至二十秒,發現 Rails developer 人生大半的時間其實都浪費在啟動時間…

除了加強硬體規格以外,比較能做的事情就是用 preloader 預先加載 Rails 進行加速,一般常用的有 spring / zeus,加上近期的 bootsnap 又更好的提昇效率了,依照多數使用者回報的數據,幾乎都能減少一半以上的時間,這個 Gem 預先把 code compile 出來的 bytecode cache 到 tmp/cache 之下再加上把 Ruby 不太有效率的 require 也 cache 所以就更快了。

不過如果用了這些工具還是想要更快的話,那就需要一些 tricks 了,這邊分享一下我的優化過程,主要會是著重於 Test + Development 模式, Production 能加速的著實有限。

如何測試啟動時間

完全測冷啟動,不吃 preloader

$ time bundle exec rake environment
# bundle exec rake environment  22.78s user 11.76s system 75% cpu 45.601 total

會吃 preloader 的啟動

$ time (bin/rails runner nil)

# Running via Spring preloader in process 60378
# ( bin/rails runner nil; )  0.22s user 0.10s system 31% cpu 1.017 total

測試每個 gem 的加載速度,這裡我用 bumbler

測之前記得讓 bootsnap 關閉,否則會測不出來

$ bumbler

    296.76  carrierwave
    378.10  ethereum
    393.60  devise
    410.36  authy
    532.06  compass-rails
    546.09  newrelic_rpm
    558.73  config
    575.63  fog
    604.26  country_select
    608.17  coffee-rails
    621.22  pry
    711.51  sass-rails
    761.82  google-authenticator-rails
    887.80  stellar-sdk
    948.57  twilio-ruby
   1482.54  grape
   2627.09  rails

測試初始化程序加載的狀況

$ bumbler --initializers

Slow requires:
    279.70  newrelic_rpm.start_plugin
    383.96  compass.initialize_rails
    432.23  :set_clear_dependencies_hook
    589.01  active_record.initialize_database
    611.57  ./config/initializers/translation.rb
    753.06  :load_environment_config
    800.46  :load_config_initializers
   3288.36  :finisher_hook
   9697.74  :set_routes_reloader_hook

現在我們大概能掌握在慢在哪裡,我們可以開始著手處理了。

移除上古遺 Gem

專案大起來的時候,有些 Gem 可能是當初為了某個功能先加的,說不定後來不再使用或是有別的 Gem 取代了。

這時候就要去考古,找一下有沒有哪些 Gem 是「完全沒在用」的。

沒在用的一定要優先拔掉,這是比較好做卻也比較花時間的選項。

檢查有沒有用到較舊且有 memory leaky issue 的 Gem

可以參考 A list of gems that have memory leaks

對環境進行 Gem 分組

進行分組的用意是要明確的執行,在每個環境下只載入需要用的 Gem,避免多餘的 require 來拖慢速度。

前端相關非啟動就需要執行的 Gem, 在 Test 環境下不該被加載,應該給予 group [:production, :development]

# Styles
group :production, :development do
  gem "coffee-rails", "~> 4.2"
  gem "sass-rails", "~> 5.0"
  gem "bootstrap-will_paginate"
  gem "bootstrap-sass"
  gem "bootstrap-switch-rails"
  gem "bootstrap3-datetimepicker-rails", "~> 4.14.30"
  gem "font-awesome-rails"
  gem "jquery-ui-rails"
  gem "active_link_to"
  gem "rqrcode"
end

異步執行等相關任務,沒有要進行測試也不要在 Test 環境下加載

# Backgroud Jobs
group :production, :development do
  gem "sidekiq"
  gem "sidekiq-statistic"
  gem "sidekiq-cron"
  gem "sidekiq-unique-jobs"
  gem "sidekiq-status"
  gem "whenever"
end

只有在 Production 有用的監測服務也不要在 Development﹑Test 等其他環境下加載

group :production do
  gem "newrelic_rpm"
  gem "scout_apm"
end

只有開發使用的必須要自己一組

group :development do
  gem "letter_opener"
  gem "rubocop", require: false

  # profiling
  gem "bumbler", require: false
  gem "rack-mini-profiler", require: false

  # Dev helpers
  gem "annotate"
end

分組後可以避免加載不需要的部分,讓啟動更有效率。

require: false

在 Gem file 裡面寫 require false 是指說,bundle 的情況下一定會安裝這個 Gem, 但不在 rails 啟動時直接 require 加載進來。

只在有需要的部分 require 會更有效率。

# Web Server 不需要直接 require,因為我們是單獨啟動,並不是執行在 Rails application 內
gem "puma", "~> 3.9", require: false

# sitemap 生成器,只有執行時會用到,不需要 preload
gem "sitemap_generator", require: false

# 顯示進度條的小工具,通常用在 task 執行,也只要在 task 的檔案 require 就好 
gem "ruby-progressbar", require: false

# 只有語法檢測的時候會執行,其他時候 Rails app 並不會使用到
gem "rubocop", require: false

可以看到以上的範例有一個共通點,就是這些 Gem 都是單獨執行任務的,並不是隨時隨地在 Rails application layer 內需要用到的,所以可以直接 require: false

這樣一來,可以確保執行這個專案的時候一定會安裝相依套件,但卻不會在 Rails 啟動時直接加載。

別讓 Airbrake 在 development 被 require

這個一直是我認為滿煩的問題,常常會需要用到 Airbrake 來接 error,但畢竟這服務是賣 quota 的,恨不得你在 Development 模式下也用。

Development 的情況我是一定不用的,原因如下:

  1. 開發模式可以自己噴 Error,真的不需要你接走,謝謝喔
  2. Quota 要錢的

不過雖然 Airbrake 可以設定 ignore_environments = %w(development test),但最煩的是,他是在每個環境下都會加載的,不信你關個 server 或是退出 console 就知道了。

$ rails console

# Running via Spring preloader in process 61400
# Loading development environment (Rails 5.0.7)
# Development [1] rocket(main)> exit
# **Airbrake: closed

看到 **Airbrake: closed 了嗎?表示他是無所不在的。

如果你直接把他從 development 和其他環境下拔除

# Gemfile

group :staging, :production do
  gem "airbrake", "~> 6.1"
end

那麼就會遇到

  1. 啟動時直接噴錯,因為 initialize/airbrake.rb 初始化加載會找不到 gem
  2. 有使用 Airbrake.notify(error) 的地方會爆炸

最佳解法,在不該出現的環境做一個空殼

# config/initializers/airbrake.rb

if Rails.env.staging? || Rails.env.production?
  Airbrake.configure do |c|
    ...
    ...
  end

  Airbrake.add_filter do |notice|
    ...
    ...
  end
else
  module Airbrake
    extend self
    def notify(exception, opts = {})
    end
  end
end

就可以完美解決這個問題了。

Console 要用的套件不該 preload

我相信應該滿多人裝 awesome_rails_console 之類的,進 console 除了格式漂亮以外真的很慢,而且這種算是開發人員工具的也不該直接就在各個環境下加載。

我認為最好的做法是,在啟動 console 時才加載,不需要讓整個 Rails app 提早 preload

先用 require: false

group :staging, :development, :test do
  # Dev helpers
  gem "awesome_rails_console", require: false
end

再設定 development 環境下開啟 console 要使用 Pry

# config/environments/development.rb

config.console = Pry

建立一個 .pryrc 的檔案在 project 目錄下

# .pryrc

require "awesome_rails_console"
AwesomePrint.pry!

這樣在 Console 啟動時,會直接使用 Pry 啟動,然而 Pry 啟動前又會去讀取 .pryrc 的設定檔,如此一來,就只有在 console 啟動時會 require 了。

讓 routes 加速更快

我很遺憾,這是唯一一個目前想不到解法的部分,當專案的 routes 被越養越大,大型的路由文件在啟動上花費的時間真的很多,我目前手上最大的專案大概就有 4 秒在 reload routes,在 bumbler 的效能分析上,他是 :set_routes_reloader_hook initializer,如果把這個問題也解掉,也許能在快上 30% ?


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK