Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UDP send performance in Node.js

I am benchmarking a Java UDP client that continuously sends datagrams with a payload of 100 bytes as fast as it can. It was implemented using java.nio.*. Tests show that it's able to achieve a steady throughput of 220k datagrams per second. I am not testing with a server; the client just sends the datagrams to some unused port on localhost.

I decided to run the same test in Node.js to compare both technologies and it was surprisingly sad to see that Node.js performed 10 times slower than Java. Let me walk you through my code.

First, I create a UDP socket using Node.js's dgram module:

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

Then I create a function that sends a datagram using that socket:

function sendOne() {
    client.send(message, 0, message.length, SERVER_PORT, SERVER_ADDRESS, onSend);
}

The variable message is a buffer created from a string with a hundred characters when the application starts:

var message = new Buffer(/* string with 100 chars */);

The function onSend just increments a variable that holds how many datagrams were sent so far. Next I have a function that constantly calls sendOne() using setImmediate():

function sendForever() {
    sendOne();
    setImmediate(sendForever);
} 

Initially I tried to use process.nextTick(sendForever) but I found out that it always puts itself at the tip of the event queue, even before IO events, as the docs says:

It runs before any additional I/O events (including timers) fire in subsequent ticks of the event loop.

This prevents the send IO events from ever happening, as nextTick is constantly putting sendForever at the tip of the queue at every tick. The queue grows with unread IO events until it makes Node.js crash:

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

On the other hand, setImmediate fires after I/O events callbacks, so that's why I'm using it.

I also create a timer that once every 1 second prints to the console how many datagrams were sent in the last second:

setInterval(printStats, 1000);

And finally I start sending:

sendForever();

Running on the same machine as the Java tests ran, Node.js achieved a steady throughput of 21k datagrams per second, ten times slower than Java.

My first guess was to put two sendOne's for every tick to see if it would double the throughput:

function sendForever() {
    send();
    send();  // second send
    setImmediate(sendForever);
}

But it didn't change the throughput whatsoever.

I have a repository available on GitHub with the complete code:

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

Simply clone it to your machine, cd into the folder and run:

node client

I want to open a discussion about how this test could be improved in Node.js and if there's some way we can increase Node.js's throughput. Any ideas?

P.S.: for those interested, here is the Java part.

like image 734
Lucio Paiva Avatar asked Jun 30 '15 16:06

Lucio Paiva


2 Answers

That test is overfly flawed. UDP doesn't guarantee the delivery of anything and doesn't guarantee that it would give any error in case of error.

Your application could send 1000k datagram/s at 1GB/s from the Java application, yet 90% of datagrams never reached the destination... the destination might not even be running.

If you want to do any sort of UDP testing, you need two applications, one on each end. Send numbered datagrams 1, 2, 3... and check what's sent and what's received. Note that UDP doesn't guarantee any ordering of messages.

Kernels manage the localhost network in special ways. There are huge buffers dedicated to it and higher limits, no traffic ever goes through any network cards or drivers. It's very different from sending packets for real.

Tests might seem somewhat okay when they're only done on localhost. Expect everything to fail miserably when it's going through any physical infrastructure for real.

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

Let's say, there are two computers in the same room linked by a switch. It would be no small feat to achieve 10k/s UDP datagrams on that simple setup, without loosing messages randomly.

And that's just two computers in the same room. It can be a lot worse on the Internet and long distance.

like image 84
user5994461 Avatar answered Oct 20 '22 23:10

user5994461


If all you want is to make the performance test go faster, removing the setImmediate call and executing the next send once the first has completed i.e. in the send callback increased its performance to ~100k requests per second on my slowish laptop.

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);
like image 25
icirellik Avatar answered Oct 20 '22 22:10

icirellik