在实际应用中,我们经常会碰到这样一些需求,例如每天统计数据,监控服务器状态,定期清理缓存数据,或者每天给运营人员发送相关邮件。这时我们就需要编写计划任务脚本并定时执行。
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();
});
//...
实际收到邮件的效果:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。