1

在实际应用中,我们经常会碰到这样一些需求,例如每天统计数据,监控服务器状态,定期清理缓存数据,或者每天给运营人员发送相关邮件。这时我们就需要编写计划任务脚本并定时执行。

weroll 提供了ScheduleManager 来实现简单的计划任务功能,目前可以实现Timer任务和Daily任务两种。

  • Timer 任务 - 每隔一段时间执行一次

  • Daily 任务 - 每天在指定时间点执行一次

接下来我们来尝试实现一个简单的服务器性能监控的应用,用来监控机器的CPU和内存使用率,并以图表的形式展示出来,每天凌晨的时候把前一天的监控情况整理发送邮件给运维人员。

上一篇《快速搭建Node.js应用程序脚手架 (1)- 2分钟Demo》我们已经介绍了什么是weroll,以及快速搭建一个Node.js应用。

假设我们已经创建好了工程,为了更好的理解,我把最后完成时的工程目录文件结构截图展示出来:
工程目录文件结构

现在,让我们开始编写代码!

计划任务脚本编写

首先我们需要在 ./server/schedule 目录中创建一个脚本文件,我们取名为 monit.js,用来计算并记录CPU和内存的使用率数据,这里我们使用了 cpu-stat 来获得CPU的使用率数据。

/* ./server/schedule/monit.js */

var cpuStat = require('cpu-stat');
var os = require('os');

//exec方法是计划任务脚本的入口函数
//当脚本执行完毕后,需要调用callBack(err)来结束本次任务
exports.exec = function(callBack, option) {

    //最多存储几次数据,默认30次
    var max = option.max || 30;
    //每次监控的时间间隔,默认10秒一次
    var duration = (option.duration || 10) * 1000;
    //计算CPU使用率的耗时,默认1秒,时间越长越精确
    var checkTime = (option.checkTime || 1) * 1000;

    //这里偷懒,把监控数据存储在global对象里
    var Performance = global.Performance;
    if (!Performance) {
        //初始化监控数据
        Performance = { cpu:[], mem:[] };
        global.Performance = Performance;

        //因为不是持久化数据,为了使图表更好看
        //这里假设之前30次监控数据都为0
        var now = Date.now();
        for (var i = 0; i < 30; i++) {
            var time = now - i * duration;
            Performance.cpu.push([ time, 0 ]);
            Performance.mem.push([ time, 0 ]);
        }
    }

    //使用cpu-stat获得CPU使用率数据
    cpuStat.usagePercent({ sampleMs:checkTime }, function(err, cpuPercent) {
        if (err) {
            console.error(err);
            cpuPercent = 0;
        }
        var Performance = global.Performance;
        var time = Date.now();

        //最多记录max次监控数据, 把旧数据移除
        if (Performance.cpu.length >= max) {
            Performance.cpu.shift();
        }
        
        //记录CPU使用率
        Performance.cpu.push([ time, cpuPercent ]);
        console.log('cpu: ', cpuPercent, '%');
        
        //计算内存使用率
        var totalMem = os.totalmem();
        var usedMem = totalMem - os.freemem();
        var memPercent = 100 * usedMem / totalMem;

        //记录内存使用率
        Performance.mem.push([ time, memPercent ]);
        console.log('mem: ', memPercent, '%');

        //本次任务完成
        callBack();
    });
}

monit.js 已经完成,接下来我们需要配置它的执行规则。计划任务的运行规则是在 ./server/config/localdev/schedule.json 中配置的:

/* ./server/config/localdev/schedule.json */
{
    "ver": "1.0.0",
    "list":[
        { 
            "type":1, 
            "duration":10, 
            "script":"monit", 
            "initExecute":true, 
            "option":{ 
                "duration":10, 
                "checkTime":5, 
                "max":8640 
            } 
        }
    ]
}

以上的配置定义了 monit.js 脚本是一个 Timer脚本 ,每隔10秒运行一次(应用启动时会马上运行一次),并设置了脚本使用参数:10秒一次计算,计算CPU使用率将耗时5秒,最多存储8640次监控记录(如果10秒一次的话,那么就是最多保留24小时的监控数据)

