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

в 15:56, , рубрики: garbage collector, ruby, ruby on rails, unicorn, unicorn-worker-killer

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

Для обеспечения параллельности Unicorn использует создание множества процессов. Т.к. созданные (форкнутые) процессы являются копиями друг друга, это значит, что rails-приложение должно быть потокобезопасным.

Это здорово, т.к. нам тяжело быть уверенными, что наш код является потокобезопасным. Если мы не можем быть уверены в этом, то ни о параллельных веб-серверах, таких как Puma, ни даже об альтернативных реализациях Ruby, реализующих параллелизм, таких как JRuby и Rubinius, не может быть и речи.

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

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

Используйте Ruby 2.0!

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

Создание процессов и Copy-on-Write

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

Как все это относится к Ruby 1.9/2.0 и Unicorn’у?

Напоминаю, что Unicorn использует форки. В теории операционная система сможет использовать Copy-on-Write. К сожалению Ruby 1.9 делает это невозможным. Если быть точнее, реализация сборщика мусора в Ruby 1.9 делает это невозможным. В упрощенной версии это выглядит так — когда срабатывает сборщик мусора в 1.9, происходит запись, что делает Copy-on-Write бесполезным.

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

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

Вот несколько настроек, которые мы можем задать в config/unicorn.rb, чтобы выжать из Unicorn максимальную производительность.
worker_processes
Задает количество запускаемых порцессов. Важно знать, сколько памяти занимает один процесс. Это нужно, чтобы вы могли запустить нужное количество воркеров, не опасаясь перегрузить оперативную память вашего VPS.
timeout
Должен быть задан небольшим числом: обычно от 15 до 30 секунд является подходящим. Относительно небольшое значение задается, чтобы длительные по времени запросы не задерживали обработку других запросов.
preload_app
Должно быть выставлено в true — это уменьшает время запуска воркера. Благодаря Cope-on-Write приложение грузится до запуска остальных воркеров. Однако здесь есть важный нюанс. Мы должны убедиться, что все сокеты (включая подключения к базе данных) корректно закрыты и открыты заново. Мы сделаем это, используя 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

Очевидно, вокруг не только радуги да единороги. (тут был авторский каламбур ‘rainbows and unicorns’ — прим. переводчика). Если в вашем Rails-приложении есть утечки памяти, Unicorn сделает все еще хуже.

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

Утечки памяти в rails-приложении возникают очень просто. Но даже если нам удастся “заткнуть” все утечки памяти, все еще придется иметь дело со слегка неидеальным сборщиком мусора (я имею в виду реализацию в MRI).
Как оптимизировать процессы Unicorn в Ruby on Rails приложении
Изображение выше показывает rails-приложение с утечками памяти, запущенное Unicorn’ом.

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

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

Встречайте Unicorn Worker Killer

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

гем unicorn-worker-killer позволяет автоматически перезапускать воркеры Unicorn на основе:
1) максимального количества запросов и
2) размера памяти, занимаемой процессом (RSS), не обрабатывающим запрос.
Это сильно увеличит стабильность сайта, позволив избежать неожиданных нехваток памяти в узлах приложения.

Обратите внимание, что я предполагаю, что у вас уже есть установленный и запущенный Unicorn.
Шаг 1:
Добавьте unicorn-worker-killer в ваш Gemfile ниже, чем unicorn.

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

Шаг 2:
Запустите bundle install.
Шаг 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 запросов. Заметьте, что используется интервал. Это сводит к минимуму ситуации, когда более, чем один воркер останавливается одновременно.
  • максимальная потребляемая память. Здесь воркер убивается, если он занимает от 240 до 260 MB памяти. Интервал здесь нужен по той же причине, что и выше.

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

Если во время развертывания вашего приложения вы сконфигурировали все корректно, вы заметите гораздо менее неустойчивое поведение памяти:
Как оптимизировать процессы Unicorn в Ruby on Rails приложении
Обратите внимание на перегибы в графике — это гем делает свою работу!

Заключение

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

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

Ресурсы

Автор: rsludge

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js