与Node.js长连接,如何减少内存使用和防止内存泄漏?也与 V8 和 webkit-devtools 相关
Posted
技术标签:
【中文标题】与Node.js长连接,如何减少内存使用和防止内存泄漏?也与 V8 和 webkit-devtools 相关【英文标题】:Long connections with Node.js, how to reduce memory usage and prevent memory leak? Also related with V8 and webkit-devtools 【发布时间】:2012-12-12 12:21:04 【问题描述】:这是我正在尝试做的事情:我正在开发一个 Node.js http 服务器,它将保持长连接,以便在一台机器上从数万个移动客户端推送(与 redis 协作)。
测试环境:
1.80GHz*2 CPU/2GB RAM/Unbuntu12.04/Node.js 0.8.16
第一次,我使用“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);
最后,这是我的问题:
如何减少内存使用,从而进一步提高并发性?
我真的很困惑如何计算 Node.js 进程的内存使用量。我知道 Node.js 由 Chrome V8 提供支持,有 process.memoryUsage() api,它返回三个值:rss/heapTotal/heapUsed,它们之间有什么区别,我应该更关注哪一部分, Node.js 进程使用的内存的具体构成是什么?
我担心内存泄漏,尽管我已经做了一些测试并且似乎没有问题。有什么需要注意的地方或建议吗?
我找到了一个关于 V8 hidden class 的文档,正如它所描述的,这是否意味着每当我将一个名为 clientId 的属性添加到我的全局对象 pendingClients 时像我上面的代码一样,会生成一个新的隐藏类吗?会不会造成内存泄漏?
我使用webkit-devtools-agent 来分析Node.js 进程的堆映射。我开始这个过程并拍摄了一个堆快照,然后我向它发送了 10k 个请求并稍后断开它们,之后我再次拍摄了一个堆快照。我使用 comparison 视角来查看这两个快照之间的差异。这是我得到的: 谁能解释一下? (array)/(compiled code)/(string)/Command/Array 的数量和大小都增加了很多,这是什么意思?
编辑: 我是如何运行负载测试的? 1.首先我修改了服务端和客户端的一些参数(要实现60k以上的并发需要多台客户端,因为一台机器最多只有60k+端口(以16位表示)) 1.1。服务器和客户端机器之一,我修改了文件描述符,在将运行测试程序的 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编写了一个自定义的模拟客户端程序,因为大多数负载测试工具,ab,siege等都是针对短连接的,但我使用的是长连接并且有一些特殊要求。 3. 然后我在一台机器上启动服务器程序,在另外三台分开的机器上启动三个客户端程序。
编辑: 我确实在单台机器(2GB RAM)上达到了 250k 并发连接,但事实证明,这不是很有意义和实用。因为当连接连接时,我只是让连接挂起,没有别的。当我尝试向他们发送响应时,并发数下降到 150k 左右。据我计算,每个连接的内存使用量增加了大约 4KB,我想这与我设置为 4096 16384 33554432 的 net.ipv4.tcp_wmem 有关,但即使我修改了它变小了,没有任何改变。我不知道为什么。
编辑: 实际上,现在我更感兴趣的是每个 tcp 连接使用多少内存以及单个连接使用的内存的确切组成是什么?根据我的测试数据:
150k 并发消耗大约 1800M RAM(来自 free -m 输出),Node.js 进程大约有 600M RSS
然后,我假设:
(1800M - 600M) / 150k = 8k,这是内核TCP栈内存使用单个连接,它由两部分组成:读缓冲区(4KB)+写缓冲区(4KB)(其实这个与我上面的 net.ipv4.tcp_rmem 和 net.ipv4.tcp_wmem 的设置不匹配,系统如何确定这些缓冲区使用多少内存?)
600M / 150k = 4k,这是Node.js单个连接的内存使用量
我说的对吗?如何减少这两个方面的内存使用量?
如果有什么地方我描述的不好,请告诉我,我会完善它! 任何解释或建议将不胜感激,谢谢!
【问题讨论】:
第一印象是在具有这些规格的机器上 250k 是惊人的。可能是时候专注于吸引您现在担心的所有用户了。 =P 你是如何测量并发连接数的? 旁注:在 stackexchange 网站上提问时,最好坚持较少数量的具体问题。这样你会得到更多的答案。 @tehgeekmeister 获得这些用户不是我关心的问题,我只关心技术因素,我真的想做到最好:) @tehgeekmeister 还有一些细节我没有在上面的代码中展示:服务器也有一个统计api,所以我可以看到在特定时间有多少待处理的客户端在线。关于这个高并发问题,我脑子里有很多问题(上面的问题只是其中的一部分),它们显然是相关的,所以我认为把它们放在一起可能会更好。 【参考方案1】:我认为您不必担心会进一步降低内存使用量。从您包含的读数来看,您似乎非常接近可以想象的最低限度(我将其解释为以字节为单位,这是未指定单位时的标准)。
这是一个比我能回答的更深入的问题,但这就是RSS。据我所知,堆是 unix 系统中动态分配内存的来源。因此,堆总数似乎是在堆上分配给您使用的所有内容,而使用的堆是您已使用的分配量。
您的内存使用情况非常好,看起来您实际上并没有泄漏。我还不会担心。 =]
不知道。
这个快照看起来很合理。我预计由于请求激增而创建的一些对象已被垃圾收集,而其他对象则没有。您会看到没有超过 10k 个对象,而且这些对象中的大多数都非常小。我认为这很好。
不过,更重要的是,我想知道您是如何对此进行负载测试的。我以前尝试过像这样进行大规模负载测试,但由于打开文件描述符数量的限制(默认情况下每个进程通常大约一千个),大多数工具根本无法在 linux 上生成这种负载)。同样,一旦使用了套接字,它就不能立即再次使用。我记得,它需要一分钟的时间才能再次使用。在这一点和我通常看到系统广泛打开的文件描述符限制设置在 100k 以下的事实之间,我不确定是否有可能在未修改的盒子上接收这么多负载,或者在单个盒子上生成它。由于您没有提及任何此类步骤,我认为您可能还需要调查您的负载测试,以确保它按照您的想法进行。
【讨论】:
我已经更新了关于我如何运行测试的帖子。您提到“一旦使用套接字,它就不能立即再次使用”,在我的场景中没有这样的问题,因为我使用长连接。而且我认为您是在谈论客户端的临时端口,而不是“套接字”。 错误,我的意思是文件描述符。我想。可能是。真的,我们已经达到了我对这些事情的理解程度的极限。 我已经通过 ulimit 命令消除了这个限制。并且感谢您的帮助,顺便说一下,我不是英语母语人士,也许有一些我没有解释清楚的地方,请告诉我,我会尽力而为。 :) 我很乐意提供更多帮助,但老实说,我不确定目前还能做些什么。正如我之前所说,我认为选择一个非常具体的问题会做得更好。我最近一直在回答很多问题,像这样的问题往往收效甚微,但高度具体的问题几乎总能得到令人满意的答案。如有必要,可以提出更多问题,但保持具体是有用的。 你看这里了吗hacks.mozilla.org/2012/11/…【参考方案2】:只是一些注意事项:
是否需要将 res 包裹在一个对象中 res: res 可以直接赋值吗
pendingClinets[req.clientId] = res;
编辑另一个可能有帮助的~微优化
server.emit('request', req, res);
将两个参数传递给“请求”,但您的请求处理程序实际上只需要响应“res”。
res['clientId'] = 'whatever';
server.emit('request', res);
虽然您的实际数据量保持不变,但在“请求”处理程序参数列表中减少 1 个参数将为您节省一个引用指针(几个字节)。但是当您处理数十万个连接时,几个字节可以加起来。您还可以节省在发出调用时处理额外参数的少量 CPU 开销。
【讨论】:
是的,现在直接分配它可以工作,所以我会尝试一下。关于 'error' 事件,Node.js 官方文档说:'close' 事件会在这个事件之后直接调用,所以我觉得我的做法是可以的。感谢您提供这些说明。 @Aaron Wang - 我应该是 RTFM 的那个,我编辑了它并添加了另一个小的优化,可能会帮助你从服务器中发出更多的声音。 将 res 直接分配给 pendingClients 对象确实可以节省一些内存,每 60k 连接大约 20M,谢谢!至于你的新笔记,为什么我这样做是为了提供与官方 http 模块相同的接口,实际上我使用 req 来跟踪日志中的请求信息,但我没有显示这些细节在上面的代码中为简化起见。另一个原因是 req 会被垃圾回收,所以我不是很担心。以上是关于与Node.js长连接,如何减少内存使用和防止内存泄漏?也与 V8 和 webkit-devtools 相关的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 Node.js Stream API 减少服务器端内存消耗?
如何使用 Node.js Stream API 减少服务器端内存消耗?