异常处理与domain

异步异常捕获

由于node的回调异步特性,无法通过try catch来捕捉所有的异常:

try {
  process.nextTick(function () {
    foo.bar();
  });
} catch (err) {
  //can not catch it
}

如果try catch能够捕获所有的异常,这样我们可以在代码出现一些非预期的错误时,能够记录下错误的同时,友好的给调用者返回一个500错误。可惜,try catch无法捕获异步中的异常。所以我们能做的只能是:

app.get('/index', function (req, res) {
  // 业务逻辑  
});

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

这个时候,虽然我们可以记录下这个错误的日志,且进程也不会异常退出,但是我们是没有办法对发现错误的请求友好返回的,因为异常处理只返回给我们一个冷冰冰的 error, 脱离了上下文,我们只能够让它超时返回。

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);
});

domain虽然捕捉到了异常,但是还是由于异常而导致的堆栈丢失会导致内存泄漏,所以出现这种情况的时候还是需要重启这个进程的。

Domain剖析

Domain 自身其实也是 Event 模块一个典型的应用, 它通过事件的方式来传递捕获的错误。

inherits(Domain, EventEmitter);

function Domain() {
  EventEmitter.call(this);

  this.members = [];
}

另外, domain 为了支持深层次的嵌套, 提供了 Domain#enterDomain#exit 的 API。 先来看 enter的实现,

Domain.prototype.enter = function() {
  if (this._disposed) return;

  // note that this might be a no-op, but we still need
  // to push it onto the stack so that we can pop it later.
  exports.active = process.domain = this;
  stack.push(this);
  _domain_flag[0] = stack.length;
};

设置当前活跃的 domain, 并且为了便于回溯,将当前的 domain 加入到队列的后面,更新栈的深度。

再看 exit实现,

Domain.prototype.exit = function() {
  // skip disposed domains, as usual, but also don't do anything if this
  // domain is not on the stack.
  var index = stack.lastIndexOf(this);
  if (this._disposed || index === -1) return;

  // exit all domains until this one.
  stack.splice(index);
  _domain_flag[0] = stack.length;

  exports.active = stack[stack.length - 1];
  process.domain = exports.active;
};

相反的, 退出当前的 domain, 更新长度,设置当前活跃的 domain

读者可能好奇,我并没有显式地调用 enter, exit, 而只是简单的创建了一个 domain, 怎么会达到这种效果?

读者可以看看 AsyncWrap::MakeCallback(), 每次C++ --> JS, 都会检查 domain, 如果使用,则会显式地调用他们。 其他地方读者可以自行寻找。

为了解决不在当前作用域的异常处理, Domain 也提供 Domain#addDomain#remove 来增加 emitter 或者 Timer

回到事件的根本, 什么时候触发domain的error事件?

process._fatalException = function(er) {
      var caught;

      if (process.domain && process.domain._errorHandler)
        caught = process.domain._errorHandler(er) || caught;

      if (!caught)
        caught = process.emit('uncaughtException', er);

      // If someone handled it, then great.  otherwise, die in C++ land
      // since that means that we'll exit the process, emit the 'exit' event
      if (!caught) {
        try {
          if (!process._exiting) {
            process._exiting = true;
            process.emit('exit', 1);
          }
        } catch (er) {
          // nothing to be done about it at this point.
        }

      // if we handled an error, then make sure any ticks get processed
      } else {
        NativeModule.require('timers').setImmediate(process._tickCallback);
      }

      return caught;
    };

如果当前 process 使用了 domain, 也是就 process.domain 不为空,就调用 _errorHandler 来处理, 当前也存在没有处理的情况,职责链来到 process, process 则触发 uncaughtException 事件。

总结

domain很强大,但它只能捕获在其作用域范围内的异常。对于非预期的异常产生的时候, 我们最好让当前请求超时,然后让这个进程停止服务,之后重新启动。

但始终 Domain 在异常处理上有各种不完美,目前该模块处于即将废除阶段,取代他的可能是另一种机制。

详细讨论见:

https://github.com/nodejs/node/issues/66

参考

results matching ""

    No results matching ""