iT邦幫忙

4

使用cluster模組做出簡單的node.js的hosting架構

這部份原先是想在jsdc分享的展示,不過當時時間不夠,結果就沒有做這部份的展示。

node.js的模組功能有幾個必須了解的限制:

  1. 他是同步執行的,並不符合AMD的規格
  2. 他會做模組的cache,所以已經載入過的模組,就算改過模組程式,再次呼叫require時,還是使用cache中的版本,並不會用更新過的。

第二個限制,會讓deploy變得困難,每次都要重新啟動整個程式,不過利用cluster模組,搭配fs.watch的功能,就可以讓每次程式有變動就自動重新載入worker,來克服這個問題。

當然,如果程式寫壞,上線就自動死掉XD
簡單的邏輯,就是在master偵測程式變動,有變動時就重啟worker,這樣就不需要停止程式然後再啟動程式。

用一個簡單的hello world範例來看:

var cluster = require('cluster');
var http = require('http');
var numCPUs = require('os').cpus().length;
var fs = require('fs');

if (cluster.isMaster) {
    // Fork workers.
    var pool = {};
    for (var i = 0; i < numCPUs; i++) {
        createworker(pool);
    }

    cluster.on('death', function(worker) {
        console.log('worker ' + worker.pid + ' died');
        createworker(pool);
    });
  
    fs.watch('./test005.js', function() {
        for(var i in pool) {
            pool[i].send({cmd: 'suicide'});
        }
    });

    function createworker(pool) {
        var worker = cluster.fork();
        var sid = new Date().getTime() + '' + Math.random();
        worker.on('message', function(msg) {
            if(msg.id) {
                pool[msg.id] = null;
                delete pool[msg.id];
            }
        });
        pool[sid] = worker;
        worker.send({id: sid});
        console.log('worker created: ' + sid);
    }
 
} else {
    // Worker processes have a http server.
    http.Server(function(req, res) {
        res.writeHead(200);
        res.end("hello world\n");
    }).listen(8000);
    
    var id = "";
    
    process.on('message', function(msg) {
        if(msg.id) {
            id = msg.id;
        }
        if(msg.cmd && msg.cmd === 'suicide') {
            process.send({id: id});
            process.exit();
        }
    });
}

他做的事情很簡單,就是用fs.watch來監控檔案,有變動時,就叫worker自殺,然後master會自動重啟掛掉的worker。

先把程式跑起來:

從瀏覽器可以看到:

然後修改一下程式,把hello world改成hello world reload,存檔後就會看到伺服器有反應:

瀏覽器refresh一下,就會看到hello world變成hello world reload:

其實程式裡面,伺服器只有幾行,其他都是cluster相關的東西。對於只想專注於伺服器程式開發,其實可以把程式拆成兩個部分,配合一些「策略」定義,就可以把伺服器程式放進模組。例如:

  1. 模組必須提供一個start函數,來啟動程式
  2. 模組必須提供一個stop函數,來結束程式
  3. deploy時,模組的檔案預設會放進main目錄中

這樣,伺服器的程式調整成

var cluster = require('cluster');
var numCPUs = require('os').cpus().length;
var fs = require('fs');

if (cluster.isMaster) {
    // Fork workers.
    var pool = {};
    for (var i = 0; i < numCPUs; i++) {
        createworker(pool);
    }

    cluster.on('death', function(worker) {
        console.log('worker ' + worker.pid + ' died');
        createworker(pool);
    });
  
    fs.watch('./main', function() {
        for(var i in pool) {
            pool[i].send({cmd: 'suicide'});
        }
    });

    function createworker(pool) {
        var worker = cluster.fork();
        var sid = new Date().getTime() + '' + Math.random();
        worker.on('message', function(msg) {
            if(msg.id) {
                pool[msg.id] = null;
                delete pool[msg.id];
            }
        });
        pool[sid] = worker;
        worker.send({id: sid});
        console.log('worker created: ' + sid);
    }
 
} else {
    // Worker processes have a http server.
    var main = require('./main');
    main.start();
    
    var id = "";
    
    process.on('message', function(msg) {
        if(msg.id) {
            id = msg.id;
        }
        if(msg.cmd && msg.cmd === 'suicide') {
            main.stop();
            process.send({id: id});
            process.exit();
        }
    });
}

在程式目錄中,新增一個main目錄,然後把伺服器模組的index.js放進去,程式如下:

var http = require('http');

module.exports = {
    start: function() {
        http.Server(function(req, res) {
            res.writeHead(200);
            res.end("hello world reload\n");
        }).listen(8000);
    },
    stop: function(){}
};

當然,做成模組比較好的方式是用package.json來描述,不過這裡只是做概念驗證,所以就用index.js來代替,這樣速度會比較慢就是了。

改過之後重啟伺服器,然後去修改模組中的index.js,可以看到有改並的話都會重啟worker,然後從瀏覽器就可以看到改變的結果。

接下來,大部分的時候,就只要去維護main裡面的東西,而且每次deploy,伺服器就會重啟worker,自動apply變更。這樣就很像做了一個簡單的hosting服務,只要依照上述的規則來寫模組,就可以在不知道hosting程式長怎樣的狀況下,專注於寫main模組中的程式。。


1 則留言

0
fillano
iT邦超人 1 級 ‧ 2012-05-22 16:50:40

補充一下。

其實用fs.watch目錄的方式來實作,會有一個問題:每個檔案更新時,worker都會重啟一次,如果在做批次更新,一次更新很多檔案,就會造成worker不斷重啟,這樣會影響服務及效率。

也許可以從deploy工具下手,例如會在main目錄中自動新增一個timestamp檔,而程式就調整為fs.watch這個檔案。然後每次deploy完畢時,會去touch main/timestamp。這樣就可以讓每次deploy新的版本時,worker只會重啟一次。

我要留言

立即登入留言