weroll - 快速搭建Node.js應用程序腳手架 (2)- 使用Schedule實現一個服務器性能監控應用

在實際應用中,咱們常常會碰到這樣一些需求,例如天天統計數據,監控服務器狀態,按期清理緩存數據,或者天天給運營人員發送相關郵件。這時咱們就須要編寫計劃任務腳本並定時執行。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

腳本和配置已經完成了,如今咱們能夠啓用 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...

相關文章
相關標籤/搜索