与Node.js长时间连接,如何减less内存使用并防止内存泄漏? 还与V8和webkit-devtools相关

下面是我正在做的事情:我正在开发一个Node.js http服务器,它将在一台机器上为成千上万的移动客户端推送目的(与redis协作)保持长时间连接。

testing环境:

1.80GHz*2 CPU/2GB RAM/Unbuntu12.04/Node.js 0.8.16 

在第一次使用“快速”模块时,在使用交换之前,我可以达到约12万个并发连接,这意味着RAM不够。 然后,我切换到本地“http”模块,我得到的并发性高达160K。 但是我意识到在本地http模块中仍然有太多的function,所以我把它切换到本地“net”模块(这意味着我需要自己处理http协议,但没关系)。 现在,我可以达到每台机器大约25万个并发连接。

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

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

最后,这是我的问题:

  1. 如何减less内存使用量,以便进一步提高并发性?

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

  3. 我担心内存泄漏,即使我已经做了一些testing,似乎没有问题。 有任何我应该关注的问题或build议吗?

  4. 我发现一个关于V8 隐藏类的文档,就像它描述的那样,这是否意味着每当我把我的全局对象pendingClients添加一个名为clientId的属性,就像我上面的代码一样,会产生一个新的隐藏类? 剂量会造成内存泄漏?

  5. 我使用webkit-devtools-agent来分析Node.js进程的堆映射。 我开始了这个过程并拍摄了一个堆快照,然后我向它发送了10k个请求,并在稍后断开连接,之后我再次获取了一个堆快照。 我用比较的angular度来看看这两个快照之间的区别。 这是我得到的: 在这里输入图像说明 有谁能解释这个吗? (数组)/(编译代码)/(string)/ Command / Array的数量和大小增加了很多,这是什么意思?

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

 ulimit -Hn 999999 ulimit -Sn 999999 

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 

1.3。 至于客户端机器:

 net.ipv4.ip_local_port_range = 1024 65535 

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

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

编辑 :其实,现在我更感兴趣,每个TCP连接使用多less内存,以及单个连接使用的内存的组成是什么? 根据我的testing数据:

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

然后,我假设:

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

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

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

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

  1. 我想你不应该担心进一步减less内存使用量。 从包含的读数来看,似乎你已经非常接近最低限度的可能了(我把它解释为以字节为单位,这在没有指定单位时是标准的)。

  2. 这是一个比我可以回答更深入的问题,但这里是RSS 。 这个堆是在unix系统中dynamic分配内存的地方,据我所知。 所以,堆总看起来好像是所有分配在堆上的用于你的使用,而堆使用的是多less分配你已经使用。

  3. 你的内存使用情况相当不错,而且似乎并没有泄漏。 我现在不用担心。 =]

  4. 不知道。

  5. 这个快照看起来合理。 我期望从请求的激增中产生的一些对象被垃圾收集,而其他的则没有。 你看,没有什么比10k对象多,而且这些对象大部分都很小。 我称之为好。

更重要的是,我想知道你是如何进行负载testing的。 我以前试过做这样的大规模负载testing,而大多数工具根本无法设法在linux上产生这种负载,因为打开的文件描述符的数量是有限制的(默认每个进程大约一千个)。 而且,一旦使用套接字,就不能立即使用。 我记得,这需要一分钟的时间才能再次使用。 在这之间和我通常看到系统全开的文件描述符限制设置在100k以下的事实之间,我不确定是否有可能在一个未经修改的盒子上接收那么多的负载,或者在一个盒子上生成它。 既然你没有提到任何这样的步骤,我想你可能还需要调查你的负载testing,以确保它在做你的想法。

只是几个注意事项:

你需要将res包装在一个对象{res:res}中,你可以直接指定它

 pendingClinets[req.clientId] = res; 

编辑另一个〜微型优化,可能会有所帮助

 server.emit('request', req, res); 

将两个parameter passing给“请求”,但是你的请求处理程序确实只需要响应“res”。

 res['clientId'] = 'whatever'; server.emit('request', res); 

而你的实际数据量保持不变,在'request'处理程序参数列表中less一个参数将为你节省一个引用指针(几个字节)。 但是当你处理数十万个连接时,几个字节可以加起来。 您还将保存处理发出呼叫中额外参数的次要cpu开销。