成熟的Web Service技術,例如Fast CGI、J2EE、php,必然會對代碼異常有足夠的保護,好的Web必然會在出錯后給出友好的提示,而不是莫名其妙的等待504超時。
而node.js這里比較年輕,而開發人員就更年輕了,大家都沒有太多的經驗,也沒有太多的參考。
###單進程+PM2
最簡單的方式http處理方式,可以經常見到這樣的模式:
var http = require('http'); http.createServer(function (req, res) { res.end('hello'); }).listen(80);
簡單處理請求,沒有任何的全局異常處理。也就是說,只在業務邏輯中加一些try catch,信心滿滿的說,不會出現未捕獲的異常。
- 一般來說,這樣都沒太多問題,正常返回的時候一切都好。
- 但如果,哪天,某個代碼出現了點bug~~ 誰都不是神,總有bug的時候。
var http = require('http'); http.createServer(function (req, res) { var a; a.b(); res.end('hello'); }).listen(80);
例如這樣,node.js進程馬上會因為報錯而掛掉~~~
聰明的孩子,也許早就聽說世界上有forever、pm2等玩意。
於是,服務啟動就變成
pm2 start index.js
這樣的模式太常見,尤其是內部的小系統。pm2監控node.js進程,一旦掛掉,就重啟。
似乎
這樣也挺好的,簡單易懂,整體的web服務不會受到太大影響。
這種就是最最最簡單的模式:單進程+pm2。
###第一個全局處理:process.on(‘uncaughtException’)
不過,哪里出錯了,似乎都不知道,也不大好,總得記錄一下錯誤在哪里吧?
於是,聰明的孩子又找到了process.on(‘uncaughtException’)
process.on('uncaughtException', function(er){ console.error("process.on('uncaughtException')", er); });
這樣通過log就可以發現哪里出錯了。
而且因為截獲了異常,所以進程也不會掛掉了~~~ 雖然按照官方的說法,一旦出現未處理的異常,還是應該重啟進程,否則可能有不確定的問題。
好了,似乎
這個方式已經很完美了~~~ 程序不掛掉,也能輸出log。那么聰明的孩子還要做更多的事嗎?
###致命問題:出錯后,沒有任何返回
哪天老板體驗了一下產品,正好逮到了一次出錯,此時頁面已經顯示加載中,等了半天之后,終於出現“服務器錯誤”。
可以想象,老板肯定要發話了,做小弟的必然菊花一緊,好了,又有活干了。。。
那么,我們的目標就是,要在出錯后,能友好的返回,告訴用戶系統出錯了,給個有趣的小圖,引導一下用戶稍后重試。
但,單靠process不可能完成啊,因為錯誤來自五湖四海,單憑error信息不可能知道當時的request和response對象是哪個。
此時只能翻書去。
在官網資料中,可以發現domain這個好東西。雖然官方說正在出一個更好的替代品,不過在這出來之前,domain還是很值得一用的。
This module is pending deprecation. Once a replacement API has been finalized, this module will be fully deprecated.
關於domain的文章,在網上挺多的,直接了當,列出最簡單的Web處理方式吧
const domain = require('domain'); const http = require('http'); http.createServer(function (req, res) { var d = domain.create(); d.on('error', function (er) { console.error('Error', er); try { res.writeHead(500); res.end('Error occurred, sorry.'); } catch (er) { console.error('Error sending 500', er, req.url); } }); d.run(handler); function handler() { var a; a.b(); res.end('hello'); } }).listen(80);
上邊的代碼片段,在每次request處理中,生成了一個domain對象,並注冊了error監聽函數。
這里關鍵點是run函數,在d.run(handler)中運行的邏輯,都會受到domain的管理,簡單理解,可以說,給每一個request創建了獨立的沙箱環境。(雖然,事實沒有這么理想)
request的處理邏輯,如果出現未捕獲異常,都會先被domain接收,也就是on('error')。
由於每個request都有自己獨立的domain,所以這里我們就不怕error處理函數串台了。加上閉包特性,在error中可以輕松利用res和req,給對應的瀏覽器返回友好的錯誤信息。
###domain真的是獨立的嗎?
這里沒打算故作玄虛,答案就是“獨立的”。
看官方的說法:
The enter method is plumbing used by the run, bind, and intercept methods to set the active domain. It sets domain.active and process.domain to the domain, and implicitly pushes the domain onto the domain stack managed by the domain module (see domain.exit() for details on the domain stack).
有興趣的同學可以深入看看domain的實現,node.js維護一個domain堆棧。
這里有一個小秘密,代碼中執行process.domain將獲取到當前上下文的domain對象,不串台。
我們做個簡單試驗:
const domain = require('domain'); const http = require('http'); http.createServer(function (req, res) { var d = domain.create(); d.id = '123'; d.res = res; d.req = req; d.on('error', function (er) { console.error('Error', er); var curDomain = process.domain; console.log(curDomain.id, curDomain.res, curDomain.req); try { curDomain.res.writeHead(500); curDomain.res.end('Error occurred, sorry.'); } catch (er) { console.error('Error sending 500', er, curDomain.req.url); } }); var reqId = parseInt(req.url.substr(1)); setTimeout(function () { d.run(handler); }, (5-reqId)*1000); function handler() { if(reqId == 3){ var a; a.b(); } res.end('hello'); } }).listen(80);
我們用瀏覽器請求http://localhost/2 ,1-5
node.js分別會等待5-1秒才返回,其中3號請求將會返回錯誤。
error處理函數中,沒有使用閉包,而是使用process.domain,因為我們就要驗證一下這個玩意是否串台。
根據fiddler的抓包可以發現,雖然3號請求比后邊的4、5號請求更晚返回,但process.domain對象還是妥妥的指向3號請求自己。
###domain的坑
domain管理的上下文,可以隱式綁定,也可以顯式綁定。什么意思呢?
隱式綁定
If domains are in use, then all new EventEmitter objects (including Stream objects, requests, responses, etc.) will be implicitly bound to the active domain at the time of their creation.
在run的邏輯中創建的對象,都會歸到domain上下文管理;
顯式綁定
Sometimes, the domain in use is not the one that ought to be used for a specific event emitter. Or, the event emitter could have been created in the context of one domain, but ought to instead be bound to some other domain.
一些對象,有可能是在domain.run以外創建的,例如我們的httpServer/req/res,或者一些數據庫連接池。
對付這些對象的問題,我們需要顯式綁定到domain上。
也就是domain.add(req)
看看實際的例子:
var domain = require('domain'); var EventEmitter = require('events').EventEmitter; var e = new EventEmitter(); var timer = setTimeout(function () { e.emit('data'); }, 10); function next() { e.once('data', function () { throw new Error('something wrong here'); }); } var d = domain.create(); d.on('error', function () { console.log('cache by domain'); }); d.run(next);
上述代碼運行,可以發現錯誤並沒有被domain捕獲,原因很清晰,因為timer和e都在domain.run之外創建的,不受domain上下文管理。需要解決這個問題,只需要簡單的add一下即可。
d.add(timer); //or d.add(e);
例子終歸是例子,實際項目中必然情況要復雜多了,redis、mysql等等第三方組件都可能保持長連,那么這些組件往往不在domain中管理,或者出一些差錯。
所以,保底起見,都要再加一句process.on(‘uncaughtException’)
不過,如果異常真到了這一步,我們也沒什么可以做的了,只能寫好log,然后重啟子進程了(關於nodejs多進程,大家可以看看下一篇文章:http://www.cnblogs.com/kenkofox/p/5431643.html)。
###domain帶來的額外好處:request生命周期的全局變量
做一個webservice,一個請求的處理過程,往往會經過好幾個js,接入、路由、文件讀取、數據庫訪問、數據拼裝、頁面模版。。。等等
同時,有一些數據,也是到處需要使用的,典型的就是req和res。
如果不斷在函數調用之間傳遞這些公用的數據,想必一定很累很累,而且代碼看起來也非常惡心。
那么,能否實現request生命周期內的全局變量,存儲這些公用數據呢?
這個全局變量,必須有兩個特點:
1. 全局可訪問
2. 跟request周期綁定,不同的request不串台
聰明的孩子應該想到了,剛才domain的特性就很吻合。
於是,我們可以借助domain,實現request生命周期內的全局變量。
簡單代碼如下:
const domain = require('domain');const http = require('http'); Object.defineProperty(global, 'window', { get : function(){ return process.domain && process.domain.window; } }); http.createServer(function (req, res) { var d = domain.create(); d.id = '123'; d.res = res; d.req = req; d.window = {name:'kenko'}; d.on('error', function (er) { console.error('Error', er); var curDomain = process.domain; try { curDomain.res.writeHead(500); curDomain.res.end('Error occurred, sorry.'); } catch (er) { console.error('Error sending 500', er, curDomain.req.url); } }); d.add(req); d.add(res); d.run(handler); function handler() { res.end('hello, '+window.name); } }).listen(80);
這里關鍵點是process.domain和Object.defineProperty(global, 'window')
從此就能在任一個邏輯js中使用window.xxx來訪問全局變量了。
更進一步,需要大家監聽一下res的finish事件,做一些清理工作。
好了,domain的異常處理就說到這~~~