目前大部分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兩個模塊實現多進程和服務器穩定性的工作,分享一下使用方法:
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的文檔:https://github.com/Unitech/pm2
不過正是由於pm2功能太過強大,我們沒有選擇pm2,因為他太復雜了,感覺還沒有能力駕馭他,特別是萬一出現問題,我們還不知道如何解決。也許我們的顧慮是多余的,不過需要一點時間去了解他。
小結
剛開始學習Node的使用,對Node確實有點小擔心,因為使用別的語言做Web服務,根本不用擔心因為一個錯誤導致整個服務crash的問題。
隨着對Node的了解,我掌握了一些技巧,也打消了一些顧慮。
不過畢竟Node應用經驗有限,所以歡迎大家一起探討,積累更多寶貴的經驗。