この記事では主にNode.jsの非同期例外処理とドメインモジュール解析について紹介します。興味のある方は
非同期例外処理
非同期例外の機能
を参照してください。ノード コールバックの非同期的な性質のため、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); });
現時点では、エラー ログを記録することはできますが、プロセスは異常終了しません exit が、エラーが見つかったときにフレンドリーなリクエストを返す方法はありません。タイムアウトしてリターン。
ドメイン
ノードv0.8+では、モジュールドメインがリリースされました。このモジュールは、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) { //处理业务 });
非同期例外を処理するためにミドルウェアの形式でドメインを導入します。もちろん、ドメインは例外を捕捉しましたが、例外によるスタック損失によりメモリ リークが発生する可能性があるため、興味のある学生はドメイン ミドルウェアのプロセスを再起動する必要があります。 。
奇妙な失敗
テストではすべてが正常でしたが、本番環境で正式に使用したところ、突然ドメインが失敗したことがわかりました。非同期例外をキャッチできなかったため、最終的にプロセスが異常終了しました。いくつかの調査の結果、セッションを保存するための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 }));
このとき、ビジネス ロジック コードで例外が発生したとき、それがドメインによってキャプチャされていないことがわかりました。いくつかの試みの後、最終的に問題が特定されました:
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 と _tickCallback の 2 つのメソッドを理解する必要があります。
function laterCall() { console.log('print me later'); } process.nextTick(laterCallback); console.log('print me first');
上記のコードでノードを書いたことがある人なら誰でも、 nextTick の機能は、実行のために次のイベント ループに lateCallback を入れることです。 _tickCallback メソッドは非パブリック メソッドです。このメソッドは、現在のタイム ループが終了した後に次のイベント ループを継続するために呼び出されるエントリ関数 です。
言い換えると、ノードはイベント ループのキューを維持し、nextTick がキューに登録され、_tickCallback がデキューされます。ドメインの実装
ノードのイベントループの仕組みを理解した後、ドメインが何をするのかを見てみましょう。 ドメイン自体は実際には EventEmitter オブジェクトであり、キャプチャされたエラーをイベントを通じて配信します。このようにして、研究すると次の 2 つの点に単純化できます: ドメインのエラー イベントがいつトリガーされるか: プロセスは例外をスローし、try catch によってキャッチされません。 processFatal がこの時点でドメイン パッケージ内にある場合は、ドメイン上でエラー イベントがトリガーされ、それ以外の場合は、プロセス上で uncaughtException イベントがトリガーされます。変数 がこのドメイン インスタンスを指すようになります。 がこのイベント ループ で例外をスローし、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 中国語 Web サイトの他の関連記事を参照してください。