启用ScheduleManager

脚本和配置已经完成了,现在我们可以启用 ScheduleManager 来执行计划任务了。通常在weroll应用程序中,我们会在程序入口 main.js 启动计划任务:

/* ./main.js */
var App = require("weroll/App");
var app = new App();

//获得全局配置 ./server/config/localdev/setting.js
var Setting = global.SETTING;

app.addTask(function(cb) {
    //启动web服务器
    require("weroll/web/WebApp").start(Setting, function(webApp) {
        cb();
    });
});
app.addTask(function() {
    //启动计划任务管理器
    require("weroll/schedule/ScheduleManager").start();
});

//开始执行初始化
app.run();

现在我们可以运行程序看看计划任务的执行效果。

$ node main.js -env=localdev -debug

图片描述

定义API获取监控数据

经过上一步,我们已经可以在终端输出监控数据了。接着我们需要定义一个API,让前端页面通过ajax方式来获取最新的监控数据。

在weroll应用程序中,API业务逻辑需要定义在 ./server/service 目录中,这里我们取名为 SystemService.js:

exports.config = {
    name: "system",   //定义API的前缀名
    enabled: true,
    //下面我们定义并暴露了一个API方法
    //完整的API名为:system.performance
    security: {
        //@performance 获得服务器CPU和内存监控数据
        "performance":{ needLogin:false }
    }
};

exports.performance = function(req, res, params) {
    //获取存储在全局对象中的监控数据
    var Performance = global.Performance || { cpu:[], mem:[] };
    var result = {};
    //只需要显示最新的30次监控数据
    for (var key in Performance) {
        result[key] = Performance[key].slice(-30);
    }
    //将数据返回给客户端
    res.sayOK(result);
}

这样我们就定义了一个名为 system.performance 的API,客户端可以通过HTTP POST方式来调用这个接口。下一步我们会实现客户端对API的调用。

详细的API开发说明请阅读 weroll - Guide: API

实时图表显示监控数据

接着我们还要实现一个web页面,调用API获取CPU和内存的监控数据,并用图表的方式来显示。前端图表组件我们使用 链接描述Chart.js,详细使用方式请看它的官方文档,这里我们不做详述。

在weroll应用程序中,页面路由需要定义在 ./server/router 目录中,这里我们取名为 index.js:

/* ./server/router/index.js */

function renderIndexPage(req, res, output, user) {
    output({ });
}

exports.getRouterMap = function() {
    return [
        { url: "/", view: "index", handle: renderIndexPage, needLogin:false },
        { url: "/index", view: "index", handle: renderIndexPage, needLogin:false }
    ];
}

非常简单,没有任何逻辑。
接着是html页面的实现,我们在 ./client/views 目录下创建 index.html :(请无视作者蹩脚的前端代码)

<!-- ./client/views/index.html -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Monit</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge,Chrome=1">
    <!-- import js -->
    <script type="text/javascript" src="{{setting.RES_CDN_DOMAIN}}/js/jquery-3.1.1.min.js"></script>
    <script type="text/javascript" src="{{setting.RES_CDN_DOMAIN}}/js/Chart.min.js"></script>
</head>
<body>

<!-- CPU监控图表 -->
<div style="width:800px; height:300px;">
    <canvas id="cpuChart" width="800" height="300"></canvas>
</div>

<!-- 内存监控图表 -->
<div style="width:800px; height:300px;">
    <canvas id="memChart" width="800" height="300"></canvas>
</div>

