在实际应用中,咱们常常会碰到这样一些需求,例如天天统计数据,监控服务器状态,按期清理缓存数据,或者天天给运营人员发送相关邮件。这时咱们就须要编写计划任务脚本并定时执行。javascript
weroll 提供了ScheduleManager 来实现简单的计划任务功能,目前能够实现Timer任务和Daily任务两种。html
Timer 任务 - 每隔一段时间执行一次前端
Daily 任务 - 天天在指定时间点执行一次java
接下来咱们来尝试实现一个简单的服务器性能监控的应用,用来监控机器的CPU和内存使用率,并以图表的形式展现出来,天天凌晨的时候把前一天的监控状况整理发送邮件给运维人员。node
上一篇《快速搭建Node.js应用程序脚手架 (1)- 2分钟Demo》咱们已经介绍了什么是weroll,以及快速搭建一个Node.js应用。jquery
假设咱们已经建立好了工程,为了更好的理解,我把最后完成时的工程目录文件结构截图展现出来:git
如今,让咱们开始编写代码!github
首先咱们须要在 ./server/schedule 目录中建立一个脚本文件,咱们取名为 monit.js,用来计算并记录CPU和内存的使用率数据,这里咱们使用了 cpu-stat 来得到CPU的使用率数据。web
/* ./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 中配置的:ajax
/* ./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 来执行计划任务了。一般在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,让前端页面经过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(); }); //...
实际收到邮件的效果: