nodejs: менеджеры процессов и ES6-модули

В мире серверного 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 на данный момент является лидером среди менеджеров процессов по предлагаемому функционалу (помимо поддержания процесса в рабочем состоянии также есть кластеризация, мониторинг использования ресурсов, различные стратегии рестарта процессов).

CJS-сервис запускается без проблем (pm2 v4.2.1):

$ pm2 start ./src/app_cjs.js -i 4

cxtu9mfzw3hkqq-si7h_brbfejy.png

также без проблем поддерживается заданное количество экземпляров сервиса в кластере:

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"

получается вот такая картина:
ury_a3oh7cfqa-gj1-107tcxwx8.png

В логах видим:

$ 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. (/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. (/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 является следующим по популярности менеджером процессов после 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 позволяет создавать сервисы в 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
           └─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. Однако очень сильно похоже, что этот проект перестал развиваться (последняя версия 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-модулями.

© Habrahabr.ru