Ускорение активов: предварительная компиляция с Rails 3.1/3.2. Внедрение Capistrano

Мое развертывание происходит медленно, они занимают не менее 3 минут. Медленная задача Capistrano при развертывании - это активы: прекомпиляция. Это занимает, вероятно, 99% от общего времени развертывания. Как я могу ускорить это? Должен ли я предварительно скомпоновать свои активы на моем локальном компьютере и добавить их в мой git repo?

Изменить: добавление config.assets.initialize_on_precompile = false в мой файл application.rb уменьшило время прекомпиляции с половиной минуты, но оно все еще медленное.

Ответ 1

Идея состоит в том, что если вы не меняете свои активы, вам не нужно перекомпилировать их каждый раз:

Это решение, которое Бен Куртис предлагает для развертывания с git:

 namespace :deploy do
      namespace :assets do
        task :precompile, :roles => :web, :except => { :no_release => true } do
          from = source.next_revision(current_revision)
          if releases.length <= 1 || capture("cd #{latest_release} && #{source.local.log(from)} vendor/assets/ app/assets/ | wc -l").to_i > 0
            run %Q{cd #{latest_release} && #{rake} RAILS_ENV=#{rails_env} #{asset_env} assets:precompile}
          else
            logger.info "Skipping asset pre-compilation because there were no asset changes"
          end
      end
    end
  end

Вот еще один подход, основанный на возрасте активов (https://gist.github.com/2784462):

set :max_asset_age, 2 ## Set asset age in minutes to test modified date against.

after "deploy:finalize_update", "deploy:assets:determine_modified_assets", "deploy:assets:conditionally_precompile"

namespace :deploy do
  namespace :assets do

    desc "Figure out modified assets."
    task :determine_modified_assets, :roles => assets_role, :except => { :no_release => true } do
      set :updated_assets, capture("find #{latest_release}/app/assets -type d -name .git -prune -o -mmin -#{max_asset_age} -type f -print", :except => { :no_release => true }).split
    end

    desc "Remove callback for asset precompiling unless assets were updated in most recent git commit."
    task :conditionally_precompile, :roles => assets_role, :except => { :no_release => true } do
      if(updated_assets.empty?)
        callback = callbacks[:after].find{|c| c.source == "deploy:assets:precompile" }
        callbacks[:after].delete(callback)
        logger.info("Skipping asset precompiling, no updated assets.")
      else
        logger.info("#{updated_assets.length} updated assets. Will precompile.")
      end
    end

  end
end

Если вы предпочитаете предварительно компилировать свои ресурсы локально, вы можете использовать эту задачу:

namespace :deploy do
  namespace :assets do
    desc 'Run the precompile task locally and rsync with shared'
    task :precompile, :roles => :web, :except => { :no_release => true } do
      from = source.next_revision(current_revision)
      if releases.length <= 1 || capture("cd #{latest_release} && #{source.local.log(from)} vendor/assets/ app/assets/ | wc -l").to_i > 0
        %x{bundle exec rake assets:precompile}
        %x{rsync --recursive --times --rsh=ssh --compress --human-readable --progress public/assets #{user}@#{host}:#{shared_path}}
        %x{bundle exec rake assets:clean}
      else
        logger.info 'Skipping asset pre-compilation because there were no asset changes'
      end
    end
  end
end 

Еще один интересный подход может заключаться в использовании git hook. Например, вы можете добавить этот код в .git/hooks/pre-commit, который проверяет наличие различий в файлах активов и в конечном итоге прекомпилирует их и добавляет к текущему фиксации.

#!/bin/bash

# source rvm and .rvmrc if present
[ -s "$HOME/.rvm/scripts/rvm" ] && . "$HOME/.rvm/scripts/rvm"
[ -s "$PWD/.rvmrc" ] && . "$PWD/.rvmrc"

# precompile assets if any have been updated
if git diff-index --name-only HEAD | egrep '^app/assets' >/dev/null ; then
  echo 'Precompiling assets...'
  rake assets:precompile:all RAILS_ENV=production RAILS_GROUPS=assets
  git add public/assets/*
fi

Если вы решите использовать этот подход, вам, вероятно, потребуется изменить config/environments/development.rb добавление:

config.assets.prefix = '/assets_dev'

Итак, пока вы в разработке не будете обслуживать предварительно скомпилированные активы.

Ответ 2

Я только что написал камень для решения этой проблемы внутри Rails, называемый turbo-sprockets-rails3. Он ускоряет ваш assets:precompile путем перекомпиляции измененных файлов и только компиляции один раз для создания всех активов. Он работает из коробки для Capistrano, так как ваш каталог ресурсов разделяется между релизами.

Это гораздо более пуленепробиваемо, чем решения, использующие git log, так как мой патч анализирует источники ваших активов, даже если они исходят из драгоценного камня. Например, если вы обновите jquery-rails, будет обнаружено изменение для application.js, и только application.js будет перекомпилирована.

Обратите внимание, что я также пытаюсь объединить этот патч в Rails 4.0.0 и, возможно, Rails 3.2.9 (см. https://github.com/rails/sprockets-rails/pull/21). Но на данный момент было бы здорово, если бы вы могли помочь мне протестировать драгоценный камень turbo-sprockets-rails3 и сообщить мне, есть ли у вас какие-либо проблемы.

Ответ 3

Решение tommasop не работает, если включена кешированная копия, моя измененная версия:

task :precompile, :roles => :web, :except => { :no_release => true } do
  from = source.next_revision(current_revision)
  if capture("cd #{shared_path}/cached-copy && git diff #{from}.. --stat | grep 'app/assets' | wc -l").to_i > 0
    run %Q{cd #{latest_release} && #{rake} RAILS_ENV=#{Rubber.env} #{asset_env} assets:precompile:primary}
  else
    logger.info "Skipping asset pre-compilation because there were no asset changes"
  end
end

Ответ 4

Вы можете сэкономить усилия сервера для предварительной компиляции активов, выполнив то же (предварительно скомпилировав активы) в своей локальной системе. И просто перейдите на сервер.

from = source.next_revision(current_revision) rescue nil      
if from.nil? || capture("cd #{latest_release} && #{source.local.log(from)} vendor/assets/ app/assets/ | wc -l").to_i > 0
  ln_assets    
  run_locally "rake assets:precompile"
  run_locally "cd public; tar -zcvf assets.tar.gz assets"
  top.upload "public/assets.tar.gz", "#{shared_path}", :via => :scp
  run "cd #{shared_path}; tar -zxvf assets.tar.gz"
  run_locally "rm public/assets.tar.gz"    
else
  run "ln -s #{shared_path}/assets #{latest_release}/public/assets"
  logger.info "Skipping asset pre-compilation because there were no asset changes"
end

Ответ 5

Решение предложенное Бен Кертисом, не работает для меня, потому что я не копирую папку .git при развертывании (медленный и бесполезный)

set :scm, :git
set :deploy_via, :remote_cache
set :copy_exclude, ['.git']

Я использую следующий фрагмент, без load 'deploy/assets'

task :assets, :roles => :app do
  run <<-EOF
    cd #{release_path} &&
    rm -rf public/assets &&
    mkdir -p #{shared_path}/assets &&
    ln -s #{shared_path}/assets public/assets &&
    export FROM=`[ -f #{current_path}/REVISION ] && (cat #{current_path}/REVISION | perl -pe 's/$/../')` &&
    export TO=`cat #{release_path}/REVISION` &&
    echo ${FROM}${TO} &&
    cd #{shared_path}/cached-copy &&
    git log ${FROM}${TO} -- app/assets vendor/assets | wc -l | egrep '^0$' ||
    (
      echo "Recompiling assets" &&
      cd #{release_path} &&
      source .rvmrc &&
      RAILS_ENV=production bundle exec rake assets:precompile --trace
    )
  EOF
end

Ответ 6

Есть моменты, когда мне нужно принудительно препроксимировать прейк файл при развертывании исправления как можно скорее. Я использую следующий хак в качестве дополнения к другим ответам, чтобы выполнить эту работу.

callback = callbacks[:after].find{|c| c.source == "deploy:assets:precompile" }
callbacks[:after].delete(callback)
after 'deploy:update_code', 'deploy:assets:precompile' unless fetch(:skip_assets, false)

Этот script изменит привязку встроенного актива-прекомпиляции, поэтому он будет вызываться на основе параметра skip_assets. Я могу вызвать cap deploy -S skip_assets=true, чтобы полностью прекомпилировать предел.

Ответ 7

OP явно запросил Capistrano, но если вы развертываете без специального инструмента развертывания (через bash script, Ansible playbook или аналогичный), вы можете использовать следующие шаги для ускорения развертывания Rails:

  • Пропустить установку пакета
    bundle check возвращает 1, если есть драгоценные камни для установки (1 в противном случае), поэтому легко пропустить установку пакета, если это не необходимо.

  • Пропустить предварительную компиляцию ресурсов
    Используйте git rev-parse HEAD перед тем, как потянуть изменения и сохранить текущую версию SHA в переменной (скажем $previous_commit). Затем потяните изменения и выясните, изменились ли активы с помощью команды git diff --name-only $previous_commit HEAD | grep -E "(app|lib|vendor)/assets". Если это возвращает $1, вы можете безопасно пропустить предварительную компиляцию активов (если вы используете развертывания на основе релиза, вы можете скопировать свои активы в свою новую директорию выпуска).

  • Пропустить миграцию базы данных
    Если вы используете MySQL, используйте команду mysql --user=USER --password=PASSWORD --batch --skip-column-names --execute="USE MYAPP; SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 1;" из корневого каталога applcation, чтобы получить имя последней применяемой миграции. Сравните это с выводом команды ls db/migrate | tail -1 | cut -d '_' -f 1 (которая возвращает самую последнюю доступную миграцию). Если они отличаются, вам нужно мигрировать. Если нет, вы можете пропустить миграцию базы данных.

Разработчики Rails, развертывающие с помощью Ansible, могут еще больше сократить время их развертывания, выбирая факты, если они не нужны (gather_facts: no), и используют конвейерную обработку SSH (export ANSIBLE_SSH_PIPELINING=1).

Если вы хотите более подробно, я недавно написал статью об этой теме.