如何调试速度慢得无法忍受的 Rails 资产预编译

Posted

技术标签:

【中文标题】如何调试速度慢得无法忍受的 Rails 资产预编译【英文标题】:How to debug a Rails asset precompile which is unbearably slow 【发布时间】:2013-10-19 22:35:07 【问题描述】:

我正在开发一个 Rails 3.2 项目,尽管我认为项目规模不大,但最近几个月资产增加了不少。资产由 JS(无咖啡脚本)和 SASS 文件组成;我们有很多图片,但它们从早期开始就更少了,所以我认为它们不是一个重要因素。我们可能有十几个库,大多数都很小,最大的是 Jquery UI JS。部署是通过 Capistrano 完成的,并且开始变得很明显,部署到登台比部署到生产要快得多。为了说明,同时避免不同服务器和网络影响的因素,我只是在我的笔记本电脑上按顺序运行了以下三个命令,如下所示:

$ time RAILS_ENV=production bundle exec rake assets:precompile
^Crake aborted!
[Note I aborted this run as I felt it was getting stupidly long...]
real    52m33.656s
user    50m48.993s
sys 1m42.165s

$ time RAILS_ENV=staging bundle exec rake assets:precompile
real    0m41.685s
user    0m38.808s
sys 0m2.803s

$ time RAILS_ENV=development bundle exec rake assets:precompile
real    0m12.157s
user    0m10.567s
sys 0m1.531s

所以我只能摸不着头脑。为什么不同环境之间存在如此巨大的差异?我可以理解开发和登台之间的差距,但是我们的登台和生产配置是相同的。 (我应该指出,生产编译将在大约 2 小时后完成!)

虽然最终结果是让我的预编译速度更快,但我想通过了解所有时间的去向以及为什么 Rails 环境之间存在如此大的差异来实现这一目标。我看过其他关于使用不同压缩器等的帖子,但我找不到任何关于如何调试这些 rake 任务以找出时间花费的信息并确定哪些设置可能导致如此巨大的差异。

我不知道人们可能需要什么其他信息,所以如果 cmets 询问,我会更新。 TIA

更新:下方提供的其他信息

config/environments/production.rbconfig/environments/staging.rb(它们完全一样):

MyRailsApp::Application.configure do
  # Code is not reloaded between requests
  config.cache_classes = true

  # Full error reports are disabled and caching is turned on
  config.consider_all_requests_local       = false
  config.action_controller.perform_caching = true

  # Disable Rails's static asset server (Apache or nginx will already do this)
  config.serve_static_assets = true
  config.static_cache_control = "public, max-age=31536000"
  config.action_controller.asset_host = "//#MyRailsApp::CONFIG[:cdn]"

  # Compress javascripts and CSS
  config.assets.compress = true

  # Don't fallback to assets pipeline if a precompiled asset is missed
  config.assets.compile = false

  # Generate digests for assets URLs
  config.assets.digest = true

  # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
  # the I18n.default_locale when a translation can not be found)
  config.i18n.fallbacks = true

  # Send deprecation notices to registered listeners
  config.active_support.deprecation = :notify
end

基本的 config/application.rb 是:

require File.expand_path('../boot', __FILE__)

require 'rails/all'

if defined?(Bundler)
  # If you precompile assets before deploying to production, use this line
  Bundler.require(*Rails.groups(:assets => %w(development test)))
  # If you want your assets lazily compiled in production, use this line
  # Bundler.require(:default, :assets, Rails.env)
