使用 ZeroMQ 的 Node.js 性能与 Python 和 Java 相比如何?

30
我用node.js, Python和Java编写了一个简单的zeromq请求/回复测试。代码运行100K个请求的循环。平台是一台5年前的MacBook Pro,有2个内核和3G内存,运行着Snow Leopard。
与其他两个平台相比,node.js始终慢了一个数量级。
Java: real 0m18.823s user 0m2.735s sys 0m6.042s Python: real 0m18.600s user 0m2.656s sys 0m5.857s node.js: real 3m19.034s user 2m43.460s sys 0m24.668s 有趣的是,在Python和Java中,客户端和服务器进程都使用约半个CPU。node.js的客户端使用了整个CPU,服务器使用了约30%的CPU。客户端进程也有大量页面错误,这让我认为这是一个内存问题。此外,当请求数达到10K时,node.js只慢了3倍; 它在长时间运行时肯定会变慢。
下面是客户端代码(请注意,process.exit()行也不起作用,这就是为什么我除了使用time命令之外还包含一个内部计时器的原因):
var zeromq = require("zeromq");

var counter = 0;
var startTime = new Date();

var maxnum = 10000;

var socket = zeromq.createSocket('req');

socket.connect("tcp://127.0.0.1:5502");
console.log("Connected to port 5502.");

function moo()
{
    process.nextTick(function(){
        socket.send('Hello');
        if (counter < maxnum)
        {
            moo();
        }
    });
}

moo();

socket.on('message',
          function(data)
          {
              if (counter % 1000 == 0)
              {
                  console.log(data.toString('utf8'), counter);
              }

              if (counter >= maxnum)
              {
                  var endTime = new Date();
                  console.log("Time: ", startTime, endTime);
                  console.log("ms  : ", endTime - startTime);
                  process.exit(0);
              }

              //console.log("Received: " + data);
              counter += 1;

          }
);

socket.on('error', function(error) {
  console.log("Error: "+error);
});

服务器端代码:

var zeromq = require("zeromq");

var socket = zeromq.createSocket('rep');

socket.bind("tcp://127.0.0.1:5502",
            function(err)
            {
                if (err) throw err;
                console.log("Bound to port 5502.");

                socket.on('message', function(envelope, blank, data)
                          {
                              socket.send(envelope.toString('utf8') + " Blancmange!");
                          });

                socket.on('error', function(err) {
                    console.log("Error: "+err);
                });
            }
);

作比较的是 Python 客户端和服务器端的代码:

import zmq

context = zmq.Context()
socket = context.socket(zmq.REQ)
socket.connect("tcp://127.0.0.1:5502")

for counter in range(0, 100001):
    socket.send("Hello")
    message = socket.recv()

    if counter % 1000 == 0:
        print message, counter



import zmq

context = zmq.Context()
socket = context.socket(zmq.REP)

socket.bind("tcp://127.0.0.1:5502")
print "Bound to port 5502."

while True:
    message = socket.recv()
    socket.send(message + " Blancmange!")

这是Java客户端和服务器代码:

package com.moo.test;

import org.zeromq.ZMQ;
import org.zeromq.ZMQ.Context;
import org.zeromq.ZMQ.Socket;

public class TestClient
{
    public static void main (String[] args)
    {
        Context context = ZMQ.context(1);

        Socket requester = context.socket(ZMQ.REQ);
        requester.connect("tcp://127.0.0.1:5502");

        System.out.println("Connected to port 5502.");

        for (int counter = 0; counter < 100001; counter++)
        {
            if (!requester.send("Hello".getBytes(), 0))
            {
                throw new RuntimeException("Error on send.");
            }

            byte[] reply = requester.recv(0);
            if (reply == null)
            {
                throw new RuntimeException("Error on receive.");
            }

            if (counter % 1000 == 0)
            {
                String replyValue = new String(reply);
                System.out.println((new String(reply)) + " " + counter);
            }
        }

        requester.close();
        context.term();
    }
}

package com.moo.test;

