node.js WebService異常處理(domain)以及利用domain實現request生命週期的全局變量


成熟的Web Service技術,例如Fast CGI、J2EE、php,必然會對代碼異常有足夠的保護,好的Web必然會在出錯後給出友好的提示,而不是莫名其妙的等待504超時。
而node.js這裏比較年輕,而開發人員就更年輕了,你們都沒有太多的經驗,也沒有太多的參考。php


###單進程+PM2html

 


 

最簡單的方式http處理方式,能夠常常見到這樣的模式:node

var http = require('http');

http.createServer(function (req, res) {
  res.end('hello');
}).listen(80);

 

簡單處理請求,沒有任何的全局異常處理。也就是說,只在業務邏輯中加一些try catch,信心滿滿的說,不會出現未捕獲的異常。mysql

  1. 通常來講,這樣都沒太多問題,正常返回的時候一切都好。
  2. 但若是,哪天,某個代碼出現了點bug~~ 誰都不是神,總有bug的時候。
var http = require('http');

http.createServer(function (req, res) {
  var a;
  a.b();
  res.end('hello');
}).listen(80);

例如這樣,node.js進程立刻會由於報錯而掛掉~~~web

聰明的孩子,也許早就據說世界上有forever、pm2等玩意。
因而,服務啓動就變成redis

pm2 start index.js

這樣的模式太常見,尤爲是內部的小系統。pm2監控node.js進程,一旦掛掉,就重啓。sql

彷佛
這樣也挺好的,簡單易懂,總體的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號請求比後邊的四、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.domainObject.defineProperty(global, 'window')
今後就能在任一個邏輯js中使用window.xxx來訪問全局變量了。
更進一步,須要你們監聽一下res的finish事件,作一些清理工做。

好了,domain的異常處理就說到這~~~

相關文章
相關標籤/搜索