<script>
    //定义Chart.js组件的样式
    var options = {
        scales: {
            yAxes: [{
                display: true,
                ticks: {
                    beginAtZero: true,
                    steps:20,
                    stepValue:5,
                    max:100
                }
            }]
        }
    };

    var cpuChartData = {
        labels: [],
        datasets: [
            {
                label: "CPU",
                fill: true,
                lineTension: 0.1,
                backgroundColor: "rgba(75,192,192,0.4)",
                borderColor: "rgba(75,192,192,1)",
                borderCapStyle: 'butt',
                borderDash: [],
                borderDashOffset: 0.0,
                borderJoinStyle: 'miter',
                pointBorderColor: "rgba(75,192,192,1)",
                pointBackgroundColor: "#fff",
                pointBorderWidth: 1,
                pointHoverRadius: 5,
                pointHoverBackgroundColor: "rgba(75,192,192,1)",
                pointHoverBorderColor: "rgba(220,220,220,1)",
                pointHoverBorderWidth: 2,
                pointRadius: 1,
                pointHitRadius: 10,
                data: []
            }
        ]
    };

    var cpuChart = new Chart(document.getElementById("cpuChart"), {
        type: 'line',
        data: cpuChartData,
        options: options
    });

    var memChartData = {
        labels: [],
        datasets: [
            {
                label: "Memory",
                fill: true,
                lineTension: 0.1,
                backgroundColor: "rgba(51, 102, 255,0.4)",
                borderColor: "rgba(51, 102, 255,1)",
                borderCapStyle: 'butt',
                borderDash: [],
                borderDashOffset: 0.0,
                borderJoinStyle: 'miter',
                pointBorderColor: "rgba(51, 102, 255,1)",
                pointBackgroundColor: "#fff",
                pointBorderWidth: 1,
                pointHoverRadius: 5,
                pointHoverBackgroundColor: "rgba(51, 102, 255,1)",
                pointHoverBorderColor: "rgba(220,220,220,1)",
                pointHoverBorderWidth: 2,
                pointRadius: 1,
                pointHitRadius: 10,
                data: []
            }
        ]
    };

    var memChart = new Chart(document.getElementById("memChart"), {
        type: 'line',
        data: memChartData,
        options: options
    });

    //格式化时间,将时间戳转为hh:mm:ss的格式
    function formatTime(time) {
        var date = new Date();
        date.setTime(time);
        return (date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes()) + ":" + (date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds());
    }

    //调用服务端API
    function callAPI(method, data, callBack) {
        var params = {};
        params.method = method;  //请求的API名
        params.data = data;      //请求参数

        $.ajax({
            type: "post",  //POST方式
            url: "http://localhost:3000/api",  //API入口
            headers: {
                "Content-Type": "application/json; charset=UTF-8"
            },
            data: JSON.stringify(params),
            success: function (result, status, xhr) {
                if (result.code == 1) {
                    //code = 1 说明API调用没有异常
                    callBack && callBack(result.data);
                } else {
                    alert('call api error: [' + result.code + '] ' + result.msg);
                }
            },
            error: function (XMLHttpRequest, textStatus, errorThrown) {
                alert('call api error: ' + textStatus);
            }
        });
    }

    //更新图表,刷新数据
    function updateChart(chart, chartData, newData) {
        chartData.datasets[0].data.length = 0;
        chartData.labels.length = 0;
        newData.forEach(function (obj) {
            chartData.labels.push(formatTime(obj[0]));
            chartData.datasets[0].data.push(parseInt(obj[1]));
        });
        chart.update();
    }

    //每隔10秒调用一次API
    function update() {
        callAPI("system.performance", {}, function(data) {
            updateChart(cpuChart, cpuChartData, data.cpu);
            updateChart(memChart, memChartData, data.mem);
            setTimeout(update, 10000);
        });
    }

    update();

</script>
</body>
</html>

详细的路由和页面开发说明请参考文档 weroll - Guide: View Router

目前为止我们基本已经完成了监控应用的开发,现在让我们来重新运行应用,看看效果吧!

图片描述

实现自动发送每日邮件

最后,我们还需要实现每日发送邮件给运维人员,报告昨日服务器的监控状况。这时我们就需要编写 Daily脚本 来实现这个功能。

同样,我们先编写计划任务脚本,创建文件 ./server/schedule/report.js,同样实现 exec 方法:

