Оптимизация рабочих процессов Unicorn в приложении Ruby on Rails

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

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

Это удобно, поскольку потокобезопасность собственного кода трудно гарантировать. А если нет гарантии потокобезопасности приложения, то нет и возможности использовать такие веб-серверы, как Puma и альтернативные реализации Ruby (например, JRuby и Rubinius).

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

Это руководство рассматривает несколько способов контроля потребления памяти при применении параллелизма.

Ветвление и копирование при записи

Примечание: Копирование при записи (Copy-on-Write, или CoW) – это механизм оптимизации процессов, который подразумевает создание реальной копии данных только когда система обращается к копируемой области данных с целью записи.

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

Ruby 1.9/2.0 и Unicorn

Теоретически, благодаря тому, что Unicorn применяет разветвление процессов, система сможет использовать механизм CoW. К сожалению, Ruby 1.9 (а точнее, операция «сборки мусора») исключает эту возможность. Проще говоря, при «сборке мусора» система создаст запись, и механизм CoW будет бесполезен.

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

Тонкая настройка Unicorn

В файле config/unicorn.rb содержится несколько настроек, отладка которых позволяет сделать сервер Unicorn максимально производительным.

Директива worker_processes

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

Директива timeout

Эта опция должна содержать по возможности  наименьшее значение (как правило, от 15 до 30 секунд). Это позволит предотвратить задержку обработки запросов из-за обработки длительного запроса.

Директива preload_app

Эта директива должна иметь значение 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

Конечно, всё далеко не так просто. Если Rails-приложение подвержено утечкам памяти, сервер Unicorn только ухудшит положение вещей.

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

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

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

Настройка unicorn-worker-killer

Одним из простейших способов решения этой проблемы является gem unicorn-worker-killer.

Как говорится в README:

unicorn-worker-killer gem provides automatic restart of Unicorn workers based on
1) max number of requests, and
2) process memory size (RSS), without affecting any requests.
This will greatly improve the site's stability by avoiding unexpected memory exhaustion at the application nodes.

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

Добавьте unicorn-worker-killer в Gemfile, поместив его после unicorn:

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

Запустите команду:

bundle install

Затем откройте файл 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

Убедитесь, что находитесь в окружении производства. Затем выполните приведённый код.

unicorn-worker-killer останавливает рабочие процессы согласно двум условиям: Max requests и Max memory.

  • Max requests: В данном примере рабочий процесс останавливается после того как он обработал от 500 до 600 запросов. Обратите внимание: подразумевается именно диапазон обработанных запросов, что позволяет свести к минимуму возможность одновременного прерывания нескольких рабочих процессов.
  • Max Memory: Данная опция останавливает процесс, потребляющий от 240 до 260 MB памяти. Значение задаётся в виде диапазона по той же причине.

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

Обратите внимание на потребление памяти после развёртывания приложения: если всё настроено должным образом, память не будет расходоваться очень быстро.

Заключение

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

Теперь вы знаете несколько способов отладить потребление памяти рабочими процессами Unicorn:

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

 Дополнительные ссылки

Найти подробное объяснение работы сборщика мусора Ruby и copy-on-write можно здесь.

Полный список опций Unicorn – по этой ссылке.

Tags: , , ,

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