Nodejs的domain到底肿么了?
Nodejs的domain到底肿么了?
最近在项目里发现一个奇怪的问题,我们使用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);
标题:Nodejs的domain到底肿么了?
内容:
最近在项目中遇到了一个奇怪的问题。我们在使用Node.js访问MySQL或Redis时,如果回调函数出错了,domain
居然无法捕获这些错误,这让我们感到非常困惑。为了更好地理解这个问题,我模拟了一个类似的场景来展示这个问题的具体表现。
首先,我们来看一下运行过程。我们将运行两个文件:server.js
和 server2.js
。server.js
模拟了一个简单的 Redis 服务,而 server2.js
是一个 Web 服务器。通过浏览器访问 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);
server.js
创建了一个简单的 TCP 服务器,监听端口 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;
console.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);
server2.js
创建了一个 HTTP 服务器,监听端口 8080。当接收到请求时,它会创建一个 domain
并将其绑定到当前上下文。然后调用 getConnection
函数来获取连接,并尝试写入数据。在 data
事件处理程序中,我们故意抛出一个错误来模拟回调函数中的异常情况。
问题分析
问题在于 domain
只能捕获同步代码中的异常。在异步操作(如网络请求)中,如果异常发生在回调函数中,domain
无法捕获这些异常。为了解决这个问题,我们可以使用 try-catch
块或者 Promise
来确保异常被捕获。
解决方案
一种可能的解决方案是使用 Promise
来包装异步操作:
function getConnection(cb) {
return new Promise((resolve, reject) => {
if (conn) {
resolve(conn);
} else {
var client = new net.Socket();
client.connect(1104);
client.on("connect", () => {
conn = client;
resolve(conn);
});
client.on("data", (chunk) => {
console.log("recv data");
throw new Error("process error"); // 这里模拟回调出错
});
client.on("end", () => {
conn = null;
console.log("conn end");
});
client.on("error", (err) => {
reject(err);
});
}
});
}
http.createServer(function(req, res) {
var reqd = domain.create();
reqd.on("error", function(err) {
console.log("Domain Error:", err.stack);
reqd.dispose();
});
reqd.run(async function() {
try {
const conn = await getConnection();
conn.write("test", function() {
console.log("write end");
});
} catch (err) {
console.error("Caught error:", err);
}
});
}).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就为了实现连接池,就因为这样连接池功能也成了泡影了。
关于domain, 之前写过一篇blog,想通过domain来解决异步的error问题,还是很容易踩坑的吖
还是没有一个好方案来解决这个问题。
是因为conn在外面定义了 有api可以讲conn加进去 From Noder
又被挖坟了… domain 自身有很多局限性,而且导致 node core 也很难维护,所以已经被 depracated 了。所以这个问题也不用再被挖出来了 =。=
domain
模块在 Node.js 中用于捕获异步调用栈中的异常,从而避免这些异常导致整个程序崩溃。然而,domain
模块在处理一些特定情况下的错误时可能会遇到问题,尤其是在涉及网络请求或连接时。
在你提供的代码中,domain
模块没有正确捕获 getConnection
函数内部的错误,这可能是因为 domain
无法捕捉到异步操作中的所有异常。具体来说,client.on("data")
中的 throw new Error("process error")
这种错误类型可能不在 domain
的捕捉范围内。
为了更好地理解这个问题,可以尝试将错误处理逻辑进行调整。例如,可以使用 try-catch
结合 Promise
来处理异步操作中的错误:
var net = require('net');
var http = require('http');
var conn = null;
function getConnection() {
return new Promise((resolve, reject) => {
if (conn) {
resolve(conn);
} else {
var client = new net.Socket();
client.connect(1104);
client.on("connect", () => {
conn = client;
resolve(client);
});
client.on("data", () => {
console.log("recv data");
throw new Error("process error"); // 这里模拟回调出错
});
client.on("end", () => {
conn = null;
console.log("conn end");
});
}
});
}
http.createServer(async function (req, res) {
try {
const reqd = domain.create();
reqd.on("error", function (err) {
console.log("Domain Error:", err.stack);
reqd.dispose();
});
reqd.run(async function () {
const conn = await getConnection();
conn.write("test", function () {
console.log("write end");
});
});
} catch (err) {
console.error("Error caught in HTTP request handler:", err.stack);
}
}).listen(8080);
在这个例子中,我们将 getConnection
改为返回一个 Promise
,并在 http.createServer
的处理函数中使用 async/await
和 try/catch
结构来处理异步错误。这样可以确保所有的异常都能被捕获并妥善处理,避免因异常而导致进程退出。