import org.zeromq.ZMQ;
import org.zeromq.ZMQ.Context;
import org.zeromq.ZMQ.Socket;

public class TestServer
{
    public static void main (String[] args) {
        Context context = ZMQ.context(1);

        Socket socket  = context.socket(ZMQ.REP);
        socket.bind("tcp://127.0.0.1:5502");

        System.out.println("Bound to port 5502.");

        while (!Thread.currentThread().isInterrupted())
        {
            byte[] request = socket.recv(0);
            if (request == null)
            {
                throw new RuntimeException("Error on receive.");
            }

            if (!socket.send(" Blancmange!".getBytes(), 0))
            {
                throw new RuntimeException("Error on send.");
            }
        }

        socket.close();
        context.term();
    }
}

我想喜欢 Node,但由于代码大小、简洁性和性能方面的巨大差异,目前我很难说服自己。

那么,有人之前见过这样的行为吗?还是我在代码中做了些愚蠢的事情?


3
你能尝试从Python示例中模拟逻辑吗?(例如:只有在收到前一个消息后才发送下一个消息) - Andrey Sidorov
你必须记住,Node仍然很年轻。你不能期望像Python这样的老手那样简单而优雅,特别是在一个甚至还没有发布1.0.0版本的框架中。 - Swift
1
我想知道Node.js的zmq模块是否有什么问题?我进行了一个简单的微基准测试,结果显示在简单的数字计算方面,Node.js比所有版本的Python都要快得多,与Java的速度相当。 - nicerobot
我不知道这是否是一个公平的测试。正如你所说,你的节点进程只使用了一个核心。由于Node使用单个进程,因此它只能在线程解决方案可以自由使用您机器上所有内核的情况下使用一个内核。此页面显示如何(仅需非常少的代码行)启动N个节点进程,其中N是内核数:http://nodejs.org/api/cluster.html - Jess
1
@Jess 另外两个例子中都没有使用线程。 - Scott A
6个回答

17

您正在使用第三方的C++绑定。就我所了解的而言,v8的“js-land”和用“c++ land”编写的对v8的绑定之间的交叉使用是非常昂贵的。如果您注意到,一些流行的node的数据库绑定完全是用JS实现的(尽管部分原因肯定是因为人们不想编译东西,但也因为它具有潜在的快速性)。

如果我记得正确,当Ryan Dahl为node编写缓冲区对象时,他注意到,如果他主要使用JS来实现它们,它们实际上会快得多,而不是使用C ++。他最终在C++中编写了必须的部分,并在纯JavaScript中完成了所有其他工作。

因此,我猜测这里的性能问题的一部分与特定模块是一个C++绑定有关。

基于第三方模块来判断node的性能并不是确定其速度或质量的好方法。最好对node的本地TCP接口进行基准测试。


14
除了我对 Node 的原生接口不感兴趣外,我会同意您的观点:我想使用 zeromq。因此,一个使用我打算为这个项目使用的模块和接口的真实世界基准测试,正是执行最好的基准测试。其他任何测试都是人为的,并且没有太大的相关性。 - Scott A
1
如果有人编写了一个本地的JS ZeroMQ接口,我们可以期待什么样的性能呢?也许比Python或Java还要快? - dkamins
1
@dkamins: 可能存在这种情况,但此时您会遇到维护问题,因为我认为(没有检查过)所有zeromq接口都使用C实现。 - Scott A
自从提出这个问题以来,他们一直在努力解决它,并且已经通过此次提交将性能提高了一个数量级。 - Nepoxx

9

"你能否尝试从你的Python示例中模拟逻辑(例如只在接收到前一个消息后发送下一个消息)?" - Andrey Sidorov Jul 11 at 6:24

我认为这是其中的一部分:

var zeromq = require("zeromq");

var counter = 0;
var startTime = new Date();

var maxnum = 100000;

var socket = zeromq.createSocket('req');

socket.connect("tcp://127.0.0.1:5502");
console.log("Connected to port 5502.");

socket.send('Hello');

