最近在项目里发现一个奇怪的问题,我们使用nodejs去访问mysql或者redis的时候,如果回调函数出错了,domain居然抓不到,很纳闷,于是我模拟了一个这样的场景来说明这个问题。我先说一下运行过程,下面会贴出来两段代码,先运行server.js(这是一个类似redis的服务),然后运行server2.js(这个是web server服务),然后通过浏览器访问localhost:8080,第一次能看到被domain抓住了,然后打印了出错信息,但是当你再次刷新浏览器的时候,你会发现抛出的错误没被domain抓到,于是进程就退出了。。大家一起研究研究,这到是什么问题。。
server.js
var net = require('net');
net.createServer(function(conn){
console.log("new client");
conn.on("data", function(chunk){
console.log("data:", chunk.toString());
conn.write("hi " + chunk);//这里模拟数据处理完毕并且返回数据
});
conn.on("close", function(){
console.log("client end");
});
}).listen(1104);
server2.js
var net = require('net'),
http = require('http'),
domain = require('domain');
var conn = null;
function getConnection(cb)
{
if(conn){
cb(conn);
}else{
var client = new net.Socket();
client.connect(1104);
client.on("connect", function(){
conn = client;
cb(conn);
});
client.on("data", function(chunk){
console.log("recv data");
throw new Error("process error");//这里模拟回调出错
});
client.on("end", function(){
conn = null;
consoel.log("conn end");
});
}
}
http.createServer(function(req, res){
var reqd = domain.create();
reqd.on("error", function(err){
console.log("Domain Error:", err.stack);
reqd.dispose();
});
reqd.run(function(){
getConnection(function(conn){
conn.write("test", function(){
console.log("write end");
});
});
});
}).listen(8080);
reqd.dispose();
此行注释掉后能正常 catch error 这可能是个bug。 reqd与geConnection绑定而后dispose了一次,估计dispose阻止后续domain再与getConnect进行绑定,因此第二次http请求抛出的error就没有被catch
这样是不行的,你试着把reqd.dispose()改成res.end(err.stack),表示把服务器的错误信息返回给浏览器,你会发现,第一次刷新页面没有问题,但第二次、第三次之后所有请求都会一直处于加载状态。
//getConnection(function(c){ console.log('got connection'); });
var req_counter=0;
http.createServer(function(req, res){
req_counter++;
console.log('>>>>>>request: %s<<<<<<<',req_counter);
var reqd = domain.create();
res.counter=req_counter;
reqd.counter=req_counter;
console.log('[request]res.counter=%s',res.counter);
console.log('[request]reqd.counter=%s',reqd.counter);
reqd.on("error", function(err){
console.log('[error]res.counter=%s',res.counter);
console.log('[error]reqd.counter=%s',reqd.counter);
res.end(err.stack);
//reqd.dispose();
//if(conn){ conn.end(); conn=null; }
});
reqd.run(function(){
getConnection(function(conn){
conn.write("test", function(){
console.log("write end");
});
});
});
}).listen(8080);
输出信息
>>>>>>request: 1<<<<<<<
[request]res.counter=1
[request]reqd.counter=1
recv data
[error]res.counter=1
[error]reqd.counter=1
write end
>>>>>>request: 2<<<<<<<
[request]res.counter=2
[request]reqd.counter=2
recv data
[error]res.counter=1
[error]reqd.counter=1
write end
>>>>>>request: 3<<<<<<<
[request]res.counter=3
[request]reqd.counter=3
write end
recv data
[error]res.counter=1
[error]reqd.counter=1
这解释了为什么取消reqd.dispost()后,第2次、3次一直加载,因为on(‘error’)回调函数中的res对象指向的一直是第一次请求所对应的response,同样,捕捉到异常的domain对象也一直第一次请求时生成的domain。第2、3次请求所生成的domain对象完全没起到作用。
取消下面2行的注释
//reqd.dispose();
//if(conn){ conn.end(); conn=null; }
再测试一次,输出信息
>>>>>>request: 1<<<<<<<
[request]res.counter=1
[request]reqd.counter=1
recv data
[error]res.counter=1
[error]reqd.counter=1
conn end
>>>>>>request: 2<<<<<<<
[request]res.counter=2
[request]reqd.counter=2
recv data
[error]res.counter=2
[error]reqd.counter=2
这应该才是楼主原程序预期的效果。假如取消下面这行的注释
//getConnection(function(c){ console.log('got connection'); });
则后续所有的domain均失效。
domain.run(function()…此隐式(Implicit)绑定是直接绑定底层的io对象,同一个io对象只能进行一次隐式绑定,若要绑定第二个domain则要显式(Explicit)绑定domain.add(…),也就是第2次、3次要reqd.add(conn)。但由于第一次请求已经dispose过,底层conn已经关闭连接,后续的conn.write将失效,conn不会再接受到任何的数据,则又卡死运行流程。
总之,目前看来domain是个很艹蛋的东西,而dispose则又是非常糟糕的api,关于这个接口的open issue就有3个… https://github.com/joyent/node/pull/3559 https://github.com/joyent/node/issues/5019 https://github.com/joyent/node/pull/4153
1、我这里只是做个demo,在实际的开发中,httpServer层跟应用层是分开的,而getConnection这个东西是在应用层,不可能让一外层的服务去涉及应用的层的东西吧? 2、就算在httpServer去调用应用层的closeConn方法,但因为这是一个共享的conn对像,而在这个domain发生错误之前,已经有其它请求已经拿到这个共享的conn正准备用,这时候这个domain因为出错,去关闭了conn对像,其它请求里面用到这个conn对像就全出问题了; 3、如果不使用共享conn对像,那么一个请求就是一个连接,对于nodejs没有一个固定的并发请求数量控制,如果这个conn是一个mysql,估计mysql早就开始报连接已满了; 4、本来使用共享的conn就为了实现连接池,就因为这样连接池功能也成了泡影了。