Сделав поиск по ключевому слову «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.
Вы можете запустить сколько угодно процессов, но всё же рекомендуется придерживаться рекомендации «один процесс на одно ядро».
Бережное отношение к памяти
При разработке на 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)
OloloFine
17.12.2019 04:04PM2 штука полезная, да, но я как-то неуверен что «запустить процесс много раз» это то, что имел ввиду Ваш оппонент. Даже если оставить в стороне различия между процессами и потоками, это все равно не про «как их пачку стартануть».
Voiddancer
18.12.2019 07:03Однако дела с этим в NodeJS обстоят немного лучше, чем кажется на первый взгляд
Никто не говорил про серебряную пулю в сфере node.js.
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 остаётся случай когда надо по-быстренькому что-то сунуть на сервер, всякие времянки, стейджинг – такого рода вещи…
mayorovp
18.12.2019 10:13А вы не пробовали поставить pm2 без nvm, а при создании процессов указывать запуск нужной ноды через опцию
--interpreter
?
polearnik
18.12.2019 10:19Предположим у меня есть чат на nodejs и websockets которым пользуется куча народа. 1 ядро уже не справляется и я хочу его смаштабировать на свободные ядра. ЗХапускаю кучу процессов а они все слушают на одном порту? Как передать разные настройки?
Я решил эту проблему модулем Cluster. Там и общение между процессами можно организовать. А перезаупскать падающий процесс есть supervisor/ мониторинг это пкфафтф. Всеже pm это как то не unix-way. шаг в сторону и дикие костыли.
dipiash
18.12.2019 11:31Спорное решение запускать с флагом `-i N`. Производительность у этого подхода сильно меньше, чем запустить столько-же отдельных экземпляров приложения на разных портах и балансировать их, например, через nginx.
constb
18.12.2019 12:40почему производительность меньше? там же под капотом обычный нодовский cluster, он принимает соединения и сокеты прокидывает в воркеры, которые их обслуживают. откуда там задержки или низкая производительность вдруг?
Yeah
У pm2 есть одна ублюдочная особенность — он преобразует логи при помощи .toString() даже если в настройках стоит тип "json", что делает невозможным нормальный анализ логов теми же ELK или хотя бы Cloudwatch insights