Буквально пару часов назад у меня завязался спор на тему того, что Node.JS слишком медленная для крупных проектов и ей стоит предпочесть Golang, Rust, PHP, etc. Основным аргументом противоположной стороны в этом споре был факт однопоточности JavaScript. Якобы при разработке приложения производительность просто упрётся в эту однопоточность и ничего сделать уже нельзя — только переписать на каком-то другом языке. Однако дела с этим в NodeJS обстоят немного лучше, чем кажется на первый взгляд. Перед тем, как мы углубимся в эту тему хочу заявить, что уважаю право каждого разработчика использовать тот язык программирования, который пришёлся ему по душе и который он считает предпочтительным в той или иной задаче.

Сделав поиск по ключевому слову «PM2» на Хабре я не нашёл ни одной статьи, посвящённой этому process-менеджеру. Лишь одиночные упоминания в статьях других пользователей. Я загорелся (сильно сказано) идеей наверстать упущенное и пролить свет на этот тёмный уголок разработки backend на Node.JS (о котором многие знают, да, я в курсе). Всех заинтересовавшихся прошу под кат.



Пару слов о самом PM2



PM2 — это менеджер процессов с открытым исходным кодом, распространяющийся под лицензией AGPL-3.0. В момент написания статьи имеет ~350k загрузок в неделю, согласно данным NPM. В основном применяется в средах, где необходимо запустить приложение на NodeJS и забыть о нём (с остальными языками тоже можно использовать, но об этом позднее), позволяющий кластеризировать приложение и гибко распределять нагрузку между ядрами процессора. Небольшая вырезка из репозитория PM2 на GitHub:

PM2 is a production process manager for Node.js applications with a built-in load balancer. It allows you to keep applications alive forever, to reload them without downtime and to facilitate common system admin tasks.


Многие новички при разработке сталкиваются с проблемой, когда после «выкатки» приложения на production сервер, не знают как запустить его «навечно». Пишут в SSH-консоли set NODE_ENV=production && node app.js, всё отлично, приложение работает. Закрывают консоль и приложение больше не работает. Вопрос на StackOverflow — How to run node.js application permanently? набрал более 237 тыс. просмотров за всё время.

PM2 решает эту проблему одной командой:

pm2 start app.js


Эта команда «демонизирует» (от англ. «daemonize») процесс NodeJS, следит за потреблением им памяти и считает нагрузку на процессор.

Вернёмся к нашим баранам



С ростом нагрузки на backend возникает необходимость его масштабирования — как вертикального, так и горизонтального — кому что удобнее в сложившихся обстоятельствах. Как мы знаем, один процесс может использовать несколько ядер процессора, но только в том случае, если внутри процесса имеется несколько потоков. В NodeJS приложениях поток — один. PM2 способен выручить в этой ситуации и распределить нагрузку между несколькими ядрами процессора. По-прежнему всего с одной командой:

pm2 start app.js -i max


В данном случае параметр max соответствует количеству ядер процессора. Т.е. для 8-ядерного процессора будет создано 8 отдельных процессов. Можно также вместо max задать значение -1 и тогда количество процессов будет соответствовать количество_ядер минус 1. Вся прелесть заключается в том, что и HTTP(S)/Websocket/TCP/UDP соединения будут равномерно распределены между этими процессами. Ну чем не горизонтальное масштабирование? Почитать подробнее о кластеризации в PM2 можно по ссылке — PM2 Cluster Mode.

cluster_mode

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

Бережное отношение к памяти


При разработке на PHP я однажды столкнулся с проблемой. По неопытности неосознанно заложил в движок системы баг, из-за которого при определённых условиях процессы начинали поедать слишком много оперативной памяти. Вдобавок к этому нагружался процессор, из-за чего виртуальная машина просто зависла и у меня не было к ней доступа совсем.
Как знают PHP-разработчики, в PHP-FPM можно задать тип распределения процессов (если вы вдруг не знали, то в PHP-FPM для каждого нового запроса создаётся новый процесс) — статический, когда задаётся минимальный и максимальный порог, и динамический — выделение сколь угодно большого количества процессов, по необходимости. Что будет в PM2, если запустить 8 процессов и все они начнут потреблять много памяти? И эту проблему PM2 в состоянии решить — лишь одним параметром в командной строке:

# Set memory threshold for app reload
pm2 start app.js -i max --max-memory-restart <200MB>


Каждый раз при достижении лимита по памяти PM2 автоматически перезапустит процесс. Распределять память проще чем процессы, не так ли? 8 процессов * 200 мегабайт = 1,6 гигабайт. Математика уровня второго класса.

Помимо перезапуска процесса можно также настроить и перезапуск через N интервал времени. Я пока не придумал в каких случаях это может пригодиться, но не стесняйтесь указать мне на пару примеров в комментариях :)

А если я перезагружу виртуальную машину?


Сюрприз-сюрприз! Эту проблему PM2 тоже решает за вас. Всё ещё не более чем одной единственной командой в консоли:

pm2 startup


PM2 сгенерирует скрипт, который будет поднимать все необходимые процессы при запуске операционной системы. Однако здесь стоит быть бдительным — при обновлении версии Node.JS всё может сломаться. Во избежание этого, после успешного обновления до новой версии Node.JS выполните pm2 unstartup и pm2 startup. Подробнее об этом можно ознакомиться по ссылке — PM2 Startup Script Generator.

А надо ли перезапускать кластеры вручную при внесении изменений?


Конечно же нет! Ну, точнее, вы, конечно, можете перезапускать приложение вручную, но зачем? Автоматизируйте всё что можете и да прибудет с вами сила!

pm2 start env.js --watch --ignore-watch="node_modules"


Вы можете пользоваться этим при слиянии master-ветки в локальном репозитории с master-веткой из удалённого репозитория. В моём сайд-проекте это делается просто — git pull origin master && npm run build. При изменении файлов в папках server/build и client/build процессы будут автоматически перезапущены. Я понимаю, это очень простенькая фича и она не заслуживает даже быть упомянутой в этом тексте. Разбавлю его кое-чем серьёзным и напишу о том, что если вы пользуетесь кластеризацией, то все процессы будут перезагружены поочерёдно. Да так, что как минимум один из них будет всегда доступен. Это же zero-downtime deployment!

А можно и не перезапускать процессы. Для этого есть reload (нечто похожее на nginx reload):

pm2 reload all


Слишком много команд! И вообще я предпочитаю конфиги


Мне уже наскучило придумывать весёлые фразы, поэтому просто и банально: файл экосистемы — есть. Поддерживаются форматы JSON, YAML и JS. Например, когда необходимо следить за файлами в папках server и client:

module.exports = {
  apps: [{
    script: "app.js",
    watch: ["server", "client"],
    env_production : {
      "NODE_ENV": "production"
    }
  }]
}


Подробнее ознакомиться можно по ссылке — PM2 Application Declaration.

И даже мониторинг есть!


И не один. Выбирайте тот который нравится больше. Можно мониторить в консоли командой:

pm2 monit




Или же воспользоваться полноценной веб-версией мониторинга:



Вы мне, конечно же, не поверите, но она устанавливается и запускается одной командой:

pm2 plus


И многое-многое другое...


Заявлена поддержка Heroku и Docker, автоматическое инкрементирование портов с возможностью передачи в process.env (когда нужно каждый процесс запускать на отдельном порту), запуск нескольких инстансов PM2 в пределах одной ОС, наличие программного API и возможность запускать демонизированные Bash и Python скрипты!

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

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


  1. Yeah
    17.12.2019 02:18

    У pm2 есть одна ублюдочная особенность — он преобразует логи при помощи .toString() даже если в настройках стоит тип "json", что делает невозможным нормальный анализ логов теми же ELK или хотя бы Cloudwatch insights


  1. OloloFine
    17.12.2019 04:04

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


    1. Voiddancer
      18.12.2019 07:03

      Однако дела с этим в NodeJS обстоят немного лучше, чем кажется на первый взгляд

      Никто не говорил про серебряную пулю в сфере node.js.


  1. constb
    18.12.2019 09:50

    у меня с pm2 вечная проблема с потерей всего состояния при обновлении ноды. unstartup/startup недостаточно, нода установлена через nvm и у неё меняются пути при установке новой версии. это надёжно убивает все записи сохранённые в дампе (pm2 save), потому что там абсолютные пути. получается так что я обновляю ноду, переустанавливаю pm2 (npm i -g pm2) так как путь к глобальному node_modules тоже изменился, делаю pm2 update – и после перезапуска у меня девственно чистый список процессов (pm2 ls)… :(


    приходится заниматься жостким колхозингом – руками править startup-скрипты, руками править pm2.dump и то – как повезёт всё равно…


    с логами тоже не всё идеально, pm2 logs неудобен, monit даёт кучу информации, но как раз по логам отмотать на произвольное время в прошлом не получается… в итоге приходится руками лезть в ~/.pm2/logs и там искать нужное…


    по итогу несмотря на то что вроде инструмент хороший, в проде оказывается проще поднять докер, задеплоить с docker-compose up -d с параметром --scale, чтобы поднять нужное количество копий и позволить докеру самому разруливать между ними подключения… обновление версии ноды выполняется вместе с обновлением самого сервиса, а там и до интеграции с ci/cd недалеко, для автоматического прогона тестов перед деплоем, например…


    на долю pm2 остаётся случай когда надо по-быстренькому что-то сунуть на сервер, всякие времянки, стейджинг – такого рода вещи…


    1. mayorovp
      18.12.2019 10:13

      А вы не пробовали поставить pm2 без nvm, а при создании процессов указывать запуск нужной ноды через опцию --interpreter?


      1. constb
        18.12.2019 13:32

        а в том проекте amazon linux 2, у него nvm – это стандартный способ установки ноды, в официальных репах её нет… собственно «поставить pm2 без nvm» там не выйдет…


        1. mayorovp
          18.12.2019 13:49

          Ну, значит, надо поставить отдельную ноду для pm2.


  1. polearnik
    18.12.2019 10:19

    Предположим у меня есть чат на nodejs и websockets которым пользуется куча народа. 1 ядро уже не справляется и я хочу его смаштабировать на свободные ядра. ЗХапускаю кучу процессов а они все слушают на одном порту? Как передать разные настройки?
    Я решил эту проблему модулем Cluster. Там и общение между процессами можно организовать. А перезаупскать падающий процесс есть supervisor/ мониторинг это пкфафтф. Всеже pm это как то не unix-way. шаг в сторону и дикие костыли.


  1. dipiash
    18.12.2019 11:31

    Спорное решение запускать с флагом `-i N`. Производительность у этого подхода сильно меньше, чем запустить столько-же отдельных экземпляров приложения на разных портах и балансировать их, например, через nginx.


    1. constb
      18.12.2019 12:40

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