socket.on('message',
          function(data)
          {
              if (counter % 1000 == 0)
              {
                  console.log(data.toString('utf8'), counter);
              }

              if (counter >= maxnum)
              {
                  var endTime = new Date();
                  console.log("Time: ", startTime, endTime);
                  console.log("ms  : ", endTime - startTime);
                  socket.close(); // or the process.exit(0) won't work.
                  process.exit(0);
              }

              //console.log("Received: " + data);
              counter += 1;

          socket.send('Hello');
          }
     );

socket.on('error', function(error) {
    console.log("Error: "+error);
});

这个版本不像之前的版本一样越来越慢,可能是因为它没有向服务器发送尽可能多的请求,而只像之前的版本一样计算响应。与之前的版本相比,它比Python/Java慢1.5倍,而不是5-10倍。

对于这个目的来说,Node仍然不是一个惊人的推荐,但肯定比“糟糕”要好得多。


9
这是与Node的zeroMQ绑定相关的问题。我不知道从何时开始,但现在已经解决了,您可以获得与其他语言相同的结果。

这是一个好消息,而且可以合理地假设,由于大部分代码都在C绑定中运行,所以这三个示例应该是相对等效的。 - Scott A

4

我对node.js并不是很熟悉,但你执行它的方式会递归创建新函数,这就是为什么它会崩溃的原因。如果想与Python或Java保持一致,代码需要更像:

    if (counter < maxnum)
    {
       socket.send('Hello');
       processmessages();  // or something similar in node.js if available
    }

3
修改后的解决方案解决了两个问题,一个是之前存在的问题,另一个是真正影响性能的问题:在未处理响应之前将消息直接发送到服务器。虽然性能仍不如Python或Java,但比原来的错误尝试慢得少。 - Scott A

3
任何使用REQ/REP套接字的性能测试都会因往返和线程延迟而产生偏差。每个消息都会唤醒整个堆栈,让其从上至下完整运行一遍。这样的指标并不十分有用,因为REQ/REP场景下无法实现高性能(也不应该)。有两种更好的性能测试方法:
- 发送多个大小不同(从1字节到1K)的消息,在10秒内尽可能多地发送。这可以给你一个基本的吞吐量,告诉你堆栈的效率如何。 - 测量流式消息的端到端延迟;也就是在每个消息中插入时间戳,看接收者的偏差是什么。这可以告诉你堆栈是否有抖动,例如由于垃圾回收而导致的。

除了我的用例恰好是这样的:发出请求,获取响应。我不确定测试流媒体性能会向我展示在那种类型的情况下应该期望什么。我并没有打算展示哪个解决方案可以最快地传输消息。 - Scott A
1
阅读指南,其中包含许多高性能异步请求-响应示例。 - Pieter Hintjens
我愿意接受教育,但如果我要比较Python和Java,使用整个堆栈进行测试可能是个好主意,不是吗? - Scott A

1

你的客户端 Python 代码在循环中被阻塞了。在节点示例中,您可以异步地在“message”事件处理程序中接收事件。如果您的客户端只想从 zmq 接收数据,那么您的 Python 代码将更有效,因为它编码为专门的一招马戏团。如果您想添加其他未使用 zmq 的事件监听功能,则会发现重新编写 Python 代码变得复杂。对于节点,您只需要添加另一个事件处理程序即可。节点永远不会成为简单示例的性能怪兽。然而,随着项目变得越来越复杂,有更多的移动部件,使用节点正确地添加功能比使用您编写的原始 Python 更容易。我宁愿在硬件上投入更多的钱,提高可读性并减少开发时间/成本。


2
是的,这只是玩具级别的Python代码,但你正在得出不受狭隘示例支持的广泛结论。Node并不是能够使事情立即变得易读的神奇酱料:使用Node编写不可维护的垃圾代码是可能的,就像使用Python编写出色的可维护(同步或异步)代码一样。我已经写了大量的Javascript,但我发现Python通常是更易于维护的语言。你的情况可能会有所不同。 - Scott A

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