>  기사  >  웹 프론트엔드  >  js의 비동기 예외 처리 및 domian 튜토리얼 사용에 대한 자세한 설명

js의 비동기 예외 처리 및 domian 튜토리얼 사용에 대한 자세한 설명

零下一度
零下一度원래의
2017-05-11 13:35:121382검색

이 글에서는 주로 Node.js 비동기 예외 처리와 도메인 모듈 분석에 대해 소개하는데, 관심 있는 친구들은 참고할 수 있습니다

비동기 예외 처리

비동기 예외의 특징

노드의 콜백 비동기 특성으로 인해 try catch를 통해 모든 예외를 catch하는 것은 불가능합니다. 예외:

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

웹 서비스의 경우 실제로 다음을 갖는 것이 매우 바람직합니다.

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

이때 오류 로그를 기록할 수 있고 프로세스가 비정상적으로 종료되지는 않지만 잘못된 요청이 반환되었는지 확인할 방법은 없습니다. 친절하며 시간 초과가 발생한 경우에만 반환될 수 있습니다.

도메인

node v0.8+에서는 모듈 도메인이 출시되었습니다. 이 모듈은 try catch가 할 수 없는 작업, 즉 비동기 콜백에서 발생하는 예외를 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) {
 //处理业务
});

비동기 예외를 처리하기 위해 미들웨어 형태로 도메인을 도입합니다. 물론 도메인이 예외를 포착했더라도 예외로 인한 스택 손실은 여전히 ​​메모리 누수로 이어지기 때문에 관심 있는 학생은 도메인 미들웨어 도메인 미들웨어를 확인할 수 있습니다. .조각.

이상한 실패

저희 테스트는 모두 정상이었습니다. 정식으로 프로덕션 환경에서 사용해보니 갑자기 도메인이 실패하는 걸 발견했습니다! 결국 프로세스가 비정상적으로 종료되는 비동기 예외를 포착하지 못했습니다. 몇번의 조사 끝에, store session에 redis를 도입하여 문제가 발생한 것으로 확인되었습니다.

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

이때, 저희 비즈니스 로직 코드에서 Exception이 발생했을 때 도메인에서 캡쳐가 되지 않은 것을 발견했습니다! 몇 번 시도한 끝에 마침내 문제를 발견했습니다.

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); //异常被抛出

이상해요! 둘 다 비동기 호출입니다. 전자는 캡처되지만 후자는 캡처되지 않는 이유는 무엇입니까?

도메인 분석

돌아보면 비동기 요청을 캡처하기 위해 도메인이 어떤 역할을 하는지 살펴보겠습니다. (코드는 노드 v0.10.4에서 가져온 것입니다. 이 부분은 신속한 변경 최적화가 진행 중일 수 있음).

노드 이벤트루프메커니즘

도메인의 원리를 살펴보기 전에 먼저 nextTick과 _tickCall뒤로 돌아가는 방법은 2가지가 있습니다.

function laterCall() {
 console.log('print me later');
}

process.nextTick(laterCallback);
console.log('print me first');

위 코드에서 노드를 작성해 본 사람이라면 누구나 익숙할 것입니다. nextTick의 기능은 laterCallback을 다음 이벤트 루프에 넣어서 실행하는 것입니다. _tickCallback 메서드는 비공개 메서드입니다. 이 메서드는 현재 시간 루프가 끝난 후 다음 이벤트 루프를 계속하기 위해 호출되는 항목

함수 입니다.

즉, 노드는 이벤트 루프에 대한 대기열을 유지하고 nextTick은 대기열에 추가되며 _tickCallback은 대기열에서 제거됩니다.

도메인 구현

노드의 이벤트 루프 메커니즘을 이해한 후, 도메인이 어떤 일을 하는지 살펴보겠습니다.

도메인 자체는 실제로 이벤트를 통해 캡처된 오류를 전달하는 EventEmitter 객체입니다. 이런 식으로 연구할 때 다음 두 가지 사항으로 단순화합니다.

도메인의 오류 이벤트가 언제 발생합니까?

프로세스에서 예외가 발생했지만 어떤 프로세스에서도 포착되지 않았습니다. try catch 이때 전체 프로세스의 processFatal이 트리거되고, 도메인 패키지에 있으면 해당 도메인에서 오류 이벤트가 트리거됩니다.


다양한 이벤트 루프에서 도메인이 전달되는 방법:

  1. 도메인이 인스턴스화되면 일반적으로 실행 메소드(예: 이전에 웹에서 사용됨)를 호출합니다. 서비스), 이 도메인 예제의 패키지에 있는 기능을 실행합니다. 래핑된 함수가 실행되면 process.domain의 전역

    변수 가 이 도메인 인스턴스를 가리킵니다. 이 이벤트 루프에서 예외를 발생시키고 processFatal을 호출하여 process.domain이 존재하는 것을 발견하면 해당 도메인에서 오류 이벤트가 트리거됩니다.

  2. require가 도메인 모듈을 도입한 후 전역 nextTick 및 _tickCallback이 다시 작성되고 일부 도메인 관련 코드가 삽입됩니다.

//简化后的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,避免踩坑。

【相关推荐】

1. 免费js在线视频教程

2. JavaScript中文参考手册

3. php.cn独孤九贱(3)-JavaScript视频教程

위 내용은 js의 비동기 예외 처리 및 domian 튜토리얼 사용에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.