Node穩定性的研究心得


目前大部分Web服務器,如Apache,都使用多線程的方式響應多用戶請求,即一個線程服務一個用戶請求。這種模式其中一個好處是,當某個請求的線程上拋出的異常沒被捕獲,只會影響當前這個線程,不會影響其他請求。

由於Node執行在單線程上,一旦線程上拋的異常沒有被捕獲,就會引起整個進程的崩潰。所以對Node服務的異常處理、保證進程的穩定性非常重要。

再好的程序員也不敢保證自己的代碼不出現異常,除了盡可能捕獲可能出現的異常,我們還需要通過一些規范減少異常發生,通過單元測試輔助我們驗證代碼,通過一些工具保證服務的穩定性。下面我從這幾個方面探討如何保證Node的穩定性。

一 異常捕獲

提升穩定性最直接的方式就是盡可能的捕捉異常,Node提供3種方式。

1.1 try/catch

在大多數語言中,try/catch是捕獲異常的好手,能確保我們的代碼不進入不可控流程。但是由於Node回調/異步的特性,我們無法通過try/catch來捕捉所有的異常,看下面的示例:

1
try {
    process.nextTick(function () {
        throw new Error("error");
    });
} catch (err) {
    //can not catch it
    console.log(err);
}


try {
    setTimeout(function(){
        throw new Error("error");
    },1)
} catch (err) {
    //can not catch it
    console.log(err);
}

上面的代碼沒有像預期的那樣幫我們捕獲異常,后果就是這個未被捕獲的異常導致整個Node進程crash,所以Node中try/catch的方式不是那么管用。
如果有一種方法能幫我們全局捕獲異常,Node服務就不會輕易掛掉了。Node確實提供了這種方式,不過卻不能完全滿足我們的需求。

1.2 uncaughtException

當一個異常未被捕獲,冒泡回歸到事件循環中就會觸發uncaughtException事件。

1
process.on('uncaughtException', function(err) {
    console.error('Error caught in uncaughtException event:', err);
});


try {
    setTimeout(function(){
        throw new Error("error");
    },1)
} catch (err) {
    //can not catch it
    console.log(err);
}

只要給uncaughtException配置了回調,Node進程不會異常退出,但異常發生的上下文已經丟失,我們無法給出友好的返回,比如告訴用戶哪里出問題了。而且由於uncaughtException事件發生后,會丟失當前環境的堆棧,可能導致Node不能正常進行內存回收,從而導致內存泄露。所以,uncaughtException的正確使用姿勢是,當uncaughtException觸發,記錄error日志,然后結束Node進程,我們通過日志監控,報警及時解決異常。

1
process.on('uncaughtException', function(err) {
    // 記錄日志
    logger(err);
    // 結束進程
    process.exit(1);
});

1.3 domain

為了彌補try/catch和uncaughtException的不足,在node v0.8+版本的時候,發布了一個模塊domain,這個模塊能捕捉異步回調中出現的異常。
看下面的示例:

1
var d = domain.create();

process.on('uncaughtException', function(err) {
    console.error(err);
});

d.on('error', function(err) {
    console.error('Error caught by domain:', err);
});

d.run(function() {
    process.nextTick(function() {
        throw new Error("test domain");
    });
});

運行代碼我們會發現,異常會被domain捕獲到,uncaughtException不會被觸發。雖然我們對domain模塊寄予厚望,不過目前domain模塊的評級為“Unstable”,因為存在不少性能以及穩定性問題。
關於domain的詳細介紹,可以看看以下幾篇文章:

二 阻止異常發生

我們當然無法阻止異常的發生,這里說的阻止異常發生,是希望大家能養成良好的編碼習慣,嚴謹的思維邏輯,來盡可能減少代碼拋出未捕獲異常。

2.1 良好的異常處理習慣

  • 異步API編寫規范:由於異步調用中回調函數里的異常無法被外部捕獲,所以我們將API內部發生的異常作為第一個參數傳遞給回調函數,包括NodeJS官方的API是遵循這個規范的。
1
fs.readFile('/t.txt', function (err, data) {
  if (err) throw err;
  console.log(data);
});


