Read more

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

  1. 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.
  1. 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ệnh npm install --save socket.io express body-parser pm2npm install --save-dev bootstrap jquery Hoặc sử dụng file package.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
  1. 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();
});
  1. 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ằng pm2 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>
        `;
    }
});
  1. 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);
                }
            });
        }
    });
 ...
  1. 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 ...