域模块剖析
可用性题目
隐式行动
开发人员可以建立新域,然后只需运转domain.enter()
,然后,它充任未来抛出者没法视察到的任何非常的全能捕捉器,许可模块作者阻拦差别模块中不相关代码的非常,防备代码的发起者晓得自身的非常。
以下是一个间接链接模块怎样影响另一个模块的示例:
// module a.js
const b = require('./b');
const c = require('./c');
// module b.js
const d = require('domain').create();
d.on('error', () => { /* silence everything */ });
d.enter();
// module c.js
const dep = require('some-dep');
dep.method(); // Uh-oh! This method doesn't actually exist.
由于模块b
进入域但从不退出,任何未捕捉的非常都将被吞噬,不让模块c
晓得它为什么没有运转全部剧本,留下可以部份添补的module.exports
。如许做与监听'uncaughtException'
差别,由于后者明白意味着全局捕捉毛病,另一个题目是在任何'uncaughtException'
处置惩罚顺序之前处置惩罚域,并阻挠它们运转。
另一个题目是,假如事宜发射器上没有设置'error'
处置惩罚顺序,域会自动路由毛病,对此没有可选的插进去机制,而是自动跨全部异步链流传。这看起来好像很有效,然则一旦异步挪用深度为两个或更多模块,个中一个不包括毛病处置惩罚顺序,域的建立者将倏忽捕捉不测非常,而且抛出者的非常将被作者无视。
以下是一个简朴的示例,申明缺乏'error'
处置惩罚顺序怎样许可运动域阻拦毛病:
const domain = require('domain');
const net = require('net');
const d = domain.create();
d.on('error', (err) => console.error(err.message));
d.run(() => net.createServer((c) => {
c.end();
c.write('bye');
}).listen(8000));
纵然经由过程d.remove(c)
手动删除衔接也不会阻挠衔接的毛病被自动阻拦。
搅扰毛病路由和非常处置惩罚的失利是毛病被冒出的不一致,以下是嵌套域怎样依据它们什么时候发作以及不会使非常冒出的示例:
const domain = require('domain');
const net = require('net');
const d = domain.create();
d.on('error', () => console.error('d intercepted an error'));
d.run(() => {
const server = net.createServer((c) => {
const e = domain.create(); // No 'error' handler being set.
e.run(() => {
// This will not be caught by d's error handler.
setImmediate(() => {
throw new Error('thrown from setImmediate');
});
// Though this one will bubble to d's error handler.
throw new Error('immediately thrown');
});
}).listen(8080);
});
可以预期嵌套域始终保持嵌套,并始终将非常流传到域客栈中,或许非常永久不会自动冒出,不幸的是,这两种状况都邑发作,致使可以令人困惑的行动以至可以难以调试时序争执。
API差异
虽然基于运用EventEmitter
的 API可以运用bind()
,而errback作风的回调可以运用intercept()
,然则隐式绑定到运动域的替代API必需在run()
内部实行。这意味着假如模块作者想要运用替代那些提到的机制来支撑域,则他们必需自身手动完成域支撑,而不是可以应用现有的隐式机制。
毛病流传
假如可以的话,跨嵌套域流传毛病并非直接了当的,现有文档显现了假如要求处置惩罚顺序中存在毛病,怎样close()
http
效劳器的简朴示例,它没有诠释的是假如要求处置惩罚顺序为另一个异步要求建立另一个域实例,怎样封闭效劳器,运用以下作为毛病流传失利的简朴示例:
const d1 = domain.create();
d1.foo = true; // custom member to make more visible in console
d1.on('error', (er) => { /* handle error */ });
d1.run(() => setTimeout(() => {
const d2 = domain.create();
d2.bar = 43;
d2.on('error', (er) => console.error(er.message, domain._stack));
d2.run(() => {
setTimeout(() => {
setTimeout(() => {
throw new Error('outer');
});
throw new Error('inner');
});
});
}));
纵然在域实例用于当地存储的状况下,也可以接见资本,依然没法让毛病继承从d2
流传回d1
。疾速搜检可以关照我们,简朴地从d2
的域'error'
处置惩罚顺序抛出将许可d1
然后捕捉非常并实行其自身的毛病处置惩罚顺序,虽然状况并非如此,搜检domain._stack
后,你会看到客栈只包括d2
。
这可以被认为是API的失利,但纵然它确切以这类体式格局运转,依然存在通报异步实行中的分支失利的现实的题目,而且该分支中的一切进一步操纵必需住手。在http要求处置惩罚顺序的示例中,假如我们触发多个异步要求,然后每一个异步要求将write()
的数据发送回客户端,则尝试将write()
发送到封闭的句柄会发生更多毛病,
非常资本清算
以下剧本包括在给定衔接或其任何依靠项中发作非常的状况下在小资本依靠关联树中准确清算的更庞杂示例,将剧本分解为基础操纵:
'use strict';
const domain = require('domain');
const EE = require('events');
const fs = require('fs');
const net = require('net');
const util = require('util');
const print = process._rawDebug;
const pipeList = [];
const FILENAME = '/tmp/tmp.tmp';
const PIPENAME = '/tmp/node-domain-example-';
const FILESIZE = 1024;
let uid = 0;
// Setting up temporary resources
const buf = Buffer.alloc(FILESIZE);
for (let i = 0; i < buf.length; i++)
buf[i] = ((Math.random() * 1e3) % 78) + 48; // Basic ASCII
fs.writeFileSync(FILENAME, buf);
function ConnectionResource(c) {
EE.call(this);
this._connection = c;
this._alive = true;
this._domain = domain.create();
this._id = Math.random().toString(32).substr(2).substr(0, 8) + (++uid);
this._domain.add(c);
this._domain.on('error', () => {
this._alive = false;
});
}
util.inherits(ConnectionResource, EE);
ConnectionResource.prototype.end = function end(chunk) {
this._alive = false;
this._connection.end(chunk);
this.emit('end');
};
ConnectionResource.prototype.isAlive = function isAlive() {
return this._alive;
};
ConnectionResource.prototype.id = function id() {
return this._id;
};
ConnectionResource.prototype.write = function write(chunk) {
this.emit('data', chunk);
return this._connection.write(chunk);
};
// Example begin
net.createServer((c) => {
const cr = new ConnectionResource(c);
const d1 = domain.create();
fs.open(FILENAME, 'r', d1.intercept((fd) => {
streamInParts(fd, cr, 0);
}));
pipeData(cr);
c.on('close', () => cr.end());
}).listen(8080);
function streamInParts(fd, cr, pos) {
const d2 = domain.create();
const alive = true;
d2.on('error', (er) => {
print('d2 error:', er.message);
cr.end();
});
fs.read(fd, Buffer.alloc(10), 0, 10, pos, d2.intercept((bRead, buf) => {
if (!cr.isAlive()) {
return fs.close(fd);
}
if (cr._connection.bytesWritten < FILESIZE) {
// Documentation says callback is optional, but doesn't mention that if
// the write fails an exception will be thrown.
const goodtogo = cr.write(buf);
if (goodtogo) {
setTimeout(() => streamInParts(fd, cr, pos + bRead), 1000);
} else {
cr._connection.once('drain', () => streamInParts(fd, cr, pos + bRead));
}
return;
}
cr.end(buf);
fs.close(fd);
}));
}
function pipeData(cr) {
const pname = PIPENAME + cr.id();
const ps = net.createServer();
const d3 = domain.create();
const connectionList = [];
d3.on('error', (er) => {
print('d3 error:', er.message);
cr.end();
});
d3.add(ps);
ps.on('connection', (conn) => {
connectionList.push(conn);
conn.on('data', () => {}); // don't care about incoming data.
conn.on('close', () => {
connectionList.splice(connectionList.indexOf(conn), 1);
});
});
cr.on('data', (chunk) => {
for (let i = 0; i < connectionList.length; i++) {
connectionList[i].write(chunk);
}
});
cr.on('end', () => {
for (let i = 0; i < connectionList.length; i++) {
connectionList[i].end();
}
ps.close();
});
pipeList.push(pname);
ps.listen(pname);
}
process.on('SIGINT', () => process.exit());
process.on('exit', () => {
try {
for (let i = 0; i < pipeList.length; i++) {
fs.unlinkSync(pipeList[i]);
}
fs.unlinkSync(FILENAME);
} catch (e) { }
});
当新衔接发作时,同时:
- 在文件体系上翻开一个文件
- 翻开管道到独唯一的socket
- 异步读取文件的块
- 将块写入TCP衔接和任何监听sockets
- 假如这些资本中的任何一个发作毛病,请关照一切其他附加资本,他们须要清算和封闭它们
正如我们从这个例子中可以看到的,当涌现毛病时,必需采用更多步伐来准确清算资本,而不是经由过程域API严厉完成,一切域供应的都是非常聚合机制。纵然在域中流传数据的潜伏有效才能也轻易被抵消,在本例中,经由过程将须要的资本作为函数参数通报。
只管存在不测的非常,但应用领域的一个题目依然是可以继承实行(与文档所述相反)的简朴性,这个例子证明了这个主意背地的谬论。
跟着应用顺序自身的庞杂性增添,尝试对不测非常举行恰当的资本清算会变得越发庞杂,此示例仅具有3个基础资本,而且一切资本都具有明白的依靠途径,假如应用顺序运用共享资本或资本重用之类的东西,那末清算才能和准确测试清算事情的才能就会大大增添。
末了,就处置惩罚毛病而言,域不仅仅是一个美化的'uncaughtException'
处置惩罚顺序,除了第三方更隐式和不可视察的行动。
资本流传
域的另一个用例是运用它来沿异步数据途径流传数据,一个题目在于,当客栈中有多个域时(假如异步客栈与其他模块一同事情,则必需假定),什么时候希冀准确的域是隐约的。别的,可以依靠域举行毛病处置惩罚同时还可以检索必要的数据之间存在争执。
下面是一个运用域沿着异步客栈流传数据失利的示例:
const domain = require('domain');
const net = require('net');
const server = net.createServer((c) => {
// Use a domain to propagate data across events within the
// connection so that we don't have to pass arguments
// everywhere.
const d = domain.create();
d.data = { connection: c };
d.add(c);
// Mock class that does some useless async data transformation
// for demonstration purposes.
const ds = new DataStream(dataTransformed);
c.on('data', (chunk) => ds.data(chunk));
}).listen(8080, () => console.log('listening on 8080'));
function dataTransformed(chunk) {
// FAIL! Because the DataStream instance also created a
// domain we have now lost the active domain we had
// hoped to use.
domain.active.data.connection.write(chunk);
}
function DataStream(cb) {
this.cb = cb;
// DataStream wants to use domains for data propagation too!
// Unfortunately this will conflict with any domain that
// already exists.
this.domain = domain.create();
this.domain.data = { inst: this };
}
DataStream.prototype.data = function data(chunk) {
// This code is self contained, but pretend it's a complex
// operation that crosses at least one other module. So
// passing along "this", etc., is not easy.
this.domain.run(() => {
// Simulate an async operation that does the data transform.
setImmediate(() => {
for (let i = 0; i < chunk.length; i++)
chunk[i] = ((chunk[i] + Math.random() * 100) % 96) + 33;
// Grab the instance from the active domain and use that
// to call the user's callback.
const self = domain.active.data.inst;
self.cb(chunk);
});
});
};
以上显现,很难有多个异步API尝试运用域来流传数据,可以经由过程在DataStream
组织函数中分派parent: domain.active
来修复此示例,然后在挪用用户的回调之前经由过程domain.active = domain.active.data.parent
恢复它。别的,'connection'
回调中的DataStream
实例化必需在d.run()
中运转,而不是简朴地运用d.add(c)
,否则将没有运动域。
简而言之,为此祷告有时机运用,须要严厉遵守一套难以实行或测试的原则。
机能题目
运用域的主要要挟是开支,运用node的内置http基准测试http_simple.js
,没有域,它可以处置惩罚凌驾22,000个要求/秒。假如它在NODE_USE_DOMAINS=1
下运转,那末该数字会下降到低于17,000个要求/秒,在这类状况下,只要一个全局域。假如我们编辑基准测试,那末http要求回调会建立一个新的域实例,机能会进一步下降到15,000个要求/秒。
虽然这可以不会影响仅效劳于每秒几百以至一千个要求的效劳器,但开支量与异步要求的数目成正比,因而,假如单个衔接须要衔接到其他几个效劳,则一切这些效劳都邑致使将终究产物交付给客户端的整体耽误。
运用AsyncWrap
并跟踪在上述基准测试中挪用init/pre/post/destroy
的次数,我们发明一切被挪用事宜的总和凌驾每秒170,000次,这意味着纵然为每种挪用增添1微秒的开支,任何范例的设置或撤除都邑致使17%的机能丧失。
固然,这是针对基准测试的优化计划,但我置信这演示了域等机制尽量低价运转的必要性。
展望未来
域模块自2014年12月以来一向被软弃用,但还没有被删除,由于node现在没有供应替代功用,在撰写本文时,正在举行构建AsyncWrap
API的事情以及为TC39预备地区的发起,在这类状况下,有恰当的功用来替代域,它将阅历完整弃用周期并终究从核心中删除。