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);

11 回复

标题:Nodejs的domain到底肿么了?

内容: 最近在项目中遇到了一个奇怪的问题。我们在使用Node.js访问MySQL或Redis时,如果回调函数出错了,domain居然无法捕获这些错误,这让我们感到非常困惑。为了更好地理解这个问题,我模拟了一个类似的场景来展示这个问题的具体表现。

首先,我们来看一下运行过程。我们将运行两个文件:server.jsserver2.jsserver.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 了。所以这个问题也不用再被挖出来了 =。=

详细的讨论:https://github.com/nodejs/node/issues/66

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/awaittry/catch 结构来处理异步错误。这样可以确保所有的异常都能被捕获并妥善处理,避免因异常而导致进程退出。

回到顶部