node.js长连接高并发问题,很有挑战,大家来讨论下
发布于 12 年前 作者 inetfuture 64085 次浏览 最后一次编辑是 8 年前

我在做长连接服务器的时候遇到些问题,stackoverflow链接

现在平均下来每个连接占12k多的内存,感觉还不是很理想(希望降到8k以下),谁能指点下(linux内核调优方面和node.js代码方面等)? 谢谢!

另外搜集资料的时候,发现这种场景以下几种技术用得也比较多:c/c++(libev,libeio)(当然node.js底层也是他们),Java NIO,erlang,有哪位大神做过得,可否分享下经验?相比起来,哪种更有优势?或者谁对这几种方案比较熟悉,做个demo来跟node.js比一下?

2013-1-8: 做了以下测试,发现一些奇怪的现象: <table border=“1”> <tr> <th>序号</th><th>并发量(万)</th><th>free -m(used -buffers/cache)</th><th>node.js RSS</th><th>描述</th> </tr> <tr> <td>1</td><td>6</td><td>500</td><td>210</td><td>无sendResonse,无cacheSocket</td> </tr> <tr> <td>2</td><td>6</td><td>578</td><td>290</td><td>无sendResonse,有cacheSocket</td> </tr> <tr> <td>3</td><td>6</td><td>822</td><td>290</td><td>有sendResonse,有cacheSocket</td> </tr> </table>

sendResonse,cacheSocket见下面代码:

var net = require('net');

var pendingClients = {};
var clientsCount = 0;

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

            // Doing this to provide the same interface with official http module
            var req = {
                clientId: 'whatever'
            };
            var res = new ServerResponse(socket);
            server.emit('request', req, res);
        }
    }
});

server.on('request', function (req, res) {
    // Other business logic
    // ...

    clientsCount++
    console.log(clientsCount);

    sendResponse(res);
    cacheSocket(req, res);
});

server.listen(3000);

// Custom ServerResponse
function ServerResponse(socket) {
    this.socket = socket;
}
ServerResponse.prototype.write = function(data) {
    if (this._headerSent) {
        // Send http header
        // ...

        this._headerSent = true;
    }

    // Parse data to 'Transfer-Encoding: chunked'
    // ...

    this.socket.write(data);
}

function sendResponse(res) {
    res.write('PING');
}

function cacheSocket(req, res) {
    pendingClients[req.clientId] = {
        res: res
    };

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

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

        clientsCount--;
    });
}

问题:

  • 3和2相比,RSS不变,也就是说用于内核TCP栈的内存多了200多M,平均每个连接多了接近4k,我怀疑是内核socket的send buffer,但是调整net.ipv4.tcp_wmem不起作用?如果是send buffer,为何发送响应之前没有出现,难道是只有发送数据时才开辟吗?网上实在是找不到资料能解释这个问题了……
  • 2和1相比,不缓存node.js生成的socket对象,省了80M左右,我打算采用@codekiller的建议,直接操作handle,肯看效果怎么样,后面会把结果放上来
  • 还有一个十分诡异的问题,关于net.ipv4.tcp_mem。当该值设置为:48120 64161 96240(单位是page,一个page4KB,刚好是tcp_wmem和tcp_rmem的最小值)(Ubuntu12.04默认值)时,采用第一种测试方法,最多只能压进96238个并发连接,再多一个,整个server的tcp连接都会停止响应,ssh都连不进去,这肯定跟net.ipv4.tcp_mem的最大值96240有关,但是sendResponse时,却没有这个问题,实在想不通,何解?

2013-1-12 采用@codekiller的建议后,直接使用handle,每6万连接大概省下100M内存,在此表示感谢!

但是对TCP socket的receive buffer和send buffer还是没弄明白,如何进一步优化?以及上面的诡异现象是什么原因?

最近打算看一下底层的libuv,因为感觉javascript层面已经没什么可做的了,希望能从底层找到突破口(比如直接设置socket的buffer值,现在好像没有这个接口,看看自己能不能加上)

欢迎大家发表意见,谢谢!

25 回复

At the first time, I used “express” module, with which I could reach about 120k concurrent connections

express有这么强吗

120k这个也不是很准,因为我没有响应数据,后面测试发现开始相应请求后,每个连接会多占4k内存的样子,我猜跟socket send buffer有关,但是调整net.ipv4.tcp_wmem不起任何作用。另外express就是在原生http模块上封装了一些东西(比如在reqres上挂一些属性),肯定会多占一些内存,但是也不至于太多。

每人8K就是1000人在线需要8M,1W在线80M左右。

每人12KB万人在线也就消耗120M左右

很好奇LZ是什么史诗级应用, 顶级游戏服务器也不过为1W人左右并发连接设计

i5以上的CPU每核也就够NODE消耗1000个左右的并发响应, 超过这个数单是上下文切换就够它受的了,LZ还是先不要考虑内存的问题吧

纯粹用来推消息的应用,整个集群需要支撑千万级的用户量,所以单机并发量自然越高越好,可以省点机器。

实际上大部分连接大部分时候都会空闲,只占内存,所以我目前不是很担心CPU,想着先把内存占用优化到极致,再考虑推送消息的速率(CPU、网速等因素)。另外,也是好奇,极致能做到多少?在考虑这个问题的过程中也学到了不少东西,但是现在感觉卡住了。。。

i5以上的CPU每核也就够NODE消耗1000个左右的并发响应,超过这个数单是上下文切换就够它受的了

请问这个数据是怎么来得呢,node.js的上下文切换具体是指什么意思,谢谢!

