Пользователь
0,0
рейтинг
20 декабря 2013 в 19:56

Разработка → Как оптимизировать процессы 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 для решения проблемы остановки воркеров, когда они становятся слишком раздутыми.


Ресурсы


Перевод: Benjamin Tan
@rsludge
карма
36,7
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (24)

  • +1
    А мы перешли на puma и забыли unicorn как страшный сон.
    • +1
      вы используете Пуму на MRI? Расскажите про преимущества по сравнению с unicorn, с какими трудностями столкнулись при переходе?
      • +2
        Когда в последний раз тестировал: при 8 потоках пума использовала в 10 раз меньше памяти чем юникорн (при тех же 8 потоках) и обрабатывала в полтора раза больше запросов.
        Тестировал на амазоновском сервере с помощью ab, уже не помню конфигурацию, было что-то около 4-х ядер и 4гб памяти.
        Трудностей вообще никаких, конфигурируется она сильно проще чем юникорн.
        • +1
          Я не в курсе, но может вы знаете ответ. Вот у юникорна при деплое воркеры заменяются на новые постепенно, в итоге, сайт непрерывно продолжает работать. А у puma с этим как? Там есть такое, или ставится заглушка, что сайт обновляется?
          • 0
            У puma это есть, но вот стабильностью «phased-restart» похвастаться пока не может. Самый частый случай — пришел новый гем и puma просто падает при сигнале рестарта :)
        • 0
          Тут кстати логичный вопрос. У уникорна нет потоков, только воркеры, в то время как у пумы — потоки + кластер режим, что означает тоже самое, что и у юникорна. Тоесть вы сравнивали производительность 8 workers unicorns vs 8 puma cluster mode без тредов внутри каждого? Поскольку только так тестирование будет приближено к реальности тестирования самого веб сервера. Ведь puma выиграет, поскольку из коробки один демон создает 16 тредов, что уже дает фору (но и создает проблемы в MRI).
  • +2
    Если честно вообще не понимаю почему кто-то пользуется Unicornом, когда есть Puma & Thin & Rainbows!, может кто-то сказать о каких либо плюсах Unicornа?
    • 0
      zero downtime restart?

      Ну и в nginx'е нужно писать всего одну строчку на все воркеры юникорна, а не по одной на каждой :)
      • +1
        Nginx:
        upstream prodpuma {
          server unix:/.../shared/tmp/sockets/puma.sock fail_timeout=0;
        }
        


        Puma:
        bind 'unix:///.../shared/tmp/sockets/puma.sock'
        state_path '/.../shared/tmp/sockets/puma.state'
        


        Zero downtime:
        desc 'Restart puma (phased restart)'
        task :phased_restart, :roles => lambda { puma_role }, :on_no_matching_servers => :continue do
          run "cd #{current_path} && #{pumactl_cmd} -S #{state_path} phased-restart"
        end
        
        • 0
          Ну я thin'ом сравнивал, puma не пробовал пока
    • 0
      Да, кстати, если уж и сравнивать, то unicorn и rainbows!, то последний основан на первом. Основное отличие — разные задачи использования. Задача unicorn — обслуживание быстрых запросов, в то время как для rainbows! — долго «живучих» запросов. Поэтому первый хорош для простых сайтов с запрос-ответ (тоесть почти для всех веб сайтов), а второй для обслуживание websocket или commet соединений.
  • –1
    Использую связку 1 Nginx + 20 Thin. Версия руби 1.8.7 и вполне доволен скорость работы. Памяти используется очень мало.

    Unicorn устанавливал один раз. После его запуска был поражен сколько памяти он «съел» по сравнению с Thin. Автору может быть присмотреться к другим веб-серверам? Чем уж так хорош Unicorn? Или просто лень переучиваться?
    • 0
      Это ж как вы забили на технический долг, раз используете 1.8.7, на который уже даже security обновления не выходят :)
      • 0
        Вы не правы.
        Цитирую новость с официального сайта Руби:
        Новость от 17 декабря 2013 года — версии Ruby 1.8.7 и 1.9.2 будут поддерживаться патчами для безопасности до июня 2014 г.
        • 0
          Ну главное что бы этого времени хватило мигрировать.
  • +2
    А почему passenger не обсуждается в комментариях?
    • +2
      А что вы хотите обсудить по поводу passenger'а?
      • 0
        Например, почему бы его не использовать в продакшене как альтернативу вышеупомянутым thin, unicorn и недоделанном puma?
        • +1
          А почему puma вдруг недоделанный?
  • 0
    Т.к. созданные (форкнутые) процессы являются копиями друг друга, это значит, что rails-приложение должно быть потокобезопасным.
    Неправда.
    • 0
      Подтверждаю. Форк и тред — это разные вещи.
  • 0
    Странно, но мы всегда находили и исправляли утечку памяти. Вариант unicorn-worker-killer уже если вообще команда не может с этим справится (хотя мы monit для этого использовали, что бы следить за памятью — он сразу и писал письмо, что воркер пришлось перегрузить). Но это точно не решает проблему с утечками.
    • –1
      кроме утечек есть же еще и bloating
      • 0
        Это не поводу убивать процесс — там может выполнятся важная задача (например, идет работа с платежом от клиента). Если все написано без особой магии, то Ruby GC почистить обьекты и память вернется на место.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.