Поиск по сайту:

Как оптимизировать Unicorn Workers в приложении Ruby on Rails


Представляем Единорога

Если вы разработчик Rails, вы, вероятно, слышали о Unicorn, HTTP-сервере, который может обрабатывать несколько запросов одновременно.

Unicorn использует разветвленные процессы для достижения параллелизма. Поскольку разветвленные процессы по существу являются копиями друг друга, это означает, что приложение Rails не обязательно должно быть потокобезопасным.

Это здорово, потому что трудно гарантировать, что наш собственный код является потокобезопасным. Если мы не можем гарантировать, что наш код является потокобезопасным, то о параллельных веб-серверах, таких как Rubinius, не может быть и речи.

Таким образом, Unicorn предоставляет нашим приложениям Rails параллелизм, даже если они не являются потокобезопасными. Однако за это приходится платить. Приложения Rails, работающие на Unicorn, обычно потребляют гораздо больше памяти. Не обращая внимания на потребление памяти вашим приложением, вы вполне можете оказаться с перегруженным облачным сервером.

В этой статье мы рассмотрим несколько способов использования параллелизма Unicorn, в то же время контролируя потребление памяти.

Используйте Руби 2.0!

Если вы используете Ruby 1.9, вам следует серьезно подумать о переходе на Ruby 2.0. Чтобы понять почему, нам нужно немного разобраться в разветвлении.

Разветвление и копирование при записи (CoW)

Когда дочерний процесс разветвляется, это точно такая же копия, как и родительский процесс. Однако копировать реальную физическую память не обязательно. Поскольку они являются точными копиями, и дочерние процессы могут совместно использовать одну и ту же физическую память. Только при выполнении записи мы копируем дочерний процесс в физическую память.

Итак, как это связано с Ruby 1.9/2.0 и Unicorn?

Напомним, Unicorn использует разветвление. Теоретически операционная система сможет использовать преимущества CoW. К сожалению, в Ruby 1.9 это невозможно. Точнее, реализация сборки мусора в Ruby 1.9 не делает этого возможным. Крайне упрощенная версия такова — когда срабатывает сборщик мусора Ruby 1.9, запись должна была быть сделана, что делало CoW бесполезным.

Не вдаваясь в подробности, достаточно сказать, что сборщик мусора Ruby 2.0 исправляет это, и теперь мы можем эксплуатировать CoW.

Настройка конфигурации Unicorn

Есть несколько параметров, которые мы можем настроить в config/unicorn.rb, чтобы выжать из Unicorn как можно больше производительности.

рабочие_процессы

Это устанавливает количество рабочих процессов для запуска. Важно знать, сколько памяти занимает один процесс. Это для того, чтобы вы могли смело планировать количество воркеров, чтобы не исчерпать оперативную память вашего VPS.

тайм-аут

Это должно быть небольшое число: обычно от 15 до 30 секунд является разумным числом. Этот параметр задает количество времени до истечения времени ожидания работника. Причина, по которой вы хотите установить относительно низкое число, состоит в том, чтобы не допустить, чтобы долго выполняющийся запрос задерживал обработку других запросов.

preload_app

Для этого должно быть установлено значение true. Установка для этого параметра значения true сокращает время запуска рабочих процессов Unicorn. Это использует CoW для предварительной загрузки приложения перед разветвлением других рабочих процессов. Однако есть большой подвох. Мы должны уделять особое внимание правильному закрытию и открытию любых сокетов (например, соединений с базой данных). Мы делаем это с помощью before_fork и after_fork.

Вот пример:

before_fork do |server, worker|
  # Disconnect since the database connection will not carry over
  if defined? ActiveRecord::Base
    ActiveRecord::Base.connection.disconnect!
  end
  
  if defined?(Resque)
    Resque.redis.quit
    Rails.logger.info('Disconnected from Redis')
  end
end

after_fork do |server, worker|
  # Start up the database connection again in the worker
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.establish_connection
  end
  
  if defined?(Resque)
    Resque.redis = ENV['REDIS_URI']
    Rails.logger.info('Connected to Redis')
  end
end

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

Приручение вашего Unicorn Workers потребления памяти

Очевидно, что это не все радуги и единороги (каламбур!). Если в вашем приложении Rails есть утечка памяти, Unicorn усугубит ситуацию.

