与Node.js的长连接,如何减少内存使用并防止内存泄漏?也与V8和webkit-devtools有关

Aar*_*ang 47 javascript sockets linux tcp node.js

以下是我正在尝试做的事情:我正在开发一个Node.js http服务器,该服务器将在一台计算机上保存数千个移动客户端的推送目的(与redis协作)的长连接.

测试环境:

1.80GHz*2 CPU/2GB RAM/Unbuntu12.04/Node.js 0.8.16
Run Code Online (Sandbox Code Playgroud)

第一次,我使用"express"模块,在使用交换之前我可以使用它来达到大约120k并发连接,这意味着RAM不够.然后,我切换到本机"http"模块,我得到了大约160k的并发.但我意识到在本机http模块中仍然有太多功能我不需要,所以我把它切换到本机"net"模块(这意味着我需要自己处理http协议,但没关系).现在,每台机器可以达到大约250k并发连接.

这是我的代码的主要结构:

var net = require('net');
var redis = require('redis');

var pendingClients = {};

var redisClient = redis.createClient(26379, 'localhost');
redisClient.on('message', function (channel, message) {
    var client = pendingClients[channel];
    if (client) {
        client.res.write(message);
    }
});

var server = net.createServer(function (socket) {
    var buffer = '';
    socket.setEncoding('utf-8');
    socket.on('data', onData);

    function onData(chunk) {
        buffer += chunk;
        // Parse request data.
        // ...

        if ('I have got all I need') {
            socket.removeListener('data', onData);

            var req = {
                clientId: 'whatever'
            };
            var res = new ServerResponse(socket);
            server.emit('request', req, res);
        }  
    }
});

server.on('request', function (req, res) {
    if (res.socket.destroyed) {            
        return;
    }

    pendingClinets[req.clientId] = {
        res: res
    };

    redisClient.subscribe(req.clientId);

    res.socket.on('error', function (err) {
        console.log(err);
    });

    res.socket.on('close', function () {
        delete pendingClients[req.clientId];

        redisClient.unsubscribe(req.clientId);
    });
});

server.listen(3000);

function ServerResponse(socket) {
    this.socket = socket;
}
ServerResponse.prototype.write = function(data) {
    this.socket.write(data);
}
Run Code Online (Sandbox Code Playgroud)

最后,这是我的问题:

  1. 如何减少内存使用量以便进一步增加并发性?

  2. 我真的很困惑如何计算Node.js进程的内存使用情况.我知道Node.js是由Chrome V8驱动的,有process.memoryUsage() api,它返回三个值:rss/heapTotal/heapUsed,它们之间的区别是什么,我应该关注哪个部分,以及它的确切组成是什么Node.js进程使用的内存?

  3. 我担心内存泄漏,即使我做了一些测试,似乎没有问题.我应该关注或有任何建议吗?

  4. 我发现了一个关于V8 隐藏类的文档,正如它所描述的那样,这意味着每当我将clientId命名的属性添加到我的全局对象pendingClients时,就像上面的代码一样,会生成一个新的隐藏类吗?剂量会导致内存泄漏吗?

  5. 我使用webkit-devtools-agent来分析Node.js进程的堆映射.我启动了这个过程并拍了一个堆快照,然后我向它发送了10k个请求并在以后断开它们之后,我再次拍摄了一个堆快照.我使用比较视角来查看这两个快照之间的区别.这是我得到的: 在此输入图像描述 有人能解释一下吗?(数组)/(编译代码)/(字符串)/ Command/Array的数量和大小增加了很多,这是什么意思?

编辑:我是如何运行加载测试的?
1.首先,我在服务器机器和客户端机器上修改了一些参数(实现超过60k并发需要多个客户端机器,因为一台机器最多只有60k +端口(最多由16位表示))
1.1.无论是服务器还是客户端机器,我都修改了文件描述符,在运行测试程序的shell中使用这些命令:

ulimit -Hn 999999
ulimit -Sn 999999
Run Code Online (Sandbox Code Playgroud)

1.2.在服务器机器上,我还修改了一些net/tcp相关的内核参数,最重要的是:

net.ipv4.tcp_mem = 786432 1048576 26777216
net.ipv4.tcp_rmem = 4096 16384 33554432
net.ipv4.tcp_wmem = 4096 16384 33554432
Run Code Online (Sandbox Code Playgroud)

1.3.至于客户端机器:

net.ipv4.ip_local_port_range = 1024 65535
Run Code Online (Sandbox Code Playgroud)

2.其次,我使用Node.js编写了一个自定义模拟客户端程序,因为大多数负载测试工具,ab,siege等都是用于短连接,但我使用的是长连接并且有一些特殊要求.
然后我在一台机器上启动了服务器程序,在另外三台机器上启动了三个客户机程序.

编辑:我确实在一台机器(2GB RAM)上达到了250k并发连接,但事实证明,这不是很有意义和实用.因为当连接连接时,我只是让连接挂起,没有别的.当我试图向他们发送响应时,并发数量下降到150k左右.据我计算,每个连接的内存使用量大约增加4KB,我想它与net.ipv4.tcp_wmem相关,我将其设置为4096 16384 33554432,但即使我将其修改为更小,也没有任何改变.我无法弄清楚为什么.

编辑:实际上,现在我对每个TCP连接使用多少内存以及单个连接使用的内存的确切组成更感兴趣?根据我的测试数据:

150k并发消耗了大约1800M RAM(来自free -m输出),而Node.js进程大约有600M RSS

然后,我假设这个:

  • (1800M - 600M)/ 150k = 8k,这是单个连接的内核TCP堆栈内存使用情况,它由两部分组成:读缓冲区(4KB)+写缓冲区(4KB)(实际上,这与我的设置不符上面的net.ipv4.tcp_rmemnet.ipv4.tcp_wmem,系统如何确定这些缓冲区使用多少内存?)

  • 600M/150k = 4k,这是单个连接的Node.js内存使用情况

我对吗?如何减少这两个方面的内存使用量?

如果有任何我没有描述的地方,请告诉我,我会改进它!任何解释或建议将不胜感激,谢谢!

teh*_*ter 5

  1. 我认为你不应该担心进一步减少内存使用量.从你所包含的读数中,你似乎非常接近最低限度(我将其解释为以字节为单位,这是未指定单位时的标准).

  2. 这是一个比我能回答的更深入的问题,但这里是RSS.堆是动态分配内存来自unix系统的地方,据我所知.因此,堆总数似乎就是在堆上为您的使用分配的所有内容,而使用的堆是您使用的分配了多少.

  3. 你的内存使用情况非常好,而且你实际上并没有泄漏.我不担心.=]

  4. 不知道.

  5. 这个快照看似合理.我期望从请求激增中创建的一些对象被垃圾收集,而其他对象则没有.你看到没有超过10k的物体,而且大多数物体都很小.我称之为好.

但更重要的是,我想知道你是如何加载测试的.我曾尝试过像这样的大规模负载测试,并且大多数工具都无法管理在linux上产生这种负载,因为打开文件描述符的数量有限(默认情况下每个进程大约有一千个) ).同样,一旦使用套接字,它就不能立即再次使用.我记得,它需要一段时间才能再次使用.在这个和我通常看到系统广泛打开文件描述符限制设置在100k以下的事实之间,我不确定是否可以在未修改的盒子上接收那么多的负载,或者在单个盒子上生成它.由于您没有提及任何此类步骤,我认为您可能还需要调查负载测试,以确保它按照您的想法进行.