В мире серверного JavaScript'а я — новичок с чистым, практически незамутнённым разумом. Поэтому когда я узнал о существовании менеджеров процессов, а конкретно — о pm2, то сразу же попробовал применить его для запуска какого-нибудь простейшего backend-сервиса на nodejs
в целях самообразования. Мне очень импонирует возможность подключения модулей в JS-коде через import
(ES6 modules), т.к. он позволяет использовать один и тот же код как в браузере, так и на серверной стороне, и я запилил простой сервис с ES6-модулями.
Если вкратце, то запустить ES6-версию приложения под pm2
у меня не получилось, для запуска таких приложений лучше использовать либо forever
, либо systemd
. Под катом — отчёт о результатах для тех, кто любит тексты подлинее.
Введение
В контексте данной публикации под менеджером процессов подразумевается сервис, основной задачей которого является мониторинг запущенного nodejs
-приложения и его перезапуск в случае падения. Также менеджер процессов может (но не обязан) собирать информацию о потребляемых приложением ресурсах (процессор, память).
Тестовый сервис
Для тестирования менеджеров процессов я использовал вот такой код в ES6-сервисе (github repo):
# src/app_es6.mjs
import express from "express";
import mod from "./mod/es6.mjs";
const app = express();
const msg = "Hello World! " + mod.getName();
app.get("/", function (req, res) {
console.log(msg);
res.send(msg);
});
app.listen(3000, function () {
console.log('ES6 app listening on port 3000!');
});
и в ES6-модуле:
# src/mod/es6.mjs
export default {
getName: function () {
return "ES6 module is here.";
}
}
Аналогичный сервис, выполненный c CommonJS-модулями выглядит так:
# src/app_cjs.js
const express = require("express");
const mod = require("./mod/cjs.js");
const app = express();
const msg = "Hello World! " + mod.getName();
app.get("/", function (req, res) {
console.log(msg);
res.send(msg);
});
app.listen(3000, function () {
console.log("CommonJS app listening on port 3000!");
});
CJS-модуль:
# src/mod/cjs.js
module.exports = {
getName: function () {
return "CommonJS module is here.";
}
};
Запуск сервиса без использования менеджера процессов на nodejs
v12.14.0:
$ node --experimental-modules ./src/app_es6.mjs # ES6-service
$ node ./src/app_cjs.js # CJS-service
pm2
pm2
на данный момент является лидером среди менеджеров процессов по предлагаемому функционалу (помимо поддержания процесса в рабочем состоянии также есть кластеризация, мониторинг использования ресурсов, различные стратегии рестарта процессов).
CJS-сервис запускается без проблем (pm2
v4.2.1):
$ pm2 start ./src/app_cjs.js -i 4
также без проблем поддерживается заданное количество экземпляров сервиса в кластере:
root@omen17:~# ps -Af | grep app_cjs
alex 29848 29828 0 15:31 ? 00:00:00 node /.../src/app_cjs.js
alex 29855 29828 0 15:31 ? 00:00:00 node /.../src/app_cjs.js
alex 29864 29828 0 15:31 ? 00:00:00 node /.../src/app_cjs.js
alex 29875 29828 0 15:31 ? 00:00:00 node /.../src/app_cjs.js
После "убийства" одного экземпляра (PID 29864
) менеджер процессов сразу же поднял новый (PID 30703
):
root@omen17:~# kill -s SIGKILL 29864
root@omen17:~# ps -Af | grep app_cjs
alex 29848 29828 0 15:31 ? 00:00:00 node /.../src/app_cjs.js
alex 29855 29828 0 15:31 ? 00:00:00 node /.../src/app_cjs.js
alex 29875 29828 0 15:31 ? 00:00:00 node /.../src/app_cjs.js
alex 30703 29828 7 15:35 ? 00:00:00 node /.../src/app_cjs.js
Но ES6-версия приложения не отрабатывает корректно в pm2
. При передаче в nodejs аргумента "--experimental-modules":
$ pm2 start ./src/app_es6.mjs -i 4 --node-args="--experimental-modules"
получается вот такая картина:
В логах видим:
$ pm2 log
...
/home/alex/.pm2/logs/app-es6-error-2.log last 15 lines:
2|app_es6 | at /usr/lib/node_modules/pm2/node_modules/async/internal/onlyOnce.js:12:16
2|app_es6 | at WriteStream.<anonymous> (/usr/lib/node_modules/pm2/lib/Utility.js:186:13)
2|app_es6 | at WriteStream.emit (events.js:210:5)
2|app_es6 | at internal/fs/streams.js:299:10
2|app_es6 | Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /home/alex/work/sof_es6_pm/src/app_es6.mjs
2|app_es6 | at Object.Module._extensions..mjs (internal/modules/cjs/loader.js:1029:9)
2|app_es6 | at Module.load (internal/modules/cjs/loader.js:815:32)
2|app_es6 | at Function.Module._load (internal/modules/cjs/loader.js:727:14)
2|app_es6 | at /usr/lib/node_modules/pm2/lib/ProcessContainer.js:297:23
2|app_es6 | at wrapper (/usr/lib/node_modules/pm2/node_modules/async/internal/once.js:12:16)
2|app_es6 | at next (/usr/lib/node_modules/pm2/node_modules/async/waterfall.js:96:20)
2|app_es6 | at /usr/lib/node_modules/pm2/node_modules/async/internal/onlyOnce.js:12:16
2|app_es6 | at WriteStream.<anonymous> (/usr/lib/node_modules/pm2/lib/Utility.js:186:13)
2|app_es6 | at WriteStream.emit (events.js:210:5)
2|app_es6 | at internal/fs/streams.js:299:10
То есть, по факту pm2
не может без транспиляции запускать скрипты, в которых используются ES6-модули. Последний issue на эту тему создан 5 декабря 2019 (примерно месяц назад).
forever
forever
является следующим по популярности менеджером процессов после pm2
(npmtrends). Это более старый проект (начат в 2010 году против 2013 для pm2
), но у него более узкий фокус по функциональности, чем у pm2
. forever
"заточен" на постоянное поддержание работоспособности процесса без всяких дополнительных pm2
-плюшек в виде балансировки нагрузки и мониторинга используемых ресурсов. Судя по частоте коммитов проект находится в стабильном состоянии (фазу активного развития уже прошёл) и каких-то новых функций от него ждать не приходится. Я не нашёл способа передачи аргументов в nodejs
из командной строки при запуске forever
, но такая возможность есть, если использовать конфигурационный файл:
{
"uid": "app_es6",
"max": 5,
"spinSleepTime": 1000,
"minUptime": 1000,
"append": true,
"watch": false,
"script": "src/app_es6.mjs",
"command": "node --experimental-modules"
}
Запуск приложения в таком варианте выглядит так:
$ forever start forever.es6.json
...
$ forever list
info: Forever processes running
data: uid command script forever pid id logfile uptime
data: [0] app_es6 node --experimental-modules src/app_es6.mjs 3972 3979 /home/ubuntu/.forever/app_es6.log 0:0:0:3.354
Вот сами процессы:
$ ps -Af | grep es6
ubuntu 3972 1 0 12:01 ? 00:00:00 /usr/bin/node /usr/lib/node_modules/forever/bin/monitor src/app_es6.mjs
ubuntu 3979 3972 0 12:01 ? 00:00:00 node --experimental-modules /home/ubuntu/sof_es6_pm/src/app_es6.mjs
При "убийстве" процесса (PID 3979
) менеджер исправно поднимает новый (PID 4013
):
$ kill -s SIGKILL 3979
ubuntu@vsf:~/sof_es6_pm$ ps -Af | grep es6
ubuntu 3972 1 0 12:01 ? 00:00:00 /usr/bin/node /usr/lib/node_modules/forever/bin/monitor src/app_es6.mjs
ubuntu 4013 3972 4 12:10 ? 00:00:00 node --experimental-modules /home/ubuntu/sof_es6_pm/src/app_es6.mjs
forever
прекрасно справляется с запуском приложения, использующего ES6-модули, но возникает вопрос, зачем тянуть на linux-системы forever
, если подобной функциональности можно добиться и через средства самой ОС?
systemd
systemd позволяет создавать сервисы в linux-среде и контролировать их запуск, в том числе и в случае их внезапного падения. Достаточно создать unit-файл с описанием сервиса (./app_es6.service
):
[Unit]
Description=Simple web server with ES6 modules.
After=network.target
[Service]
Type=simple
Restart=always
PIDFile=/run/app_es6.pid
WorkingDirectory=/home/ubuntu/sof_es6_pm
ExecStart=/usr/bin/nodejs --experimental-modules /home/ubuntu/sof_es6_pm/src/app_es6.mjs
[Install]
WantedBy=multi-user.target
и залинковать его в каталог /etc/systemd/system
(в unit-файле пути должны быть абсолютными). За рестарт сервиса в случае его внезапного останова отвечает опция:
Restart=always
Запуск сервиса осуществляется так:
# systemctl start app_es6.service
# systemctl status app_es6.service
? app_es6.service - Simple web server with ES6 modules.
Loaded: loaded (/home/ubuntu/sof_es6_pm/app_es6.service; linked; vendor preset: enabled)
Active: active (running) since Thu 2020-01-02 11:09:42 UTC; 9s ago
Main PID: 2184 (nodejs)
Tasks: 11 (limit: 4662)
CGroup: /system.slice/app_es6.service
L-2184 /usr/bin/nodejs --experimental-modules /home/ubuntu/sof_es6_pm/src/app_es6.mjs
Jan 02 11:09:42 vsf systemd[1]: Started Simple web server with ES6 modules..
Jan 02 11:09:42 vsf nodejs[2184]: (node:2184) ExperimentalWarning: The ESM module loader is experimental.
Jan 02 11:09:42 vsf nodejs[2184]: ES6 app listening on port 3000!
При "убийстве" процесса (PID 2184
) systemd
исправно поднимает новый (PID 2233
):
# ps -Af | grep app_es6
root 2184 1 0 11:09 ? 00:00:00 /usr/bin/nodejs --experimental-modules /home/ubuntu/sof_es6_pm/src/app_es6.mjs
# kill -s SIGKILL 2184
# ps -Af | grep app_es6
root 2233 1 3 11:10 ? 00:00:00 /usr/bin/nodejs --experimental-modules /home/ubuntu/sof_es6_pm/src/app_es6.mjs
Т.е., systemd
делает то же самое, что и forever
, но на более фундаментальном уровне.
StrongLoop
При обзоре вариантов имплементаций менеджеров процессов часто всплывает StrongLoop. Однако очень сильно похоже, что этот проект перестал развиваться (последняя версия 6.0.3 вышла 3 года назад). Мне не удалось его даже установить на Ubuntu 18.04 через npm
:
# npm install -g strongloop
npm WARN deprecated swagger-ui@2.2.10: No longer maintained, please upgrade to swagger-ui@3.
...
npm ERR! A complete log of this run can be found in:
npm ERR! /root/.npm/_logs/2020-01-02T11_25_15_473Z-debug.log
Через yarn
пакет установился, несмотря на большое количество сообщений о deprecated версиях зависимостей и ошибок установки, тем не менее, от изучения StronLoop'а я отказался.
Инструменты разработчика
Очень часто рядом с pm2
и forever
встречаются такие пакеты, как nodemon, watch, onchange. Эти инструменты не являются менеджерами процессов, но позволяют мониторить изменения в файлах и выполнять команды, привязанные к этим изменениям (в том числе, и перезапускать приложение).
Резюме
Менеджер процессов, подобный pm2
, является очень полезным сервисом в мире серверного JS. Но, к сожалению, сам pm2
не позволяет запускать современные nodejs
-приложения (в частности — с ES6-модулями). Так как я не очень люблю транспиляцию, то наиболее приемлемым на данный момент менеджером процессов в nodejs
для меня является традиционный systemd
(или его альтернативы). Однако я с радостью буду использовать pm2
, как только pm2
сможет поддерживать приложения с ES6-модулями.
Комментарии (34)
apapacy
02.01.2020 21:12+1Спасибо что обратили внимание на такую проблему. Я бы все же работал скорее с pm2 например как описано в статье https://en.programqa.com/question/52499715/ т.к. pm2 это (если не под cubernetes) наверное наилучший вариант.
flancer Автор
02.01.2020 22:45Спасибо за ссылку. Интересный workaround. Попробовал его применить на тестовом сервисе. Добавил в
package.json
:
"scripts": { "safestart":"node --experimental-modules src/app_es6.mjs" }
и запустил сервис командой:
$ pm2 start npm -- run safestart
Видно, что ES6-приложение запускается:
$ pm2 log ... /home/alex/.pm2/logs/npm-error.log last 15 lines: 0|npm | (node:11489) ExperimentalWarning: The ESM module loader is experimental. /home/alex/.pm2/logs/npm-out.log last 15 lines: 0|npm | 0|npm | > pm2es6@0.1.0 safestart /home/alex/work/sof_es6_pm 0|npm | > node --experimental-modules src/app_es6.mjs 0|npm | 0|npm | ES6 app listening on port 3000! 0|npm | Hello World! ES6 module is here. 0|npm | Hello World! ES6 module is here. 0|npm | Hello World! ES6 module is here. 0|npm | Hello World! ES6 module is here.
К сожалению, нельзя использовать возможности кластеризации (
-i 4
) — запускается только первый инстанс приложения, остальные сваливаются. Но и то, что есть, гораздо интереснее выглядит, чемforever
илиsystemd
.monochromer
03.01.2020 01:54+1Попробовал так (
npm run cluster
):
{ "type": "module", "scripts": { "start": "node --experimental-modules app.js", "cluster": "pm2 -i 4 start npm -- run start" }, }
Вроде запустилось:
flancer Автор
03.01.2020 09:37+1Попробовал так же. Похоже, что запускается только один процесс, хотя
pm2
думает, что запустились все:
в
pm2
-логах также видны ошибки:
$ pm2 log ... 1|npm | npm ERR! code ELIFECYCLE 1|npm | npm ERR! errno 1 1|npm | npm ERR! pm2es6@0.1.0 safestart: `node --experimental-modules src/app_es6.mjs` 1|npm | npm ERR! Exit status 1 1|npm | npm ERR! 1|npm | npm ERR! Failed at the pm2es6@0.1.0 safestart script. 1|npm | npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
В "нормальном" кластере запускается указанное кол-во процессов:
alex@omen17:~/work/sof_es6_pm$ ps -Af | grep app_ alex 10903 25382 2 08:29 ? 00:00:00 node /home/alex/work/sof_es6_pm/src/app_cjs.js alex 10910 25382 3 08:29 ? 00:00:00 node /home/alex/work/sof_es6_pm/src/app_cjs.js alex 10919 25382 2 08:29 ? 00:00:00 node /home/alex/work/sof_es6_pm/src/app_cjs.js alex 10932 25382 3 08:29 ? 00:00:00 node /home/alex/work/sof_es6_pm/src/app_cjs.js
Думаю, что в моём тестовом приложении идёт конкуренция за порт 3000 для
express
-сервера. Какой-то из процессов первым захватывает порт, остальные отваливаются по ошибке. В логах это видно — при 4 экземплярах в кластере сообщений об ошибке только 3.pm2
каким-то образом обрабатывает вариант с портом самостоятельно, но как только мы стартуем приложение черезnode
/npm
, тоpm2
теряет эту возможность.
Kanumowa
02.01.2020 23:45+3А почему запускали на 12ой ведь они там под флагом? Почему не попробовали тоже самое для 13.2.0+ без флага?
flancer Автор
03.01.2020 00:0112-я версия — LTS (Long Term Support). Только поэтому.
justboris
03.01.2020 10:26Long Term Support — это про стабильность. А модули пока ещё экспериментальные и их стабильность не гарантируется. Поэтому, в вашем случае от LTS пользы нет
flancer Автор
03.01.2020 11:32LTS — это наиболее вероятная версия софта. Я рассматривал частый кейс, а не последний. У меня и Ubuntu стоит 18.4 по той же причине — стабильность.
Babayka_od
03.01.2020 01:22Если хочется использовать es6 модули, то почему бы не воспользоваться бабалем?
apapacy
03.01.2020 02:29+1Бабелем я пользовался когда еs6 был еще практически без поддержки на нативном уровне. Сейчас когда все практически нативно бабелем нет резона пользоваться. Я даже в свое время отказался для этого от import в пользу require
Сейчас я скорее предпочел бы не использовать import, чем использовать бабел.
potorochinau
03.01.2020 09:04Ещё бы рекомендовал использовать ecosystem.config.js в pm2. Про environments не забыть тоже с примерами.
Давно пользуюсь pm2. Но вот тоже думаю пора в Докер. Ибо развёртывание приложений на новые машины уже утомляет, но я и не DevOps, просто FullStackJS. Пока не было задач, где бы я мог на 100% оправдать использование Докера. Чтобы выделить время на полное изучение его workflow.А очень хочется.VolCh
03.01.2020 09:36От девопосов всё чаще слышу, что от разработчиков они ожидают минимум докерфайл (манифест для сборки контейнера) для включения нового сервиса или ui в систему. Типа сеньору уже не простительно не знать Докер хотя бы поверхностно, они потом оптимизируют, дотюнят, но базу им надо.
А если вы занимаетесь разворачиванием на продакшен, то вы уже девопс в современных массовых понятиях.
flancer Автор
03.01.2020 11:39В некоторых случаях Docker — это просто лишний слой. Даже на проде. Если DevOps без базы не может сам засунуть приложение в контейнер, то у меня возникают вопросы уже к его квалификации. В конце концов, разрабы не обязаны знать все варианты, в которых будут гонять их приложения. Если в вашей конторе так устроено, что разрабы должны выкатывать приложения в Docker-контейнерах, то тогда — да, сеньору не простильно не знать.
VolCh
04.01.2020 10:31Можно рассматривать базовый докерфайл и ко от разрабов как доку для девопса. Какие енв переменные приложение ожидает, какие конфиги, как его вообще собирать из исходников и как запускать. Есть и другие способы, конечно. И часть из них тоже отчасти самодокументируемые типа набора баш-скриптов, но плюс докера и подобных подходов (вагрант, например) — разработчик вынужден полностью описывать среду для своего приложения. Нет места (почти) "ой, забыл сказать, что нужно новое расширение для языка установить, я-то его ещё в первый день работы установил себе, наверное, но в проекте раньше не использовалось"
apapacy
03.01.2020 10:09Докер это очень просто, например гораздо проще чем bash. Нужно просто понять зачем он нужен. Но вот докер на проде это уже сложно. Т.к. без средств оркестрации на проде докер предоставит больше проблем чем преимуществ. А вот средства оркестрации это уже сложнее. Например cubernetes практически невозможно развернуть и главное поддерживать в рабочем состоянии не специалисту. Поэтому приходится покупать облачный. Поэтому его пиарят нещадно. Nomad реально развернуть и поддерживать самостоятельно но мало специалистов.
VolCh
04.01.2020 10:32Докер не проще чем баш, потому что без баша нормально с докером работать не получится обычно :)
Viceroyalty
03.01.2020 19:20Возможно я отстал от жизни или прилетел с другой планеты — но неужели JS настолько популярнее стандартных С/С++/С#, что он активно развивается на стороне сервера?
apapacy
03.01.2020 21:45С на стороне сервера используется для написание собственно серверов. Веб приложения как правило на с не пишут. Js используется чаще чем с. С# это как бы в основном для экосистемы windows. Для этой экосистемы с# скорее всего превалирует.
Fi1osof
03.01.2020 20:17Менеджер процессов, подобный pm2, является очень полезным сервисом в мире серверного JS. Но, к сожалению, сам pm2 не позволяет запускать современные nodejs-приложения (в частности — с ES6-модулями).
Я сильно не изучал этот момент, но у меня вполне получается через pm2 запускать package.json-скрипт с передачей этого флага, и все работает норм.
«scripts»: {
«start-server»: «node --experimental-modules src/server/»,apapacy
03.01.2020 21:42Воблер сейчас флаги практически не актуально тк последние версии нативно поддерживают модули без флагов. Проблема кроется в оператора require которым pm2 пытается инклудить основной модуль. И если в обычном режиме всякими хитростями удается все же запустить процесс, сложности начинаются когда пытаются запустить несколько процессов в режиме кластера
vasyapivo
Современным способом запуска node-пиложений является
CMD [ «node», «index.js» ]
Akuma
Ну кстати да, контейнеры так-то удобнее будут. Упало приложение — упал контейнер.
apapacy
С контейнерами все отлично если это под kubernetes — в остальных случаях скорее больше чем меньше проблем.
flancer Автор
А почему контейнер должен упасть, если упало приложение? Я с контейнерами знаком слабо, но по моим представлениям так быть не должно (по крайней мере в docker'е).
VolCh
Докер (или надстройки над ним) заботятся о замене контейнера, если основной процесс в нём упал. Процесс в контейнере не демонизируется даже.
Yeah
Именно так и должно быть. контейнер обязан падать при падении приложения.
flancer Автор
Заинтригован. Не могли бы вы дать ссылку на описание этого способа запуска?
apapacy
CMD это имеется в виду ситнаксис Dockerfile например можно почитать в не моей статье https://habr.com/ru/company/southbridge/blog/329138/
Если основной процесс валится то и контейнер валится. Можно этого избежать если основной просеыы будет например supervisor но это будет уже не docker way
Кстати supervisor это еще один способ запустить то что нужно.
Проблема докер-контейнеров в том что для реальной работы на проде они требуют оркестратор в качестве которого выступает например nomad или cubernetes — которые как раз и следят чтобы процесс был рабочий и в случае ошибки запускают новый процесс. Но по сравнению с pm2 для оркестратура нужно еще очень и очень много чего.
VolCh
restart always или on failure базовая фича докера. Для перезапуска дополнительно оркестраторы не нужны. Они для реплицирования и обнаружения (связи) сервисов прежде всего
apapacy
Речь скорее о том что запускать контейнеры на проде командной строкой в 100500 символов не так уж удобно. А от этой командной строки как раз зависит что и как собственно будет работать
Например если я хочу разобраться как работает хостовый не в докере nginx или mysql я изучаю его конфиги. В случае докере мне для этого нужно ещё знать командную строку, dockerfile которого может и не быть в исходниках а где-то взят готовый чужой имидж.
t_kanstantsin
Какие-то надуманные проблемы. Чтобы запускать сервер — надо будет знать 100500 символов настройки этого сервера (установка всего и вся, настройка портов, копирование конфигов). А переехать на другой сервер — это значит опять всё устанавливать и не факт, что что-нибудь не забудется и/или версия пакета не обновится. А с докером: установить докер, скачать проект и запустить подготовленную строку.
Чужой имидж — это просто стандартный официальный имидж от разработчика приложения, который легко настраивается и всегда предсказуемо вебя ведёт. Не представляю, чтобы кто-то использовал какой-то мутный имидж от no-name. Проще самому написать.
apapacy
Откуда берутся мутные имиджи. Девопс творческий в кавычках и без кавычек человек. Слабо контролиуемый так как в его хозяйство никто не лезет. Фигачит налево и направо кастомные имиджи и заливает их на публичные хабы. Прошли годы. Сломался проект или нужно как Вы справедливо заметели переместить проект на другой сервер. И тут начинаются не надуманные проблемы.
t_kanstantsin
Если в примере имидж — "мутный", то в примере без docker'a — у вас не будет никаких сведений (или очень поверхностных) о конфигурации сервера. Но это крайние случаи — и оба варианта одинаково неприятны. Тут причина не в docker'e, а в девопсе.
В моём опыте не крупных проектов с dockerfile'ами (без выделенного девопса) — они хранятся либо в самом проекте в папке docker, либо в отдельном репозитории той же компании. Это значительно упрощает настройку как локального окружения, так и сервера.