Cайт веб-разработчика, программиста Ruby on Rails ESV Corp. Екатеринбург, Москва, Санкт-Петербург, Новосибирск, Первоуральск

Настройка параллелизма Phusion Passenger

Каково оптимальное значение для PassengerMaxPoolSize? Это нетривиальный вопрос, и ответ охватывает больше, чем просто PassengerMaxPoolSize. В данной статье описаны способы вычисления оптимальных значений для настроек.

Для простоты предполагаем, что Ваш сервер обслуживает только одно веб-приложение. Рассчеты становятся более сложными, когда на сервере работает больше веб-приложений, но Вы можете использовать принципы, описанные в этой статье, чтобы применить их для рассчетов к мультисредам серверов приложений.

Аспекты настройки параллелизма

Цель настройки состоит в том, чтобы обычно максимизировать пропускную способность. Увеличение числа процессов или потоков увеличивает максимальную пропускную способность и параллелизм, но есть несколько факторов, которые должны быть учтены.

  • Память. Большое число процессов подразумевает более высокое использование памяти. Если слишком много памяти будет использоваться, то будет включен механизм подкачки, который значительно замедляет производительность системы вцелом. У Вас должно быть только столько процессов, чтобы они помещались в оперативной памяти. Потоки используют меньше памяти, поэтому они более предпочтительны, если это возможно. Вы можете создать десятки потоков вместо одного процесса.
  • Число процессоров. Аппаратно число параллельных процессов не может быть больше, чем число центральных процессоров. В теории, если все процессы/потоки в Вашей системе постоянно используют центральные процессоры, то:

    Наличие большего количества процессов, чем центральных процессоров может немного уменьшить общую пропускную способность благодаря издержкам контекстного переключения, но различие не большое, потому что ОС (операционная система) способна к контекстному переключению в настоящее время.

    С другой стороны, если Ваши центральные процессоры не используются постоянно, например, потому что они часто блокируются на вводе-выводе, тогда увеличение числа процессов/потоков действительно увеличивает параллелизм и пропускную способность, по крайней мере пока центральные процессоры не загружены полностью.
    • Вы можете увеличить пропускную до числа процессоров.
    • При увеличении числа процессов/потоков более числа процессоров в системе, увеличится виртуальный параллелизм, но не будет увеличен аппаратный параллелизм, и не будет увеличена максимальная пропускная способность.
  • Блокирующий ввод-вывод. Это охватывает весь ввод-вывод, включая задержки доступа жесткого диска, задержки вызова базы данных, веб-вызовы API, и т.д. Обработка ввода от клиента и вывода клиенту не рассматривается как блокирующий ввод-вывод, потому что у Phusion Passenger есть уровень буферизации.

    Чем больше вызовов блокирующих вводов-выводов, которые выполняет Ваш процесс/поток приложения, тем больше времени он тратит на ожидание внешних компонентов. В то время как процесс/поток ожидает окончания процесса ввода-вывода, он не использует ЦП, поэтому именно тогда другой процесс/поток должен получить шанс использовать ЦП. Если никакой другой процесс/поток не нуждается в ЦП прямо сейчас (например, все процессы/потоки ожидают ввода-вывода), тогда, процессорное время по существу тратится впустую. Увеличение числа процессов или потоков уменьшает шанс потраченного впустую процессорного времени.

Настройка количества процессов и потоков приложения

В нашем примере мы предполагаем типичный однопоточный процесс приложения Ruby on Rails, который использует 100 Мбайт RAM на 64-разрядной машине, и в отличие от этого, поток использовал бы только 10%. Мы используем этот факт в определении надлежащей формулы.

Шаг 1: Определение ограничений системы

Во-первых, давайте определим максимальное количество (однопоточных) процессов или число потоков, разумный верхний предел, которого Вы можете достигнуть, не ухудшая производительность системы. Мы используем следующие формулы.

В чисто однопоточном многопроцессном варианте формула следующая:

max_app_processes = (TOTAL_RAM * 0.75) / RAM_PER_PROCESS

, где
(TOTAL_RAM * 0.75): Мы можем предположить, что должно быть по крайней мере 25% свободной RAM, которую операционная система может использовать для других вещей. Результат этого вычисления - RAM, которая в свободном доступе для приложений.
/ RAM_PER_PROCESS: Каждый процесс использует примерно постоянную сумму RAM, таким образом, максимальное количество процессов - единственный делитель.

В многопоточном процессе:

max_app_threads_per_process =
  ((TOTAL_RAM * 0.75) - (NUMBER_OF_PROCESSES * RAM_PER_PROCESS * 0.9)) /
  (RAM_PER_PROCESS / 10)

