dùng PM2 API để quản lý các tiến trình NodeJs

Giới thiệu PM2

Trong quá trình phát huy một app nodejs, bạn thường khởi chạy app bằng lệnh node app.js, nhưng khi đưa app lên môi trường production(prod) thì không dễ hiểu như vậy. Trên môi trường prod bạn cần phải quan tâm tới nhiều thứ hơn: Phân quyền người sử dụng chạy app, quản lý tiến trình, logs, tự khởi động lại…
PM2 là một công cụ quản lý tiến trình nodejs hoàn hảo cho bạn trong hầu như tình thế chạy ứng nodejs trên môi trường prod.
Những người phát triền app nodejs có thể đã và đang dùng công cụ này, tôi khuyên bạn nên dùng nó.
Một trong những điểm mạnh của công cụ này là có cung ứng API cấp quyền coder có thể điều khiển, giám sát các tiến trình nodejs khác trong một app nodejs.
Trong bài viết tôi sẽ xây dụng một webapp bằng nodejs có chức năng quản lý các tiến trình nodejs khác.

thực hiện

Sẽ làm gì?

Xậy dụng một webapp có chức năng hiển thị những tiến trình nodejs cần quản lý, có thể start, stop, restart, quản lý tài nguyên (ram, cpu) đang dùng, tail log của app.

Cần những gì?

Để thực hiện được bài viết này ta cần có hiểu biết cơ bản đủ để tạo dựng một webapp bằng nodejs dùng express framework, có dùng socketIO

Bắt đầu

Cấu trúc thư mục
Thư mục miners: Chứa các tiến trình con sẽ quản lý. Thư mục static: Chứ tài nguyên lặng dùng cho webapp (html, css, js file). File app.js: Phần chính chứa logic của app. File package.json: Quản lý tin tức, dependencies của app. Cài đặt các thư viện, frameword cần dùng
Cài đặt môi trường nodejs và công cụ pm2 global (npm install -g pm2).
Vào thư mục dự án:
Chạy lệnh npm install --save socket.io express body-parser pm2
npm install --save-lập trình viên bootstrap jquery
Hoặc dùng file package.json với content:

{   "name": "Pm2API_demo",   "version": "1.0.0",   "description": "",   "main": "app.js",   "scripts": {     "start": "node app.js",     "lập trình viên": "nodemon app.js",     "test": "echo \"Error: no test specified\" && exit 1"   },   "dependencies": {     "body-parser": "^1.17.2",     "express": "^4.15.3",     "pm2": "^2.6.1",     "socket.io": "^2.0.3"   },   "keywords": [],   "author": "",   "license": "ISC",   "lập trình viênDependencies": {     "bootstrap": "^3.3.7",     "jquery": "^3.2.1"   } } 

tiếp theo chạy lệnh npm install

copy file jquery.js trong thư mục node_modules\jquery\dist vào thư mục static\script
copy thư mục dist trong thư mục node_nodules\boostrap\dist vào thư mục static\css, đặt lại tên thành boostrap

tạo dựng app web Khởi tạo http, socket server với nodejs + express + socketIO
app.js

var express = require('express'); var app = express(); var http = require("http").Server(app); var io = require("socket.io")(http); var bodyParser = require('body-parser');  var PORT = process.env.PORT || 8080; const MINERS = ["miner001.js", "miner002.js"]; app.use(bodyParser.urlencoded({     extended: true })); app.use(bodyParser.json({     type: '*/*' })); app.use(express.static("static"));  /* Routes */ app.use('/', function (req, res) {     res.sendFile(__dirname + "/static/index.html"); }); // socket.io io.on("connection", function (client) {     console.log("A client connect. ClientId: " + client.id); }); http.listen(PORT, function () {     console.log('Server is running on port: ' + PORT); }); module.exports = app; 

File index.html
content hiển thị list các tiến trình

<!DOCTYPE html> <html lang="en"> <head>     <meta charset="UTF-8">     <đường dẫn rel="stylesheet" href="css/boostrap/css/bootstrap.min.css">     <đường dẫn rel="stylesheet" href="css/style.css">     <script src="script/jquery.js"></script>     <script src="css/boostrap/js/bootstrap.min.js"></script>     <script src="/socket.io/socket.io.js"></script>     <script src="script/main.js"></script>     <title>PM2 API DEMO</title> </head> <body> <div class="container">     <div class="panel panel-success">         <div class="panel-heading">Miners</div>         <div class="panel-body">             <table id="tbl-miners" class="table table-chức năng cung ứng table-bordered">                 <thead>                 <tr>                     <td>Id</td>                     <td>Status</td>                     <td>Performance</td>                     <td>Action</td>                 </tr>                 </thead>                 <tbody>                 </tbody>             </table>         </div>     </div> </div> </body> </html> 

File js client main.js
liên kết tới socket server

$(document).ready(function () {     var socket = io(); }); 

Hiển thị list các process sẽ quản lý
pm2.describe cấp quyền lấy tin tức của process đang được quản lý bằng pm2 theo tên hoặc pm2_id.
Hàm này trả lại một array(tình thế tiến trình ở cluster mode) các object chứ tin tức các tiến trình. app.js

... var pm2 = require("pm2"); ... function getProcessSetting(name) {     var alias = name.replace(".js", "");     return {         script: __dirname + "/miners/" + name,         name: name,         log_date_format: "YYYY-MM-DD HH:mm Z",         out_file: __dirname + "/miners/" + alias + ".stdout.log",         error_file: __dirname + "/miners/" + alias + ".stderr.log",         exec_mode: "fork",         autorestart: false     }; } ... app.get("/miners", function (req, res) {     var promiseLst = [];     MINERS.map(function (t) {         return getProcessSetting(t);     }).forEach(function (t) {         promiseLst.push(new Promise(function (resolve, reject) {             pm2.describe(t.name, function (err, processDescription) {                 if (err) {                     reject(err);                 } else {                     var des = processDescription[0];                     var process = des? des : {                         name: t.name,                         pid: "N/A",                         pm_id: "N/A",                         monit: {                             memory: "N/A",                             cpu: "N/A"                         },                         pm2_env: {                             status: "unregistry"                         }                     };                     resolve(process);                 }             });         }));     });     Promise.all(promiseLst)         .then(function (result) {             console.log(result);             res.json(result);         })         .catch(function (err) {             console.log(err);             res.json(err);         }); }); ... 

script/main.js

$(document).ready(function () {     var socket = io();     // get list miners     $.get("/miners", function (data) {         data.forEach(function (t) {             $('#tbl-miners').find('tbody').append(`             <tr id="${t.name}">                 <td>${t.name}</td>                 <td>${getStatusLabel(t.pm2_env.status)}</td>                 <td>                     <div class="btn-group">                         <button type="button" class="btn btn-default btn-sm">CPU: ${t.monit.cpu} %</button>                         <button type="button" class="btn btn-default btn-sm">RAM: ${(t.monit.memory / (1024 * 1024)).toFixed(1)} MB</button>                       </div>                 </td>                 <td>                     ${getPowerButton(t.pm2_env.status)}                     <button type="button" class="btn btn-default btn-sm btn-action" data-action="restart">                             <span class="glyphicon glyphicon-repeat success-color"></span> Restart                     </button>                     <button type="button" class="btn btn-default btn-sm btn-action" data-action="tail">                             <span class="glyphicon glyphicon-eye-open success-color"></span> Tail logs                     </button>                 </td>             </tr>             `)         });     })     function getStatusLabel(status) {         switch (status) {             case "stopped":                 return `<span class="label label-danger">${status}</span>`;             case "trực tuyến":                 return `<span class="label label-success">${status}</span>`;             default:                 return `<span class="label label-default">${status}</span>`;         }     }      function getPowerButton(status) {         if (status === "trực tuyến") {             return `             <button type="button" class="btn btn-default btn-sm btn-action" data-action="stop">                     <span class="glyphicon glyphicon-flash danger-color"></span> Stop             </button>         `;         }         return `             <button type="button" class="btn btn-default btn-sm btn-action" data-action="start">                     <span class="glyphicon glyphicon-flash success-color"></span> Start             </button>         `;     } }); 

Action start, stop, restart
Chuẩn bị 2 processes:
miners/miner001.js

setInterval(function () {     console.log("I am miner001!"); }, 1000); 

miners/miner002.js

setInterval(function () {     console.log("I am miner002!");     for (var i =0 ; i < 9999; i++) {         Math.random()     }     console.log("Detect a block on chanel: " + Math.random()); }, 2000); 

dùng http request để điều khiển process.
Chỉ action start ta mới khai báo đầy đủ tin tức theo setting. Còn những action còn lại chỉ cần dùng tên của process

app.js

... app.put("/miners/:name", function (req, res) {     var processName = req.params["name"];     var action = req.query["action"];     if (["start", "stop", "restart"].indexOf(action) >= 0 && MINERS.indexOf(processName) >= 0) {         var process = processName;         if (action === "start") {             process = getProcessSetting(processName);         }         pm2.connect(function(err) {             if (err) {                 console.error(err);                 res.status(500).json(err);                 return;             }             pm2[action](process, function(err, apps) {                 // pm2.disconnect();   // Disconnects from PM2                 if (err) {                     res.status(500).json(err);                     return;                 }                 res.json({success: true});             });         });         return;     }     res.status(404).json({message: "action or process not found!"}); }); ... 

script/main.js

...  $(document).on('click', '.btn-action', function (e) {         e.preventDefault();         var self = $(this);         var action = self.data('action');         var process = self.parents('tr').attr('id');         if (action !== "tail") {             $.ajax(`/miners/${process}?action=${action}`, {                 type: "PUT",                 success: function (data) {                     location.reload(); // :D                 },                 error: function (error) {                     console.log(error);                 }             });         }     });  ... 

Hiển thị log của đã có lần process đang chạy. Đăng ký lắng nghe sự kiện log sdt out
app.js

http.listen(PORT, function () {    ...     pm2.connect(function(err) {         if (err) {             console.error(err);             process.exit(0);             return;         }         pm2.launchBus(function (err, bus) {             bus.off('log:out');             bus.on('log:out', function (data) {                 var processName = data.process.name;                 if (MINERS.indexOf(processName) >= 0) {                     io.emit(`${processName}:log`, {log: data.data})                 }             });         });     });     ... }); 

Đăng ký lắng nghe sự kiện từ socketIO hiển thị log khi click vào button Tail logs
script/main.js

... if (action === "tail") {     // To unsubscribe all listeners of an log event     $('#tbl-miners').find('tbody').find('tr').each(function () {         socket.off(`${$(this).attr('id')}:log`);     });     var jConsole = $('#console');     jConsole.empty();     socket.on(`${process}:log`, function (data) {         if (jConsole.find('p').length > 32) {             jConsole.empty();         }         jConsole.append(`<p id="console-text">${data.log}</p>`);         console.log(data);     }); }  ... 

Tổng kết

Hy vọng bài viết sẽ giúp đỡ ích cho những coder nodejs trong việc monitor app của mình.
ta có thể dùng PM2 API triền khai nhiều hướng app vào thực tế các dự án:
Ví dụ: thời gian thực status của process. Trong bài viết mỗi khi update trạng thái của process ta lại phải reload lại website, PM2 API launchBus còn khá nhiều events có thể khai thác, ví dụ: process:event, event này quản lý các trạng thái của process exit, restart, trực tuyến, restart overlimit ...

Tài liệu tham khảo

pm2-api pm2-doc pm2-gui

đường dẫn source code

Nguồn viblo.asia