Автоскейлинг node.js приложений с помощью PM2

Многие node.js приложения которые работают в продакшен запускают через процесс менеджер PM2. PM2 — это менеджер процессов с открытым исходным кодом позволяющий кластеризировать приложение и гибко распределять нагрузку между ядрами процессора. Не буду расписывать все удобства этого менеджера, напишу сразу о его небольшом недостатке и как решить эту проблему.


PM2 позволяет запускать нужное количество инстансов приложения которое вам необходимо. Например, у вас есть сервер на котором есть 4 ядра и вы можете кластеризировать ваше приложение и запустить его 4 копии которые позволят вам обрабатывать запросы от пользователей в параллель, что увеличит скорость их обработки.

Благодаря конфигурационному файлу, который поддерживает PM2 можно создавать конфиг для каждого вашего приложения. Например, вот такой конфиг для запуска приложения:

module.exports = {
    apps: [
        {
            name: 'app',
            script: 'build/app.js',
            instances: '4',
            autorestart: true,
            watch: false,
            max_memory_restart: '512M',
            vizion: false,
            exec_mode: 'cluster',
        },
    ],
};

Тут мы видим, что приложение запускается в режиме кластера и поднимает 4 инстанса (копии приложения). Приложение поднимается на одном порту\сокете и PM2 сам распределяет запросы между инстансами. Но потребление памяти будет 4х в данном случае, потому что каждый инстанс приложения будет активен. Если один инстанс у вас потребляет 100МБ оперативной памяти, то всего PM2 займет 400МБ только под ваше приложение.

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

Предположим вы делаете сервер с 8ми ядрами и 4Гб ОЗУ. Вы запускаете 4 процесса одного приложения и 4 процесса другого. И вроде бы все хорошо, нагрузка распределяется между 2 приложениями. Но, внезапно, одно ваше приложение начинает испытывать нагрузку больше чем другое, однако оно не выйдет за 4 доступных процесса. Вы заходите на сервер и пытаетесь увеличить количество процессов с помощью CLI команды в PM2.

pm2 scale app +3

И PM2 поднимает вам 3 новых инстанса. Но когда нагрузка спадет, вам снова придется зайти на сервер и уменьшить количество инстансов. Достаточно муторная схема, да и еще с ручным мониторингом. А теперь представьте у вас на одном сервере 10 приложений. Если у вас сервер с 48 ядрами, то можно запустить по 48 инстансов каждого приложения, но тогда у вас будет просто занято ~4800МБ оперативной памяти под каждое приложение (а если 10 приложений, то это почти 50ГБ) и вы не будете знать какое из них в ближайшее время будет под нагрузкой.  

Что бы решить эту проблему я написал плагин pm2-autoscale (https://www.npmjs.com/package/pm2-autoscale) для PM2 который позволяет запускать каждое приложение с минимальным количеством инстансов и которые потом будут автоматически увеличиваться по мере нагрузки на приложение. Установить плагин можно командой.

pm2 install pm2-autoscale

Он считывает текущую нагрузку на приложение, смотрит есть ли свободная память и увеличивает количество инстансов до количество CPU - 1, если это возможно. Когда нагрузка спадает, то он уменьшает количество инстансов и освобождает память. Например, пример ниже:

0|pm2-autoscale | App "app" has 1 worker(s). CPU: 0.
0|pm2-autoscale | App "app" has 1 worker(s). CPU: 17.
0|pm2-autoscale | App "app" has 1 worker(s). CPU: 29.
0|pm2-autoscale | INFO: Increase workers
0|pm2-autoscale | App "app" scaled with +1 worker
0|pm2-autoscale | App "app" has 2 worker(s). CPU: 26,34.
0|pm2-autoscale | App "app" has 2 worker(s). CPU: 29,43.
0|pm2-autoscale | App "app" has 2 worker(s). CPU: 28,38.
0|pm2-autoscale | INFO: Increase workers
0|pm2-autoscale | INFO: App "app" is busy
0|pm2-autoscale | App "app" scaled with +1 worker
0|pm2-autoscale | App "app" has 3 worker(s). CPU: 27,35,33.
0|pm2-autoscale | App "app" has 3 worker(s). CPU: 25,31,22.
0|pm2-autoscale | App "app" has 3 worker(s). CPU: 24,29,23.
0|pm2-autoscale | App "app" has 3 worker(s). CPU: 23,25,20.
0|pm2-autoscale | App "app" has 3 worker(s). CPU: 15,18,17.
0|pm2-autoscale | App "app" has 3 worker(s). CPU: 11,13,13.
0|pm2-autoscale | App "app" has 3 worker(s). CPU: 8,9,10.
0|pm2-autoscale | App "app" has 3 worker(s). CPU: 6,7,7.
0|pm2-autoscale | App "app" has 3 worker(s). CPU: 5,5,5.
0|pm2-autoscale | INFO: Decrease workers
0|pm2-autoscale | INFO: App "app" is busy
0|pm2-autoscale | App "app" decresed one worker
0|pm2-autoscale | App "app" has 2 worker(s). CPU: 4,4.
0|pm2-autoscale | App "app" has 2 worker(s). CPU: 3,3.
0|pm2-autoscale | App "app" has 2 worker(s). CPU: 2,2.
0|pm2-autoscale | INFO: Decrease workers
0|pm2-autoscale | INFO: App "app" is busy
0|pm2-autoscale | App "app" decresed one worker

У плагина есть несколько свойств конфигурации:

  • scale_cpu_threshold Максимальное значение загрузки CPU для приложения после которого плагин попытается увеличить количество инстансов. Если у вас 4 инстанса, то когда хотя бы один из них превысит это значение. (по умолчанию 30)
  • release_cpu_threshold Среднее значение суммы нагрузки всех инстансов приложения после которого количество инстансов будет уменьшаться.  (по умолчанию to 5)
  • debug Включение подробного лога для плагина (по умолчанию false)

Для изменения значений конфигурации плагина можно воспользоваться командами ниже:

pm2 set pm2-autoscale:debug true
pm2 set pm2-autoscale:scale_cpu_threshold 50

Благодаря этому плагину нам удалось в разы снизить ресурсы для сервера и правильно их распределить между приложениями.

Если у вас есть вопросы и предложения - пишите мне на GitHub https://github.com/VeXell