здесь, NUMBER_OF_PROCESSES - число процессов приложения, которое Вы хотите использовать. В случае Ruby, это должно быть равно NUMBER_OF_CPUS (число процессоров в системе). Это вызвано тем, что у Ruby есть Глобальная Блокировка Интерпретатора так, что невозможно использовать многопоточность независимо от того, сколько потоков использовано внутри приложения. При помощи множества процессов Вы можете использовать многопоточность.

/ (RAM_PER_PROCESS / 10): Поток использует приблизительно 10% объема памяти процесса, таким образом, мы делим сумму RAM, доступной потокам. То, что мы получаем, является числом потоков, которые может обработать система.

В 32-разрядных системах max_app_threads_per_process не должен быть выше, чем приблизительно 200. Принимая размер стека на 8 Мбайт за поток, Вы исчерпаете виртуальное адресное пространство, если Вы пойдете намного далее. В 64-разрядных системах Вы не должны волноваться об этой проблеме.

Шаг 2: Определение потребности приложения

Показанные ранее две формулы позволяют вычислить, сколько система может обработать максимально. Вашему приложению, возможно, фактически не понадобится столько много процессов или потоков. Если Ваше приложение ограничено ЦП, то Вы только нуждаетесь в производительном центральном процессоре. Только если Ваше приложение выполняет много операций блокированного ввода-вывода (например, вызовы базы данных, которые берут десятки миллисекунд, чтобы завершиться), Вы нуждаетесь в большом количестве процессов или потоков.

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

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

# Use this formula for purely single-threaded multi-process deployments.
desired_app_processes = max_app_processes

# Use this formula for multithreaded deployments.
desired_app_threads_per_process = max_app_threads_per_process

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

# Use this formula for purely single-threaded multi-process deployments.
desired_app_processes = min(max_app_processes, NUMBER_OF_CPUS)

# Use this formula for multithreaded deployments.
desired_app_threads_per_process =
  min(max_app_threads_per_process, 2 * NUMBER_OF_CPUS)

 

Шаг 3: Конфигурация Phusion Passenger

Вы должны поместить число desired_app_processes в опцию PassengerMaxPoolSize. Хотите ли Вы сделать PassengerMinInstances равным тому числу или нет, ваше дело: одинаковое значение этих параметров сделает число из процессов статичным, независимо от трафика. Если у Вашего приложения есть пиковые скачки трафика, время отклика возрастает, в то время как Phusion Passenger запускает новые процессы, чтобы обработать пиковый трафик. Установка PassengerMinInstances в максимальное возможное значение предотвращает эту проблему.

Если desired_app_processes равняется 1, то Вы должны установить PassengerSpawnMethod conservative (на Phusion Passenger 3 или ранее) или PassengerSpawnMethod direct (на Phusion Passenger 4 или позже). При помощи conservative/direct запуска вместо smart Phusion Passenger не будет запускать процесс ApplicationSpawner/Preloader. Это связано с тем, что процесс ApplicationSpawner/Preloader бесполезен, когда есть только 1 процесс приложения.

Возможный шаг 4: Конфигурация Rails

Только для использования многопоточности Вы должны сконфигурировать Rails.

Rails ориентированы на многопотоковое исполнение начиная с версии 2.2, но Вы должны включить потокобезопасность, устанавливая конфигурацию thread_safe! в config/environments/production.rb.

Вы должны также увеличить размер пула ActiveRecord, потому что он ограничивает параллелизм. Вы можете сконфигурировать его в config/database.yml. Установите размер пула в число потоков, но только если Вы уверены, что Ваша база данных может обработать так много запросов.

Примеры

Пример 1: чисто однопоточное многопроцессное приложение с большим количеством операций блокированного ввода-вывода

Предположим, у нас в системе 1Gb RAM.

# Use this formula for purely single-threaded multi-process deployments.
max_app_processes = (1024 * 0.75) / 100 = 7.68
desired_app_processes = max_app_processes = 7.68

Вывод: Вы должны использовать 7 или 8 процессов. Phusion Passenger должен быть сконфигурирован следующим образом:

PassengerMaxPoolSize 7

Однако, параллелизм 7 или 8 слишком низок, если Ваше приложение выполняет большое количество операций блокирующего ввода-вывода. Вы должны использовать многопоточное развертывание вместо этого, или Вы должны увеличить RAM, таким образом, Вы можете выполнить больше процессов.

Пример 2: многопоточное приложение с большим количеством операций блокированного ввода-вывода

В этом примере: 1Gb RAM, 4 CPU. Предположим, что будут запущены 2 приложения. Сколько потоков может быть в каждом приложении?

# Use this formula for multithreaded deployments.
max_app_threads_per_process
= ((1024 * 0.75) - (4 * 100)) / (100 / 10)
= 368 / 10
= 36.8

Вывод: Вы должны использовать 4 процесса, каждый с 36-37 потоками. Конфигурация будет выглядеть следующим образом:


PassengerMaxPoolSize 4
PassengerConcurrencyModel thread
PassengerThreadCount 36