end
module MyRailsApp
  CONFIG = YAML.load_file(File.join(File.dirname(__FILE__), 'config.yml'))[Rails.env]

  class Application < Rails::Application

    # Custom directories with classes and modules you want to be autoloadable.
    config.autoload_paths += %W(#config.root/lib)
    config.autoload_paths += %W(#config.root/app/workers)

    # Configure the default encoding used in templates for Ruby 1.9.
    config.encoding = "utf-8"

    # Configure sensitive parameters which will be filtered from the log file.
    config.filter_parameters += [:password]

    # Enable the asset pipeline
    config.assets.enabled = true

    # Stop precompile from looking for the database
    config.assets.initialize_on_precompile = false

    # Version of your assets, change this if you want to expire all your assets
    config.assets.version = '1.0'

    # Fix fonts in assets pipeline
    # http://***.com/questions/6510006/add-a-new-asset-path-in-rails-3-1
    config.assets.paths << Rails.root.join('app','assets','fonts')

    config.middleware.insert 0, 'Rack::Cache', 
      :verbose     => true,
      :metastore   => URI.encode("file:#Rails.root/tmp/dragonfly/cache/meta"),
      :entitystore => URI.encode("file:#Rails.root/tmp/dragonfly/cache/body")
     # unless Rails.env.production?  ## uncomment this 'unless' in Rails 3.1,
                                      ## because it already inserts Rack::Cache in production

    config.middleware.insert_after 'Rack::Cache', 'Dragonfly::Middleware', :images

    config.action_mailer.default_url_options =  :host => CONFIG[:email][:host] 
    config.action_mailer.asset_host = 'http://' + CONFIG[:email][:host]
  end
end

宝石文件:

source 'http://rubygems.org'

gem 'rails', '3.2.13'   
gem 'mysql2'
gem 'dragonfly', '>= 0.9.14'
gem 'rack-cache', :require => 'rack/cache'
gem 'will_paginate'
gem 'dynamic_form'
gem 'amazon_product' # for looking up Amazon ASIN codes of books
gem 'geoip'
gem 'mobile-fu'
gem 'airbrake'
gem 'newrelic_rpm'
gem 'bartt-ssl_requirement', '~>1.4.0', :require => 'ssl_requirement'
gem 'dalli' # memcache for api_cache
gem 'api_cache'
gem 'daemons'
gem 'delayed_job_active_record'
gem 'attr_encrypted'
gem 'rest-client'
gem 'json', '>= 1.7.7'
gem 'carrierwave' # simplify file uploads
gem 'net-scp'

# Gems used only for assets and not required
# in production environments by default.
group :assets do
  gem 'therubyracer'
  gem 'sass-rails',   '~> 3.2.3'
  gem 'compass', '~> 0.12.alpha'
  gem 'uglifier', '>= 1.0.3'
  gem 'jquery-fileupload-rails'
end

gem 'jquery-rails'
gem 'api_bee', :git => 'git://github.com/ismasan/ApiBee.git', :ref => '3cff959fea5963cf46b3d5730d68927cebcc59a8'
gem 'httparty', '>= 0.10.2'
gem 'twitter'

# Auth providers
gem 'omniauth-facebook'
gem 'omniauth-twitter'
gem 'omniauth-google-oauth2'
gem 'omniauth-identity'
gem 'omniauth-readmill'
gem 'bcrypt-ruby', "~> 3.0.0" # required for omniauth-identity
gem 'mail_view'

# To use ActiveModel has_secure_password
# gem 'bcrypt-ruby', '~> 3.0.0'

# Deploy with Capistrano
group :development do
  gem 'capistrano'
  gem 'capistrano-ext'
  gem 'capistrano_colors'
  gem 'rvm-capistrano'

  # requirement for Hoof, Linux equivalent of Pow
  gem 'unicorn'
end

group :test, :development do  
  gem 'rspec-rails'
  gem 'pry'
  gem 'pry-rails'
end

group :test do
  gem 'factory_girl_rails'
  gem 'capybara'
  gem 'cucumber-rails'
  gem 'database_cleaner'
  gem 'launchy'
  gem 'ruby-debug19'
  # Pretty printed test output
  gem 'shoulda-matchers'
  gem 'simplecov', :require => false
  gem 'email_spec'
  gem 'show_me_the_cookies'
  gem 'vcr'
  gem 'webmock', '1.6'
end

【问题讨论】:

传递--trace 标志会告诉你什么有趣的事情吗? 您可以为config/environments/production.rbconfig/environments/staging.rb 发布您的文件吗? @JeremyGreen 我已更新帖子以包含环境配置。请注意,暂存配置和生产配置是相同的。 您是否使用diff 验证了它们是否相同?通常这些类型的问题归结为他们需要相同,但由于某种原因不相同。 @JeremyGreen 根据diff,它们确实是相同的。 【参考方案1】:

这个问题有很多可能的原因。

出于可能的原因,我想知道在您上次部署的多个环境中编译资产的时间是如何增加的。这可能表明问题只是在环境中还是在资产编译本身中。您可以为此使用git bisect。我通常通过 jenkins 或其他 ci 系统将我的应用程序部署到 staging,以便我可以看到部署时间和引入它们的时间的任何变化。

这可能归结为大量使用资源 CPU、内存(任何交换?)、IO。如果您在生产系统上编译资产,它们可能正忙于为您的应用程序请求提供服务。转到您的系统,为资源执行top,可能同时有太多文件句柄(lsof 很好)。

另一件事可能是您为应用程序加载或缓存了一些数据。数据库在登台和生产环境中通常比它们在开发盒上大得多。您可以在初始化程序或其他程序中添加一些 Rails.logger 调用。

【讨论】:

当然,我会一分为二,看看是否有任何特定的资产或变化会导致用户流失。请注意,我正在我的笔记本电脑上进行所有基准测试——而且我在后台没有做太多其他事情——通过设置 RAILS_ENV 并且在应用程序配置中我有 config.assets.initialize_on_precompile = false ,这使数据库脱离了等式。 【参考方案2】:

我认为您需要查看 Prod 服务器上的 cpu 使用参数。

此外,资产可能会被多次预编译。 我建议在 capistrano 创建的共享目录中创建一个资产目录,复制您的更改并在部署时将其链接到您的应用程序。

这就是我的做法,

  after "deploy:update_code" do
    run "export RAILS_ENV=production"
    run "ln -nfs #shared_path/public/assets #release_path/public/assets"
    # Also for logs and temp section.
    # run "ln -nfs #shared_path/log #release_path/log"
    # run "ln -nfs #shared_path/tmp #release_path/tmp"
    #sudo "chmod -R 0777 #release_path/tmp/"
    #sudo "chmod -R 0777 #release_path/log/"
  end

【讨论】:

【参考方案3】:

这可能无法完全回答您的问题,但我相信这是一个足够好的开始。正如您将看到的,准确的答案将取决于各个应用程序、gem 版本等。

所以。如您所知,对于与资产相关的工作,Rails 使用一个名为 Sprockets 的库,我相信在新版本的 Rails 中,它作为 Railtie 与 Rails 挂钩。它初始化了一个 Sprockets“环境”,它可以做一些事情,比如查看你的资产清单、加载这些文件、压缩它们、为编译的资产提供合理的名称等。

默认情况下,Sprockets::Environment 将其活动记录到STDERR,日志级别为FATAL,这在这些情况下不是很有用。幸运的是,Sprockets::Environment(截至2.2.2)有一个可写的记录器属性,您可以使用初始化器通过 Rails 修补它。


所以,我的建议是:

config/initializers 中,创建一个文件,类似于asset_logging.rb。在里面写:

Rails.application.config.assets.logger = Logger.new($stdout)

这会用一个将更多信息输出到STDOUT 的记录器覆盖默认记录器。一旦你完成了这个设置,然后运行你的资产预编译任务:

rake RAILS_ENV=production assets:precompile

你应该会看到稍微更有趣的输出,例如:

...
Compiled jquery.ui.core.js  (0ms)  (pid 66524)
Compiled jquery.ui.widget.js  (0ms)  (pid 66524)
Compiled jquery.ui.accordion.js  (10ms)  (pid 66524)
...

但是,最终的答案将取决于:

您希望记录这些资产内容的“深度”程度 您使用的是什么特定版本的 Rails、Sprockets 等 以及沿途发现的东西

正如您已经了解到的,在 Rake 任务级别,甚至在 Rails 级别进行日志探索并不能提供太多信息。甚至让 Sprockets 本身变得冗长(见上文)并不能告诉你太多。

如果您想比 Sprockets 更深入,您可以修改 Sprockets 尽职尽责地链接在一起的各种引擎和处理器,以使资产管道正常工作。例如,您可以查看这些组件的日志记录功能:

Sass::Engine(将 SASS 转换为 CSS) Uglifier(JavaScript 压缩器包装器) ExecJS(在 Ruby 中运行 JavaScript;Sprockets 和 Uglifier 的依赖项) therubyracer(嵌入在 Ruby 中的 V8;由 ExecJS 使用) 等

但我会将所有这些作为“读者练习”。如果有灵丹妙药,我当然想知道!

【讨论】:

+1 表示尝试查看日志。这个问题闻起来像一个循环依赖。仔细检查日志中的双精度可能会有所帮助。 在最近的 Rails 版本中,您必须像这样添加一个额外的.assetsRails.application.config.assets.logger = Logger.new($stdout) 我无法获得那些“有趣的输出”,其他人有运气吗? 在 Rails 4.2.3 上运气不好 对我不起作用。我在 4.2.11 的轨道上。有人知道为什么吗?

以上是关于如何调试速度慢得无法忍受的 Rails 资产预编译的主要内容,如果未能解决你的问题,请参考以下文章

加速资产:使用 Rails 3.1/3.2 Capistrano 部署预编译

如何加快 Rails 3+ 中的资产预编译?

黑名单Rails资产被预编译[重复]

您如何修复 AWS 弹性 beanstalk rails 资产预编译的文件权限错误?

rails 应用程序未启动,资产上的“ActiveSupport::Deprection”错误:预编译

Rails 4 - 生产资产:预编译导致 ActiveRecord::NoDatabaseError: FATAL: database does not exist