Как оптимизировать процессы 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).

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

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

Ресурсы

ссылка на оригинал статьи http://habrahabr.ru/post/206840/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *