Sử dụng PM2 API để quản lý các tiến trình NodeJs
Bài đăng này đã không được cập nhật trong 3 năm
Giới thiệu PM2
Trong quá trình phát triển một ứng dụng nodejs, bạn thường khởi chạy app bằng lệnh node app.js
, nhưng khi đưa ứng dụng lên môi trường production(prod) thì không đơn giản 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 dùng chạy ứng dụng, 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 hết trường hợp chạy ứng nodejs trên môi trường prod.
Những người phát triền ứng dụng nodejs có thể đã và đang dùng công cụ này, tôi khuyên bạn nên sử dụng nó.
Một trong những điểm mạnh của công cụ này là có cung cấp API cho phép lập trình viên có thể điều khiển, giám sát các tiến trình nodejs khác trong một ứng dụng 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 sử dụng, tail log của ứng dụng.
Cần những gì?
Để thực hiện được bài viết này chúng ta cần có hiểu biết cơ bản đủ để xây dựng một webapp bằng nodejs sử dụng express framework, có sử 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 tĩnh sử dụng cho webapp (html, css, js file). - File
app.js
: Phần chính chứa logic của ứng dụng. - File
package.json
: Quản lý thông tin,dependencies
của ứng dụng.
- Cài đặt các thư viện, frameword cần sử 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ệnhnpm install --save socket.io express body-parser pm2
vànpm install --save-dev bootstrap jquery
Hoặc sử dụng filepackage.json
với nội dung:
{
"name": "Pm2API_demo",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "node app.js",
"dev": "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",
"devDependencies": {
"bootstrap": "^3.3.7",
"jquery": "^3.2.1"
}
}
Sau đó 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
, đổi tên thành boostrap
- Xây dựng ứng dụng 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
Nội dung hiển thị danh sách các tiến trình
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="css/boostrap/css/bootstrap.min.css">
<link 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-responsive 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
Kết nối tới socket server
$(document).ready(function () {
var socket = io();
});
- Hiển thị danh sách các process sẽ quản lý
pm2.describe
cho phép lấy thông tin của process đang được quản lý bằngpm2
theo tên hoặc pm2_id. Hàm này trả lại một array(trường hợp tiến trình ởcluster mode
) các object chứ thông tin 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 "online":
return `<span class="label label-success">${status}</span>`;
default:
return `<span class="label label-default">${status}</span>`;
}
}
function getPowerButton(status) {
if (status === "online") {
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);
Sử dụng http request để điều khiển process.
Chỉ action start
chúng ta mới khai báo đầy đủ thông tin 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 từng 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 lập trình viên nodejs trong việc monitor ứng dụng của mình.
Chúng ta có thể sử dụng PM2 API triền khai nhiều hướng ứng dụng vào thực tế các dự án:
Ví dụ: Realtime status của process. Trong bài viết mỗi khi cập nhật trạng thái của process ta lại phải reload lại trang web, 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, online, restart overlimit ...
Tài liệu tham khảo
All rights reserved