使用 collectd 监控 pm2 应用性能

pm2 是 node.js 应用的产品级进程管理器。

PM2 is a production process manager for Node.js applications with a built-in load balancer. It allows you to keep applications alive forever, to reload them without downtime and to facilitate common system admin tasks.

关键性能指标

通过 pm2 可以获取到 node.js 应用的几个关键性能指标:

  • Memory used

    node.js 应用的内存占用。

    node.js(v8) 通过垃圾收集(GC)技术进行自动内存管理,这里测量到的内存占用还包含一部分未回收的垃圾。

  • CPU used

    node.js 应用的 CPU 占用。

    node.js 是单线程模型,虽然所有 I/O 操作是异步的,但是代码指令执行是同步的,过多的请求处理或消耗 CPU 的操作会导致应用响应速度变慢,可能无法提供正常的服务。

  • Loop delay

    node.js 应用事件循环的延迟。

    pm2 测量 node.js 应用 Loop delay 的逻辑如下:

    记下开始时间( process.hrtime )

    设置 1 秒钟的定时器(setInterval)

    定时器触发时获取结束时间( process.hrtime )

    结束时间与开始时间的时间差减去 1 秒钟就是 Loop delay

    具体实现请查阅 pm2 源代码:node_modules/pm2/node_modules/pmx/lib/probes/pacemaker.js

一般来说 Loop delayCPU used 指标是正相关的,但是如果 node.js 应用不小心调用了一些同步 I/O 操作或 I/O 出现瓶颈,则会出现 CPU used 低但是 Loop delay 高的情况。

  • restart_time 及 unstable_restarts

    node.js(javascript)是一门动态语言,很少运行到的代码分支里一个错误的变量引用就可能导致整个应用异常退出,pm2 会在 node.js 应用退出时自动重新拉起应用,但这可能会掩盖潜藏的问题(BUG),监控 node.js 应用的重启次数可以及时发现这种问题(BUG)。

上线新代码后,通过观测这几个关键性能指标,以及与历史记录进行对比,可以用来评估新代码的运行效率与质量。

收集性能指标

通过 pm2 收集 node.js 应用性能指标的脚本 /usr/local/bin/collectd-pm2.js

var os = require('os');
var exec = require('child_process').exec;


var hostname = process.env.COLLECTD_HOSTNAME || os.hostname();
var interval = parseInt(process.env.COLLECTD_INTERVAL, 10) || 1;

function collect () {
    exec('pm2 jlist', function (error, stdout, stderr) {
        if (error) {
            process.stderr.write(error.toString());
            process.exit(1);
        }

        if (stderr) {
            process.stderr.write(stderr.toString() + "\n");
        }

        var timestamp = Math.floor(Date.now() / 1000);
        var list = JSON.parse(stdout);
        list.forEach(function (item) {
            var name = '';
            for(var i = 0, n = item.name.length; i < n; ++i) {
                name += item.name[i].match(/^[0-9a-zA-Z]+$/) ? item.name[i] : '_';
            }
            process.stdout.write("PUTVAL \"" + hostname + "/" + name + "-loop_delay" + "/delay-" + item.pm_id + "\" interval=" + interval + " " + timestamp + ":" + item.pm2_env.axm_monitor["Loop delay"].value.replace('ms', '') + "\n");
            process.stdout.write("PUTVAL \"" + hostname + "/" + name + "-memory_used" + "/gauge-" + item.pm_id + "\" interval=" + interval + " " + timestamp + ":" + item.monit.memory + "\n");
            process.stdout.write("PUTVAL \"" + hostname + "/" + name + "-cpu_used" + "/gauge-" + item.pm_id + "\" interval=" + interval + " " + timestamp + ":" + item.monit.cpu + "\n");
            process.stdout.write("PUTVAL \"" + hostname + "/" + name + "-restart_time" + "/gauge-" + item.pm_id + "\" interval=" + interval + " " + timestamp + ":" + item.pm2_env.restart_time + "\n");
            process.stdout.write("PUTVAL \"" + hostname + "/" + name + "-unstable_restarts" + "/gauge-" + item.pm_id + "\" interval=" + interval + " " + timestamp + ":" + item.pm2_env.unstable_restarts + "\n");
        });

        setTimeout(collect, interval*1000);
    });
}

collect();

pm2 是使用 root 帐号运行的,collectd exec 插件不允许以 root 权限运行收集统计的程序(collectd-pm2.js),一个简单的方法是用 c 写一个包裹程序,使用 setuid 切换到 root 帐号。

collectd-pm2-root.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>


int main(int argc, char* argv[]) {
    if (setuid(0) == -1 || setgid(0) == -1) {
        perror("setuid or setgid to root user error");
        fprintf(stderr, "\npermit setuid and setgid to root user: \n\tchown root:root %s\n\tchmod 4755 %s\n", argv[0], argv[0]);
        return EXIT_FAILURE;
    }

    return system("/bin/bash -c 'export PM2_HOME=${PM2_HOME:-~root/.pm2}; node /usr/local/bin/collectd-pm2.js'");
}

编译安装

gcc -O2 collectd-pm2-root.c -o collectd-pm2-root
cp collectd-pm2-root /usr/local/bin
chown root:root /usr/local/bin/collectd-pm2-root
chmod 4755 /usr/local/bin/collectd-pm2-root

配置 collectd,修改 collectd.conf

LoadPlugin exec

<Plugin exec>
    Exec "nobody:nobody" "/usr/local/bin/collectd-pm2-root"
</Plugin>

测试运行统计收集脚本

sudo -u nobody -g nobody /usr/local/bin/collectd-pm2-root

重启 collectd 生效即可。

以上代码已在 github 开源:https://github.com/tangxinfa/collectd-pm2