nodejs是单线程的,不存在上下文切换,楼主的思路是对的,应该从消耗内存入手 ,我猜你目前要做的是网关服务器,只负责维护大量socket长链接,做消息转发,这样的话最好不要用express,它加那些东西没必要,另外,记得设置socket的两个属性,socket.setTimeout(0);socket.setNoDelay(true);这样内存消耗最少,实时性好,但是通信效率低一些,多个tcp packet不会组合成一个发送。如果真的是长链接,做游戏服务器,推荐ws,一个高效率的websocket实现,性能很好,我们也在用它做手游的服务端。

看到你发了代码? 另外发现这句: var res = new ServerResponse(socket);

完全没有必要用ServerResponse包装,你要写什么直接调用socket的write就好了,SR内部也会做buffer消耗你的内存。对了,kernel层面为你的每个socket消耗的内存你是很难优化了,默认的设置已经很合理,上面的socket.setTimeout(0);socket.setNoDelay(true);其实也是在设置该socket的内核handle参数了

你可以改变的是nodejs层面的东西,socket本身是继承自Stream接口的,Stream内存也有缓存,你想不用socket,直接使用handle也可以,在net模块里,self._handle.onconnection = onconnection;自己写onconnection,这个回调里每个连接进来你基本上就是得到一个socket handle,也就是对kernel socket handle的包装,直接用它好了

如果你还想再进一步,算了 哥们儿 你放弃nodejs,自己用c吧,用libev+mmap ,再进一步,你还可以写kernel driver,直接读写网卡硬件端口,再进一步。。。哥已经帮不了你了。。。

有意义嘛?

的确是类似网关的东西

socket默认就是没有超时和setNoDelay的

ws不太了解,一会看一下,谢谢!

ServerResponse这个是我自己定义的,里面封装了http响应头Transfer-Encoding: chuned的处理,这样做是为了跟原生http模块保持一致。

直接使用handle

我以为在node.js层面net.Socket已经够底层了,看来不是,你说得方法值得一试。

自己用c吧,用libev+mmap

这个有空绝对要做一下看看

kernel driver,直接读写网卡硬件端口

这个就有点太底层了……目前没这个能力

有意义吗?

当然,可以了解到不少底层的东西。。。

谢谢你的建议!

v8是单线程的,不代表NODE就是单线程的,之所以NODE不是单线程的是因为其各种中间件模块的运作不是单线程的,例如MYSQL BINDING;当然会存在上下文切换

@inetfuture “实际上大部分连接大部分时候都会空闲” 这样的连接用长连接没有意义吧

觉得NODE不适合做这种应用

var rep = '高手甚多呀';

@seasonx4

好吧 严格来说,node确实不是单线程的,底层eventio也是多个线程协作,其它中间件比如数据库连接池也可能开自己的线程。我表达的准确意思是,它的执行模型是单线程的。

所以要使用多核cpu的优势,你得用cluster启动多进程,而不是单进程多线程。

我认为楼主的上下文切换是特指: 比如在 connection per thread 处理模型下,导致高并发时产生的大量线程切换的系统开销,包括每次线程切换保存的上下文所消耗的内存*几万 + 每次从几万个候选线程中正确选择下个线程并恢复上下文的调度算法所消耗的cpu 这个是性能杀手,到这个量级的 经常会有20%以上并且波动很大的系统资源用于干这个事情

只有这么大量的上下文切换导致的开销才值得去考虑和优化,这就是为什么eventio得服务器框架通常是单线程执行模型得原因

你说的这几个线程总数加起来太少了 不超过100个吧? 它们的上下文开销在大规模高性能服务器设计里可以忽略不计得。

@seasonx4 推消息要么长连接要么轮询,我们的客户端主要是手机等移动设备,所以从省电省流量角度考虑,长连接应该合适一些(而且实时性更好)。大量并发长连接,多线程什么的肯定不行的,个人觉得node.js这种模型正适合干这个,还有什么更好的选择吗?

看了一下net.Socket的实现,觉得这个地方没办法省了,它上面挂的属性也不多,多数是方法,直接用handle的话差不多也要处理那么多东西。。。

node.js的单线程是指所有用户代码是在单线程中执行的是吧?我用pstree看node.js进程,显示这个:

node───2*[{node}]

这应该是两个线程吧?

@inetfuture 排查了一下,其中一个线程应该是mongodb driver的

@inetfuture 你得看Stream.Readable和Stream.Writeable

他们内部都有缓存,你可以设置高水底水去控制,或者直接不要用Steam,我觉得这更适合你得需求

@codekiller 它们内部的缓存就是数组,这个没数据的时候不会占内存吧?

@inetfuture 要无所不用其极的压榨内存 自然可以无关效率

假设现在有10万个socket 就会有20万个buffer数组 在每个时刻,其中绝大部分都有还来不及处理的数据。

@codekiller 嗯,准备直接操作handle试下,谢谢!

想问一下,压测长连接的并发量,一般通过什么方法压比较靠谱?

我都是用node.js根据业务场景自己模拟客户端来测,长连接不像http,http是标准的,有很多开源工具(ab、siege)可以用。

楼主你好,我们做的应用和你这个类似,是连接网关的一个东西。目前我们是用java nio来实现的,我想问的是如果换成node来实现是不是代码会更简单,效率能更高些。

纯粹比效率java比node.js还是要高点的,不过java的gc比较难调,设得不好来次gc会有很长的延时。

用netty的话每个连接要占50k左右的内存。

我们做mqtt的长连接每个连接也要10k以上,用socket.io要更多,不过现在内存便宜, 觉得没必要做优化。 从实用的角度这样已经够用了, 不过研究一下底层确实可以学到不少东西,期待楼主的成果。

LZ 菜鸟请教一个问题,怎么看一个socket连接占用内存数? 用工具?

回到顶部