如何调试Rails资产预编译过程中极其缓慢的问题

43

我正在开发一个Rails 3.2项目,最近几个月资产有了相当大的增长,尽管我不认为这个项目很大。这些资产包括JS(没有coffee-script)和SASS文件;我们有相当多的图片,但它们自从早期以来就一直存在,所以我认为它们不是一个重要因素。我们可能有大约十几个库,大部分都很小,最大的是Jquery UI JS。部署是通过Capistrano完成的,开始变得明显的是,部署到staging比部署到production要快得多。为了说明问题,避免涉及不同服务器和网络效应的因素,我在我的笔记本电脑上按照以下顺序运行了以下三个命令:

$ 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任务以确定花费时间的位置并识别可能导致如此巨大差异的设置的信息。

我不知道人们可能需要什么额外的信息,如果有评论问到,我会进行更新。谢谢。

更新:下面提供了额外信息

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
    # https://dev59.com/hGw15IYBdhLWcg3wmc4J
    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

Gem 文件:

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

1
你能否发布 config/environments/production.rbconfig/environments/staging.rb 这两个文件? - Jeremy Green
@JeremyGreen 我已经更新了帖子,包括环境配置。请注意,暂存和生产配置是相同的。 - andyroberts
你是否使用 diff 验证它们是否完全相同?通常这些问题归结为它们需要完全相同,但由于某种原因它们并不完全相同。 - Jeremy Green
@JeremyGreen 根据 diff,它们确实完全相同。 - andyroberts
@freemanoid 哦,我明白了。是的,我已经进行了一些二分法,并发现了一些小的贡献资产变化。然而,这并不是我问题的关键所在。我如何发现 rake assets:precompile 正在做什么?它在调用什么?我能否让它记录它的进展情况?即使我找到了罪魁祸首文件,我如何确定为什么在暂存和生产之间存在如此大的差异? - andyroberts
显示剩余10条评论
3个回答

37

这可能不是完全回答你的问题,但我认为它是一个足够好的开始。正如你所看到的,确切的答案取决于个人应用程序、gem版本等。

对于与资产相关的工作,正如您所知,Rails使用一个名为Sprockets的库,它在较新版本的Rails中被钩入Rails作为一个Railtie。它初始化了一个Sprockets“环境”,可以执行诸如查看您的资产清单、加载这些文件、压缩它们、给编译后的资产合理的名称等操作。

默认情况下,Sprockets :: Environment将其活动记录到具有FATAL日志级别的STDERR中,在这些情况下并不是非常有用。幸运的是,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。问题似乎是循环依赖关系之一。仔细检查日志中的重复可能会有所帮助。 - kik
10
在最近的 Rails 版本中,你需要额外添加 .assets,像这样:Rails.application.config.assets.logger = Logger.new($stdout) - amoebe
6
我无法得到那些“有趣的输出”,其他人有没有成功? - mountriv99
1
Rails 4.2.3没有成功。 - Shan
1
对我不起作用。我使用的是Rails 4.2.11。有人知道为什么吗? - Nathan B
rake RAILS_ENV=production assets:precompile --trace在生产环境下预编译资产。 - Nathan B

2

我认为您需要查看生产服务器上的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

2

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

作为一个可能的原因,我想知道你最近几次部署时编译资产的时间在几个环境中是否有所增加。这可能表明问题只是在环境中,或者在资产编译本身内部。你可以使用git bisect来解决这个问题。通常我会通过Jenkins或其他CI系统将我的应用程序部署到staging环境,以便看到部署时间的任何变化以及它们引入的时间。

这可能归结为资源CPU、MEMORY(任何交换?)、IO的广泛使用。如果您在生产系统上编译资产,则它们可能正在忙于处理应用程序请求。前往您的系统,为资源执行top命令,也许同时有太多的文件句柄(lsof非常好)。

另一件事可能是您要为应用程序加载或缓存某些数据。数据库在staging和production环境中通常比在开发环境中大得多。您可以在初始化程序或其他地方放置一些Rails.logger调用。


当然,我正在执行二分法查找,以发现是否存在特定的资源或更改导致了性能下降。请注意,我正在我的笔记本电脑上进行所有基准测试 - 并且我在后台没有做太多其他操作 - 通过设置RAILS_ENV,并在应用程序配置中设置config.assets.initialize_on_precompile = false,这将使数据库不参与编译初始化。 - andyroberts

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接