Node.js中UDP发送性能

25

我正在对一个Java UDP客户端进行基准测试,它连续发送100字节的数据报,速度尽可能快。它是使用java.nio.*实现的。测试表明,它能够实现稳定的吞吐量为每秒220k个数据报。我没有使用服务器进行测试;客户端只是将数据报发送到localhost上的某个未使用的端口。

我决定在Node.js中运行相同的测试,以比较这两种技术,但令人惊讶的是,Node.js的性能比Java慢10倍。让我带你走一遍我的代码。

首先,我使用Node.js的dgram模块创建了一个UDP套接字:

var client = require('dgram').createSocket("udp4");

然后我创建一个函数,使用该套接字发送数据报:
function sendOne() {
    client.send(message, 0, message.length, SERVER_PORT, SERVER_ADDRESS, onSend);
}

变量message是应用程序启动时从一个包含一百个字符的字符串创建的缓冲区:
var message = new Buffer(/* string with 100 chars */);

函数onSend仅增加一个变量,该变量保存到目前为止发送的数据报数量。接下来,我有一个使用setImmediate()不断调用sendOne()的函数。
function sendForever() {
    sendOne();
    setImmediate(sendForever);
} 

起初我尝试使用process.nextTick(sendForever),但是我发现它总是将自己放在事件队列的顶部,甚至在IO事件之前,正如文档says:

它在事件循环的后续刻度中,在任何其他I/O事件(包括计时器)触发之前运行。

这会防止发送IO事件的发生,因为nextTick不断将sendForever放在每个刻度的队列顶部。队列随着未读IO事件而增长,直到使Node.js崩溃:

fish: Job 1, 'node client' terminated by signal SIGSEGV (Address boundary error)

另一方面,setImmediate 在 I/O 事件回调之后触发,这就是我使用它的原因。
我还创建了一个定时器,每秒钟打印一次控制台,显示上一秒发送了多少数据报:
setInterval(printStats, 1000);

最后,我开始发送:

sendForever();

在与Java测试运行相同的机器上,Node.js每秒稳定处理 21k个数据报,比Java慢十倍
我最初的猜测是为了尝试使吞吐量加倍,在每个时刻放置两个sendOne
function sendForever() {
    send();
    send();  // second send
    setImmediate(sendForever);
}

但这并没有改变吞吐量。
我在GitHub上有一个可用的仓库,其中包含完整的代码:

https://github.com/luciopaiva/udp-perf-js

只需将其克隆到您的计算机中,cd进入该文件夹并运行:

node client

我希望能就如何在Node.js中改进此测试以及是否有办法提高Node.js的吞吐量开展讨论。有什么想法吗?

P.S.: 对于那些感兴趣的人,这里是Java部分


@mscdex,使用while循环是无济于事的,因为Node.js无法完成当前的tick并且也无法处理排队的IO事件... 应用程序会冻结。 - Lucio Paiva
1
@rels 是的,这肯定可以解释差异。我们需要通过确认 Node.js 在调用系统之前是否进行缓冲(并且进一步确认 Java 文档所指的底层缓冲区实际上是 Java 的而不是操作系统的)来测试它,并修改 Node 的源代码以添加缓冲区并查看发生了什么。如果您对此进行任何测试,请告诉我。我已经不再处理这个问题,但仍然对可能出错的原因感到好奇。 - Lucio Paiva
1
嗯,我只是在想新版本是否会表现更好。感谢分享,@JrBenito。 - Lucio Paiva
@LucioPaiva 那么这里的解决方案是什么呢?我需要建议来制作一个实时多人游戏服务器,发送udp协议数据包。Java是否是更好的选择? - newguy
如果你想处理成千上万个同时在线的玩家,建议使用Java或C++。在我的测试中,异步C++比Java快约10%。当然,如果你只期望有几百个玩家,也可以考虑使用Node.js。在开始实现服务器时,应该尽早进行负载测试,以了解它能承受多少负荷。 - Lucio Paiva
显示剩余11条评论
2个回答

2

这个测试有很大的缺陷。 UDP不保证任何传输的送达,也不保证在出现错误时会给出任何错误提示。

你的应用程序可以从Java应用程序以1GB/s的速度发送1000k数据报,但是90%的数据报从未到达目的地......目的地甚至可能没有运行。

如果您想进行任何类型的UDP测试,您需要两个应用程序,一个在每端。发送编号为1、2、3...的数据报并检查发送和接收的内容。请注意,UDP不保证任何消息的顺序。

内核以特殊方式管理本地主机网络。有专门的大型缓冲区和更高的限制,任何流量都不会通过任何网络卡或驱动程序。这与真正发送数据包非常不同。

当只在本地主机上进行测试时,测试可能看起来还算可以。但是当它经过任何物理基础设施时,一切都可能失败得很惨。

PC1 <-----> switch <-----> PC2

假设有两台计算机在同一个房间里通过交换机相连。没有丢失随机消息的情况下,在这种简单的设置上实现10k/s UDP数据包不是小事。

而这只是在同一个房间里的两台计算机。在互联网和长距离情况下,情况可能会更糟。


8
你没有抓住重点。我非常清楚UDP的工作原理,我对接收端是否能处理数据报也不感兴趣,网络连接是否能无损地传输它们也不在意;这就是为什么我明确说数据报被发送到一些未使用的本地端口。这次讨论是关于在相同的外部条件下(即相同的硬件、操作系统等),应用程序能够每秒发送多少数据报。 - Lucio Paiva

2

如果您只想使性能测试更快,可以删除setImmediate调用,并在第一个请求完成后立即执行下一个发送,即在send回调中。这样,在我的相对较慢的笔记本电脑上,其性能提高到了约100k个请求每秒。

function send(socket, message) {
  socket.send(message, SERVER_PORT, (err) => {
    send(socket, message);
  });
}

const socket = require('dgram').createSocket('udp4');
const message = new Buffer('dsdsddsdsdsjkdshfsdkjfhdskjfhdskjfhdsfkjsdhfdskjfhdskjfhsdfkjdshfkjdshfkjdsfhdskjfhdskjfhdkj');
send(socket, message);

此外,对于 CPU 的数量,在一个 child_process 中运行上述操作。 - adamrights

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接