/* ./server/schedule/report.js */

//使用weroll内置的邮件工具类发送邮件
//你也可以用其他邮件库实现发送
var MailUtil = require('weroll/utils/MailUtil');

exports.exec = function(callBack, option) {

    var Performance = global.Performance;
    if (!Performance) return callBack(new Error("invalid Performance data"));

    var result = { cpu:{}, mem:{} }, total = 0;

    var yesterdayEnd = new Date();
    yesterdayEnd.setHours(0);
    yesterdayEnd.setMinutes(0);
    yesterdayEnd.setSeconds(0);
    yesterdayEnd.setMilliseconds(0);
    //yesterdayEnd 即是新的一天的开始, 昨日的结束
    yesterdayEnd = yesterdayEnd.getTime() - 1;

    var cpu = Performance.cpu || [];
    result.cpu.max = cpu[0][11];
    cpu.forEach(function(obj) {
        if (obj[0] > yesterdayEnd) return;
        //获得昨日CPU使用率峰值和出现时间
        result.cpu.max = Math.max(obj[1], result.cpu.max);
        result.cpu.maxTime = obj[0];
        total += obj[1];
    });
    //获得昨日CPU使用率平均值
    result.cpu.avg = total / cpu.length;

    total = 0;
    var mem = Performance.mem || [];
    result.mem.max = mem[0][12];
    mem.forEach(function(obj) {
        if (obj[0] > yesterdayEnd) return;
        //获得昨日内存使用率峰值和出现时间
        result.mem.max = Math.max(obj[1], result.mem.max);
        result.mem.maxTime = obj[0];
        total += obj[1];
    });
    //获得昨日内存使用率平均值
    result.mem.avg = total / mem.length;

    //撰写报告, 发送邮件
    var title = (option.title || "{DATE} 服务器性能监控报告").replace("{DATE}", moment(yesterdayEnd).format("YYYY-MM-DD"));
    //定义文本格式的正文
    var plain = `CPU 平均使用率: ${Number(result.cpu.avg).toFixed(2)}%\r\n`;
    plain += `CPU 峰值: ${Number(result.cpu.max).toFixed(2)}%    出现于: ${moment(result.cpu.maxTime).format("HH:mm:ss")}\r\n\r\n`;
    plain += `内存 平均使用率: ${Number(result.cpu.avg).toFixed(2)}%\r\n`;
    plain += `内存 峰值: ${Number(result.cpu.max).toFixed(2)}%    出现于: ${moment(result.cpu.maxTime).format("HH:mm:ss")}\r\n\r\n`;
    plain += "\r\n\r\n" + (option.senderName || "");

    //定义HTML格式的正文
    var html = plain.replace(/\r\n/g, '<br>');

    var content = { plain:plain, html:html };

    MailUtil.send(option.sendTo, title, content, function(err) {
        //结束
        callBack();
    });

}

然后配置计划任务的执行规则,在 ./server/config/localdev/schedule.json 里的 list 数组添加:

{ "type":2, "time":"00:00:10", "script":"report", "option":{ "sendTo":"admin@xxx.com", "senderName":"Robot" } }

以上规则定义了 report.js 脚本将在每天00:00:10的时候执行一次,将邮件发送给admin@xxx.com。

最后,我们需要初始化邮件发送工具类 MailUtil

/* ./main.js */
//...
app.addTask(function(cb) {
    //初始化邮件服务
    var config = {
        smtp:{
            user:"developer@magicfish.cn",
            password:"xxxxxxxxx",
            host:"smtp.xxxx.com",
            port:465,
            ssl:true
        },
        sender:"developer@magicfish.cn",
        senderName:"Robot"
    };
    require("weroll/utils/MailUtil").init(config);

    //启动计划任务管理器
    require("weroll/schedule/ScheduleManager").start();
    cb();
});
//...

实际收到邮件的效果:
图片描述

项目源代码:https://github.com/jayliang70...


哼亨哈唏
43 声望7 粉丝

主业奶爸,副业码农。