Каждый из этих разветвленных процессов потребляет память, поскольку они являются копиями приложения Rails. Следовательно, хотя наличие большего количества рабочих процессов означает, что наше приложение может обрабатывать больше входящих запросов, мы ограничены объемом физической оперативной памяти, имеющейся в нашей системе.

Приложение Rails легко может утечь память. Даже если нам удастся устранить все утечки памяти, нам все равно придется бороться с далеко не идеальным сборщиком мусора (я имею в виду реализацию MRI).

Выше показано приложение Rails, работающее с Unicorn, с утечками памяти.

Со временем потребление памяти будет продолжать расти. Использование нескольких воркеров Unicorn просто ускорит скорость использования памяти до такой степени, что свободной оперативной памяти больше не будет. Затем приложение останавливалось, что приводило к ордам недовольных пользователей и клиентов.

Важно отметить, что это не вина Unicorn. Однако это проблема, с которой вы рано или поздно столкнетесь.

Введите убийцу рабочих-единорогов

Одно из самых простых решений, с которыми я столкнулся, — это гем unicorn-worker-killer.

Из README:

Гем unicorn-worker-killer обеспечивает автоматический перезапуск воркеров Unicorn на основе

  1. максимальное количество запросов и
  2. объем памяти процесса (RSS), не влияя на какие-либо запросы.

Это значительно улучшит стабильность сайта, избегая неожиданного исчерпания памяти в узлах приложений.

Обратите внимание, что я предполагаю, что у вас уже настроен и работает Unicorn.

Шаг 1:

Добавьте unicorn-worker-killer в свой Gemfile. Поместите это под драгоценным камнем единорога.

group :production do 
  gem 'unicorn'
  gem 'unicorn-worker-killer'
end

Шаг 2:

Запустите установка пакета.

Шаг 3:

А вот и самое интересное. Найдите и откройте файл config.ru.

# --- Start of unicorn worker killer code ---

if ENV['RAILS_ENV'] == 'production' 
  require 'unicorn/worker_killer'

  max_request_min =  500
  max_request_max =  600

  # Max requests per worker
  use Unicorn::WorkerKiller::MaxRequests, max_request_min, max_request_max

  oom_min = (240) * (1024**2)
  oom_max = (260) * (1024**2)

  # Max memory size (RSS) per worker
  use Unicorn::WorkerKiller::Oom, oom_min, oom_max
end

# --- End of unicorn worker killer code ---

require ::File.expand_path('../config/environment',  __FILE__)
run YourApp::Application

Во-первых, мы проверяем, что находимся в среде production. Если это так, мы продолжим и выполним следующий код.

unicorn-worker-killer убивает рабочих при двух условиях: максимальное количество запросов и максимальное количество памяти.

Максимальное количество запросов

В этом примере рабочий процесс уничтожается, если он обработал от 500 до 600 запросов. Обратите внимание, что это диапазон. Это сводит к минимуму случаи, когда более 1 работника завершается одновременно.

Макс. память

Здесь рабочий процесс уничтожается, если он использует от 240 до 260 МБ памяти. Это диапазон по той же причине, что и выше.

Каждое приложение имеет уникальные требования к памяти. У вас должен быть грубый показатель потребления памяти вашим приложением во время нормальной работы. Таким образом, вы могли бы дать более точную оценку минимального и максимального потребления памяти вашими рабочими процессами.

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

Обратите внимание на перегибы на графике. Это жемчужина делает свою работу!

Заключение

Unicorn предоставляет вашему приложению Rails безболезненный способ достижения параллелизма, независимо от того, является оно потокобезопасным или нет. Однако это связано с увеличением потребления оперативной памяти. Балансировка потребления оперативной памяти абсолютно необходима для стабильности и производительности вашего приложения.

Мы рассмотрели 3 способа настроить ваших воркеров Unicorn для максимальной производительности:

  1. Использование Ruby 2.0 дает нам значительно улучшенный сборщик мусора, который позволяет нам использовать семантику копирования при записи.
  2. Настройка различных параметров конфигурации в config/unicorn.rb.
  3. Использование unicorn-worker-killer для изящного решения проблемы уничтожения и перезапуска рабочих процессов, когда они становятся слишком раздутыми.

Ресурсы

  • Замечательное объяснение того, как работает сборщик мусора Ruby 2.0 и семантика копирования при записи.
  • Полный список параметров конфигурации Unicorn.

Прислал: Бенджамин Тан