异步异常的特点
由于node的回调异步特性,无法通过try catch来捕捉所有的异常:
try { process.nextTick(function () { foo.bar(); }); } catch (err) { //can not catch it }
而对于web服务而言,其实是非常希望这样的:
//express风格的路由 app.get('/index', function (req, res) { try { //业务逻辑 } catch (err) { logger.error(err); res.statusCode = 500; return res.json({success: false, message: '服务器异常'}); } });
如果try catch能够捕获所有的异常,这样我们可以在代码出现一些非预期的错误时,能够记录下错误的同时,友好的给调用者返回一个500错误。可惜,try catch无法捕获异步中的异常。所以我们能做的只能是:
app.get('/index', function (req, res) { // 业务逻辑 }); process.on('uncaughtException', function (err) { logger.error(err); });
这个时候,虽然我们可以记录下这个错误的日志,且进程也不会异常退出,但是我们是没有办法对发现错误的请求友好返回的,只能够让它超时返回。
domain
在node v0.8+版本的时候,发布了一个模块domain。这个模块做的就是try catch所无法做到的:捕捉异步回调中出现的异常。
于是乎,我们上面那个无奈的例子好像有了解决的方案:
var domain = require('domain'); //引入一个domain的中间件,将每一个请求都包裹在一个独立的domain中 //domain来处理异常 app.use(function (req,res, next) { var d = domain.create(); //监听domain的错误事件 d.on('error', function (err) { logger.error(err); res.statusCode = 500; res.json({sucess:false, messag: '服务器异常'}); d.dispose(); }); d.add(req); d.add(res); d.run(next); }); app.get('/index', function (req, res) { //处理业务 });
我们通过中间件的形式,引入domain来处理异步中的异常。当然,domain虽然捕捉到了异常,但是还是由于异常而导致的堆栈丢失会导致内存泄漏,所以出现这种情况的时候还是需要重启这个进程的,有兴趣的同学可以去看看domain-middleware这个domain中间件。
诡异的失效
我们的测试一切正常,当正式在生产环境中使用的时候,发现domain突然失效了!它竟然没有捕获到异步中的异常,最终导致进程异常退出。经过一番排查,最后发现是由于引入了redis来存放session导致的。
var http = require('http'); var connect = require('connect'); var RedisStore = require('connect-redis')(connect); var domainMiddleware = require('domain-middleware'); var server = http.createServer(); var app = connect(); app.use(connect.session({ key: 'key', secret: 'secret', store: new RedisStore(6379, 'localhost') })); //domainMiddleware的使用可以看前面的链接 app.use(domainMiddleware({ server: server, killTimeout: 30000 }));
此时,当我们的业务逻辑代码中出现了异常,发现竟然没有被domain捕获!经过一番尝试,终于将问题定位到了:
var domain = require('domain'); var redis = require('redis'); var cache = redis.createClient(6379, 'localhost'); function error() { cache.get('a', function () { throw new Error('something wrong'); }); } function ok () { setTimeout(function () { throw new Error('something wrong'); }, 100); } var d = domain.create(); d.on('error', function (err) { console.log(err); }); d.run(ok); //domain捕获到异常 d.run(error); //异常被抛出
奇怪了!都是异步调用,为什么前者被捕获,后者却没办法捕获到呢?
Domain剖析
回过头来,我们来看看domain做了些什么来让我们捕获异步的请求(代码来自node v0.10.4,此部分可能正在快速变更优化)。
node事件循环机制
在看Domain的原理之前,我们先要了解一下nextTick和_tickCallback的。
function laterCall() { console.log('print me later'); } process.nextTick(laterCallback); console.log('print me first');
上面这段代码写过node的人都很熟悉,nextTick的作用就是把laterCallback放到下一个事件循环去执行。而_tickCallback方法则是一个非公开的方法,这个方法是在当前时间循环结束之后,调用之以继续进行下一个事件循环的入口函数。
换而言之,node为事件循环维持了一个队列,nextTick入队,_tickCallback出列。
domain的实现
在了解了node的事件循环机制之后,我们再来看看domain做了些什么。
domain自身其实是一个EventEmitter对象,它通过事件的方式来传递捕获的错误。这样我们在研究它的时候,就简化到两个点:
什么时候触发domain的error事件:
进程抛出了异常,没有被任何的try catch捕获到,这时候将会触发整个process的processFatal,此时如果在domain包裹之中,将会在domain上触发error事件,反之,将会在process上触发uncaughtException事件。
domain如何在多个不同的事件循环中传递: