이 글에서는 주로 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은 대기열에 추가되며 _tickCallback은 대기열에서 제거됩니다.도메인 구현
노드의 이벤트 루프 메커니즘을 이해한 후, 도메인이 어떤 일을 하는지 살펴보겠습니다. 도메인 자체는 실제로 이벤트를 통해 캡처된 오류를 전달하는 EventEmitter 객체입니다. 이런 식으로 연구할 때 다음 두 가지 사항으로 단순화합니다. 도메인의 오류 이벤트가 언제 발생합니까? 프로세스에서 예외가 발생했지만 어떤 프로세스에서도 포착되지 않았습니다. try catch 이때 전체 프로세스의 processFatal이 트리거되고, 도메인 패키지에 있으면 해당 도메인에서 오류 이벤트가 트리거됩니다.변수 가 이 도메인 인스턴스를 가리킵니다. 이 이벤트 루프에서 가 예외를 발생시키고 processFatal을 호출하여 process.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,避免踩坑。
【相关推荐】
1. 免费js在线视频教程
3. php.cn独孤九贱(3)-JavaScript视频教程
위 내용은 js의 비동기 예외 처리 및 domian 튜토리얼 사용에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!