全网整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:400-708-3566

Node.js 异步异常的处理与domain模块解析

异步异常处理

异步异常的特点

由于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如何在多个不同的事件循环中传递:

  1. 当domain被实例化之后,我们通常会调用它的run方法(如之前在web服务中的使用),来将某个函数在这个domain示例的包裹中执行。被包裹的函数在执行的时候,process.domain这个全局变量将会被指向这个domain实例。当这个事件循环中,抛出异常调用processFatal的时候,发现process.domain存在,就会在domain上触发error事件。
  2. 在require引入domain模块之后,会重写全局的nextTick和_tickCallback,注入一些domain相关的代码:
//简化后的domain传递部分代码
function nextDomainTick(callback) {
 nextTickQueue.push({callback: callback, domain: process.domain});
}

function _tickDomainCallback() {
 var tock = nextTickQueue.pop();
 //设置process.domain = tock.domain
 tock.domain && tock.domain.enter();
 callback();
 //清除process.domain
 tock.domain && tock.domain.exit();    
 }
};

这个是其在多个事件循环中传递domain的关键:nextTick入队的时候,记录下当前的domain,当这个被加入队列中的事件循环被_tickCallback启动执行的时候,将新的事件循环的process.domain置为之前记录的domain。这样,在被domain所包裹的代码中,不管如何调用process.nextTick, domain将会一直被传递下去。

当然,node的异步还有两种情况,一种是event形式。因此在EventEmitter的构造函数有如下代码:

 if (exports.usingDomains) {
  // if there is an active domain, then attach to it.
  domain = domain || require('domain');
  if (domain.active && !(this instanceof domain.Domain)) {
   this.domain = domain.active;
  }
 }

实例化EventEmitter的时候,将会把这个对象和当前的domain绑定,当通过emit触发这个对象上的事件时,像_tickCallback执行的时候一样,回调函数将会重新被当前的domain包裹住。

而另一种情况,是setTimeout和setInterval,同样的,在timer的源码中,我们也可以发现这样的一句代码:

 if (process.domain) timer.domain = process.domain;

跟EventEmmiter一样,之后这些timer的回调函数也将被当前的domain包裹住了。

node通过在nextTick, timer, event三个关键的地方插入domain的代码,让它们得以在不同的事件循环中传递。

更复杂的domain

有些情况下,我们可能会遇到需要更加复杂的domain使用。

domain嵌套:我们可能会外层有domain的情况下,内层还有其他的domain,使用情景可以在文档中找到

// create a top-level domain for the server
var serverDomain = domain.create();

serverDomain.run(function() {
 // server is created in the scope of serverDomain
 http.createServer(function(req, res) {
  // req and res are also created in the scope of serverDomain
  // however, we'd prefer to have a separate domain for each request.
  // create it first thing, and add req and res to it.
  var reqd = domain.create();
  reqd.add(req);
  reqd.add(res);
  reqd.on('error', function(er) {
   console.error('Error', er, req.url);
   try {
    res.writeHead(500);
    res.end('Error occurred, sorry.');
   } catch (er) {
    console.error('Error sending 500', er, req.url);
   }
  });
 }).listen(1337);
});

为了实现这个功能,其实domain还会偷偷的自己维持一个domain的stack,有兴趣的童鞋可以在这里看到。

回头解决疑惑

回过头来,我们再来看刚才遇到的问题:为什么两个看上去都是同样的异步调用,却有一个domain无法捕获到异常?理解了原理之后不难想到,肯定是调用了redis的那个异步调用在抛出错误的这个事件循环内,是不在domain的范围之内的。我们通过一段更加简短的代码来看看,到底在哪里出的问题。

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的范围之内,因此,当在next函数中监听的事件被触发,执行抛出异常的回调函数时,其实根本就没有处于domain的包裹中,当然就不会被domain捕获到异常了!

其实node针对这种情况,专门设计了一个API:domain.add。它可以将domain之外的timer和event对象,添加到当前domain中去。对于上面那个例子:

d.add(timer);
//or
d.add(e);

将timer或者e任意一个对象添加到domain上,就可以让错误被domain捕获了。

再来看最开始redis导致domain无法捕捉到异常的问题。我们是不是也有办法可以解决呢?

其实对于这种情况,还是没有办法实现最佳的解决方案的。现在对于非预期的异常产生的时候,我们只能够让当前请求超时,然后让这个进程停止服务,之后重新启动。graceful模块配合cluster就可以实现这个解决方案。

__domain十分强大,但不是万能的。__希望在看过这篇文章之后,大家能够正确的使用domian,避免踩坑。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。


# nodejs  # 异步模块  # Node.js  # domain模块  # 异常处理  # 使用nodejs中httpProxy代理时候出现404异常的解决方法  # 浅谈NodeJs之数据库异常处理  # 解析Node.js异常处理中domain模块的使用方法  # Nodejs异步回调之异常处理实例分析  # Nodejs处理异常操作示例  # node.js express捕获全局异常的三种方法实例分析  # 如何优雅地在Node应用中进行错误异常处理  # Nodejs监控事件循环异常示例详解  # Node.js中出现未捕获异常的处理方法  # 将会  # 抛出  # 回调  # 会在  # 再来  # 这种情况  # 都是  # 多个  # 些什么  # 我们可以  # 有兴趣  # 来看看  # 没有办法  # 回过头来  # 就可以  # 范围之内  # 捉到  # 是一个  # 情况下  # 是在 


相关文章: Avalonia如何实现跨窗口通信 Avalonia窗口间数据传递  ,柠檬视频怎样兑换vip?  重庆市网站制作公司,重庆招聘网站哪个好?  头像制作网站在线制作软件,dw网页背景图像怎么设置?  网站制作怎么样才能赚钱,用自己的电脑做服务器架设网站有什么利弊,能赚钱吗?  如何用搬瓦工VPS快速搭建个人网站?  如何选择服务器才能高效搭建专属网站?  制作网站外包平台,自动化接单网站有哪些?  如何快速搭建高效简练网站?  盘锦网站制作公司,盘锦大洼有多少5G网站?  建站之星3.0如何解决常见操作问题?  如何在服务器上三步完成建站并提升流量?  如何在万网开始建站?分步指南解析  建站之星导航如何优化提升用户体验?  如何快速生成高效建站系统源代码?  ,交易猫的商品怎么发布到网站上去?  如何选择域名并搭建高效网站?  建站主机选哪家性价比最高?  新网站制作渠道有哪些,跪求一个无线渠道比较强的小说网站,我要发表小说?  中山网站制作网页,中山新生登记系统登记流程?  建站之星图片链接生成指南:自助建站与智能设计教程  制作证书网站有哪些,全国城建培训中心证书查询官网?  建站之星免费版是否永久可用?  如何快速搭建FTP站点实现文件共享?  ui设计制作网站有哪些,手机UI设计网址吗?  潮流网站制作头像软件下载,适合母子的网名有哪些?  制作网站的软件下载免费,今日头条开宝箱老是需要下载怎么回事?  国美网站制作流程,国美电器蒸汽鍋怎么用官方网站?  制作旅游网站html,怎样注册旅游网站?  清单制作人网站有哪些,近日“兴风作浪的姑奶奶”引起很多人的关注这是什么事情?  如何通过老薛主机一键快速建站?  上海网站制作网页,上海本地的生活网站有哪些?最好包括生活的各个方面的?  极客网站有哪些,DoNews、36氪、爱范儿、虎嗅、雷锋网、极客公园这些互联网媒体网站有什么差异?  如何通过免费商城建站系统源码自定义网站主题与功能?  网站微信制作软件,如何制作微信链接?  如何制作新型网站程序文件,新型止水鱼鳞网要拆除吗?  儿童网站界面设计图片,中国少年儿童教育网站-怎么去注册?  北京网页设计制作网站有哪些,继续教育自动播放怎么设置?  如何用西部建站助手快速创建专业网站?  建站之星免费模板:自助建站系统与智能响应式一键生成  如何通过万网虚拟主机快速搭建网站?  完全自定义免费建站平台:主题模板在线生成一站式服务  大连网站制作公司哪家好一点,大连买房网站哪个好?  婚礼视频制作网站,学习*后期制作的网站有哪些?  详解免费开源的DotNet二维码操作组件ThoughtWorks.QRCode(.NET组件介绍之四)  建站ABC备案流程中有哪些关键注意事项?  h5在线制作网站电脑版下载,h5网页制作软件?  已有域名如何快速搭建专属网站?  实惠建站价格推荐:2025年高性价比自助建站套餐解析  如何通过虚拟主机空间快速建站? 

您的项目需求

*请认真填写需求信息,我们会在24小时内与您取得联系。