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


Posted in Javascript onMay 10, 2017

异步异常处理

异步异常的特点

由于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,避免踩坑。

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

Javascript 相关文章推荐
JavaScript操作XML 使用百度RSS作为新闻源示例
Feb 17 Javascript
JQuery仿小米手机抢购页面倒计时效果
Dec 16 Javascript
JS模拟键盘打字效果的方法
Aug 05 Javascript
Javascript类型转换的规则实例解析
Feb 23 Javascript
AngularJS入门教程之AngularJS表达式
Apr 18 Javascript
jquery datatable服务端分页
Aug 31 Javascript
jQuery Dialog 取消右上角删除按钮事件
Sep 07 Javascript
Ajax的概述与实现过程
Nov 18 Javascript
jQuery自定义多选下拉框效果
Jun 19 jQuery
详解vue中组件参数
Jul 09 Javascript
JavaScript引用类型Function实例详解
Aug 09 Javascript
解决vue A对象赋值给B对象,修改B属性会影响到A的问题
Sep 25 Javascript
基于Node的React图片上传组件实现实例代码
May 10 #Javascript
JavaScript使用ZeroClipboard操作剪切板
May 10 #Javascript
VUE 更好的 ajax 上传处理 axios.js实现代码
May 10 #Javascript
vuejs2.0子组件改变父组件的数据实例
May 10 #Javascript
详解vue.js2.0父组件点击触发子组件方法
May 10 #Javascript
详解vue-router 2.0 常用基础知识点之router-link
May 10 #Javascript
JavaScript使用readAsDataURL读取图像文件
May 10 #Javascript
You might like
php&mysql 日期操作小记
2012/02/27 PHP
用于自动添加Digg This!按钮的JavaScript
2006/12/23 Javascript
IE与FireFox的兼容性问题分析
2007/04/22 Javascript
javascript为按钮注册回车事件(设置默认按钮)的方法
2015/05/09 Javascript
Javascript使用post方法提交数据实例
2015/08/03 Javascript
javaScript生成支持中文带logo的二维码(jquery.qrcode.js)
2017/01/03 Javascript
Node.js通过身份证号验证年龄、出生日期与性别方法示例
2017/03/09 Javascript
Angular.JS中select下拉框设置value的方法
2017/06/20 Javascript
vue中eventbus被多次触发以及踩过的坑
2017/12/02 Javascript
Bootstrap treeview实现动态加载数据并添加快捷搜索功能
2018/01/07 Javascript
详解Angular路由之路由守卫
2018/05/10 Javascript
解决JavaScript layui 下拉框不显示的问题
2018/08/14 Javascript
原生JS实现动态加载js文件并在加载成功后执行回调函数的方法
2020/12/30 Javascript
Vue 第三方字体图标引入 Font Awesome的方法
2018/09/28 Javascript
CentOS7中源码编译安装NodeJS的完整步骤
2018/10/13 NodeJs
ios中视频的最后一桢问题解决
2019/05/14 Javascript
vue 实现移动端键盘搜索事件监听
2019/11/06 Javascript
Vue插件之滑动验证码用法详解
2020/04/05 Javascript
详解JS函数防抖
2020/06/05 Javascript
Vue使用CDN引用项目组件,减少项目体积的步骤
2020/10/30 Javascript
python基础教程之字典操作详解
2014/03/25 Python
Python IDE PyCharm的基本快捷键和配置简介
2015/11/04 Python
Django数据库表反向生成实例解析
2018/02/06 Python
如何优雅地改进Django中的模板碎片缓存详解
2018/07/04 Python
Python3+Appium实现多台移动设备操作的方法
2019/07/05 Python
Python坐标线性插值应用实现
2019/11/13 Python
Python 基于FIR实现Hilbert滤波器求信号包络详解
2020/02/26 Python
使用keras和tensorflow保存为可部署的pb格式
2020/05/25 Python
python boto和boto3操作bucket的示例
2020/10/30 Python
Python基于argparse与ConfigParser库进行入参解析与ini parser
2021/02/02 Python
《雷雨》教学反思
2014/02/20 职场文书
《翻越远方的大山》教学反思
2014/04/13 职场文书
学校爱国卫生月活动总结
2014/06/25 职场文书
求职简历自我评价怎么写
2015/03/10 职场文书
篮球赛闭幕式主持词
2015/07/03 职场文书
职场领导同事生日简短祝福语
2019/08/06 职场文书