// 不推薦的做法
function fun(options,callback){
    if(!options{
        throw new Error("..")
    }
}

// 推薦的做法
function fun(options,callback){
    if(!options){
        callback(err,null)
    }
}
  • 嚴格校驗用戶的輸入
1
function doSth(cb){
    if(typeof cb === 'function'){
        cb();
    }
}
  • 使用try/catch處理可能出現異常的代碼
1
var obj;
try{
    obj = JSON.parse('')
}catch(e){
    obj = {};
}
  • 不要直接在controller中拋異常,應該用500等狀態更友好的返回錯誤
1
// 不推薦的做法
app.get('/item.html', function (req, res, next) {
    if(!query["id"]){
        throw new Error('no item id');
    }
});

// 推薦的做法
app.get('/item.html', function (req, res, next) {
    if(!query["id"]){
        next();
    }
});

2.2 單元測試

單元測試的重要性想必所有人都清楚,JS代碼跑在瀏覽器端的時候我們未必會做,因為異常通常只會影響部分人使用的部分功能,不足以引起很多人的重視。JS代碼跑在服務端情況完全不一樣了,一旦出現異常,影響的是所有的用戶,所以單元測試就顯得非常重要。

至於如何在NodeJS中寫單元測試,可以看看兩位大神的分享:

2.3 記錄日志

還是那句說,誰也不敢保證自己的代碼不出現異常,因為有運行環境,網絡環境等各種不穩定因素,所以建立健全的排查和跟蹤機制就顯得很重要,而日志就是實現這種機制的關鍵。
阿里線上環境已經有完善的日志監控體現,我們要做的就是去學會如何使用他。

ali-logger:http://search.npm.taobao.net/package/ali-logger

三 多進程架構

3.1 cluster

cluster模塊用於創建共享端口的多進程模式,這種模式使多個進程間共享一個監聽狀態的socket,並由系統將accept的connection分配給不同的子進程。
文檔上有一個簡單的示例:

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

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

  cluster.on('exit', function(worker, code, signal) {
    console.log('worker ' + worker.process.pid + ' died');
  });
} else {
  // Workers can share any TCP connection
  // In this case its a HTTP server
  http.createServer(function(req, res) {
    res.writeHead(200);
    res.end("hello world\n");
  }).listen(8000);
}

利用cluster,我們可以根據CPU的數量創建多個worker進程,用戶的請求會被分配到不同的進程上,如果某個進程出現異常,可以直接將這個進程crash掉,而不會影響其他進程。
關於cluster實現原理的介紹文章非常多,想深入了解的可以自行搜索一下。

3.1.1 graceful + recluster

數據產品中使用graceful + recluster兩個模塊實現多進程和服務器穩定性的工作,分享一下使用方法:

  • graceful : 當uncaughtException觸發后,延遲一段時間退出進程
  • recluster : 實現多進程,並且當有進程退出實現自動重啟。

app.js

1
var app = require('../app');
var graceful = require('graceful');

var server = app.listen(app.get('port'), function() {
    debug('Express server listening on port ' + server.address().port);
});

graceful({
    server: server,
    killTimeout: 30000,
    error:function(e){
        logger.error(e);
    }
});

server.js

var recluster = require('recluster'),
    path = require('path');

var cluster = recluster(path.join(__dirname, 'app.js'));
cluster.run();

process.on('SIGUSR2', function() {
    console.log('Got SIGUSR2, reloading cluster...');
    cluster.reload();
});

console.log("spawned cluster, kill -s SIGUSR2", process.pid, "to reload");

開發環境,直接通過app啟動:

$ node server.js

生產環境, 啟動多個 worker:


$ node server.js

3.1.2 pm2

如果你覺得graceful + recluster的方式太復雜,那么pm2肯定是你最理想的選擇。
pm2非常強大,生產環境使用pm2啟動你的Node服務是個不錯的選擇,他能自動利用你的多核cup,完善的監控,日志記錄等等..

pm2使用非常簡單:



$ npm install pm2@latest -g

$ pm2 start app.js

$ pm2 list

pm2

如果你想更多的了解pm2,可以直接看pm2的文檔:https://github.com/Unitech/pm2

不過正是由於pm2功能太過強大,我們沒有選擇pm2,因為他太復雜了,感覺還沒有能力駕馭他,特別是萬一出現問題,我們還不知道如何解決。也許我們的顧慮是多余的,不過需要一點時間去了解他。

小結

剛開始學習Node的使用,對Node確實有點小擔心,因為使用別的語言做Web服務,根本不用擔心因為一個錯誤導致整個服務crash的問題。
隨着對Node的了解,我掌握了一些技巧,也打消了一些顧慮。
不過畢竟Node應用經驗有限,所以歡迎大家一起探討,積累更多寶貴的經驗。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM