Wednesday, May 16, 2012

To boldly Go where Node man has gone before

With all the chatter about how uber-amazing Node.js is I figured I'd do a little comparison with my favorite language du jour: Go.  Node's claim is that it's "a platform built on Chrome's JavaScript runtime for easily building fast, scalable network applications."

So, easy to build; fast; scalable.

Here's the canonical Node program for Hello, World from the Node home page.

var http = require('http');
http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World\n');
}).listen(1337, '127.0.0.1');

console.log('Server running at http://127.0.0.1:1337/');

And here's the equivalent program written in Go. It's a little longer because Go insists on explicitly importing the things you use and has a little more boilerplate (such as having a func main()).
package main

import (
 "net/http"
 "log"
 "fmt"
)

func main() {
 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "text/plain")
  fmt.Fprintf(w, "Hello, World\r\n")
 })

 log.Printf("Server running at http://127.0.0.1:1337/")
 http.ListenAndServe("127.0.0.1:1337", nil)
}

So, in terms of 'easy to build' there's no clear winner. Node is a little more compact, but the core functionality is the same: start a server and do a callback when a connection is made.

So, then there's 'fast' and 'scalable'.  To test those I used ab on Ubuntu on a MacBook Pro with 8GB of RAM.  Here are the results.

First test was ab -n 1000000 (i.e. 1,000,000 requests):

LanguageElapsed time (seconds)Requests/secondms per requestTransfer rate (KBps)Peak real memory (KB)Peak virtual memory (KB)
Go137.5427270.510.138681.614,120145,308
Node200.3414989.260.200370.3049,258638,700

The second test was ab -n 1000000 -c 100 (i.e. 1,000,000 requests with 100 simultaneously)

LanguageElapsed time (seconds)Requests/secondms per requestTransfer rate (KBps)Peak real memory (KB)Peak virtual memory (KB)
Go141.8247051.020.142661.0021,684902,884
Node177.4725634.680.177418.2050,724643,912

So, Node was always slower than Go and (almost always) used more memory.   The only time Go was 'worse' than Node was in virtual memory usage in the second test.

I'm unimpressed by Node.  Go's approach (here it is spawning a goroutine per connection) is much simpler from a programming perspective and more performant.  The code handling the connection doesn't have to be concerned about blocking/non-blocking calls or whether something is asynchronous.  You just write the code to handle that particular URL.

PS I should add that I did these tests in a Ubuntu VM which was restricted to running on a single processor core.  That was done so that any advantage Go would get because it can inherently use multiple cores would be eliminated.  Bottom line is that Go is faster, and easy to write.

PS People have asked what happens with more simultaneous connections.  Here are some graphs showing the real and virtual memory use and the requests per second for Go and Node.   Go uses less real memory and serves more requests per second at 0, 100, 500 and 1,000 simultaneous requests, but Go's virtual memory grows.




If you enjoyed this blog post, you might enjoy my travel book for people interested in science and technology: The Geek Atlas. Signed copies of The Geek Atlas are available.

21 Comments:

OpenID baudehlo said...

Isn't the Go version using all CPUs whereas the Node version is using just one? Would be nice to test with a version using Cluster.

6:29 PM  
Blogger Leon said...

node sucks

6:36 PM  
Blogger Leon said...

node sucks

6:36 PM  
Blogger brad clawsie said...

well one of the strengths of Go is that you don't need things like Cluster to exploit modern hardware. Go has it baked-in.

6:37 PM  
Blogger brad clawsie said...

This comment has been removed by the author.

6:37 PM  
Blogger brad clawsie said...

This comment has been removed by the author.

6:38 PM  
Blogger Brad Fitzpatrick said...

The Go version is doing HTTP chunking and sending a Date header, etc.

The Node version is sending a Content-Type and omitted a Date header (IIRC).

I think there a few other differences too. I haven't looked into this particular microbenchmark in awhile.

I seem to recall Go doing even better once you made them do the same work.

6:51 PM  
Blogger Brad Fitzpatrick said...

The Go version is doing HTTP chunking and sending a Date header, etc.

The Node version is sending a Content-Type and omitted a Date header (IIRC).

I think there a few other differences too. I haven't looked into this particular microbenchmark in awhile.

I seem to recall Go doing even better once you made them do the same work.

6:52 PM  
Blogger Brad Fitzpatrick said...

The Go version is doing HTTP chunking and sending a Date header, etc.

The Node version is sending a Content-Type and omitted a Date header (IIRC).

I think there a few other differences too. I haven't looked into this particular microbenchmark in awhile.

I seem to recall Go doing even better once you made them do the same work.

6:52 PM  
Blogger John Graham-Cumming said...

@Brad.

When node is running:

* About to connect() to 127.0.0.1 port 1337 (#0)
* Trying 127.0.0.1... connected
> GET / HTTP/1.1
> User-Agent: curl/7.22.0 (x86_64-pc-linux-gnu) libcurl/7.22.0 OpenSSL/1.0.1 zlib/1.2.3.4 libidn/1.23 librtmp/2.3
> Host: 127.0.0.1:1337
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Connection: keep-alive
< Transfer-Encoding: chunked
<
Hello World
* Connection #0 to host 127.0.0.1 left intact
* Closing connection #0

When Go is used:

curl -v http://127.0.0.1:1337/
* About to connect() to 127.0.0.1 port 1337 (#0)
* Trying 127.0.0.1... connected
> GET / HTTP/1.1
> User-Agent: curl/7.22.0 (x86_64-pc-linux-gnu) libcurl/7.22.0 OpenSSL/1.0.1 zlib/1.2.3.4 libidn/1.23 librtmp/2.3
> Host: 127.0.0.1:1337
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Date: Wed, 16 May 2012 18:55:12 GMT
< Transfer-Encoding: chunked
<
Hello, World
* Connection #0 to host 127.0.0.1 left intact
* Closing connection #0

Node isn't sending the Date header, but it is sending Connection header.

6:56 PM  
Blogger pankaj said...

From these numbers, Node seems to perform better when under load. On the other, Go's resource consumption spikes up across all metrics.

Can you share why this is the case for Go?

8:04 PM  
Blogger pankaj said...

Go's resource consumption spikes up across all metrics in your second test of 100 concurrent requests. Node is handling this load much more gracefully.
I am new to Go...any idea why this behavior under load ?

8:06 PM  
OpenID baudehlo said...

I don't see why this left you unimpressed with Node. Personally I'm impressed it performed so closely to a compiled language. Add in the simplicity of a dynamic language with string manipulation, regular expressions, etc and it's a big win for most coders.

8:12 PM  
OpenID baudehlo said...

I don't see why this left you unimpressed with Node. Personally I'm impressed it performed so closely to a compiled language. Add in the simplicity of a dynamic language with string manipulation, regular expressions, etc and it's a big win for most coders.

8:12 PM  
Blogger Unknown said...

Don't forget you can write your Node code in CoffeeScript which has great syntax and is fantastically literate, and that you gain the advantage of all the server-side JS libraries that are popping up.

Of course 'real' (thousands of users, etc) applications should probably be written in a compiled language, but for toy, prototype, or specialty applications (I've written small web applications that only 10 people will ever use and probably not even concurrently) Node has its place.

12:02 AM  
Blogger Carmelo said...

Similarly to @pankaj

While Node.js increases its performance in the second test, Go makes things worse :-)

Would be nice to try with more connections in parallel and then creating some graphs.

You cannot perform just 2 experiments to state something boldly :-)

6:45 AM  
Blogger Holger said...

I recently watched a presentation about Haskell and during that a graph was put up which claimed that for a trivial benchmark like this, go really did outperform go, but all three Haskell libs tested outperformed go as well. I.e. using all cores is good but using all cores more efficiently is even better.

9:38 AM  
Blogger Mina Naguib said...

Along the single-core lines, here's a brief C version with error handling omitted for brevity: http://pastebin.com/90L5SFkg

Base memory: RSS 472K VSZ 17.4MB
Peak memory: RSS 984K VSZ 17.4MB

AB results:
Concurrency 1: 13003reqs/s
Concurrency 10: 28285reqs/s
Concurrency 100: 26872reqs/s
Log: http://pastebin.com/nM0Enke8

This is on an older Macbook Pro than the one you used for your Node & Go tests.

2:35 PM  
Blogger Florin said...

This comment has been removed by the author.

11:39 PM  
Blogger Florin said...

Add a couple more lines to make the node version a cluster:
https://gist.github.com/2722169

Then run the tests.

Here are my findings, which show that node isn't that bad, in fact it consistently outperformed go in all tests:

https://gist.github.com/2722254


Of course, results from a hello world test are hardly an indication of how a platform performs in the real world.

Go looks like a promising language, I would definitely like to learn it.
I'm sure it's a powerful tool, knowing that Google is behind it and I assume it performs well in production.

I've been very satisfied with node so far, not just for it's speed, but for the community and collection of open libraries out there.

I've never used it in production, though, but it's potential as a programming tool is great.

It is a handy little tool which gets a lot of things done fast.

Combined with coffee script, this is the fastest way to prototype almost any kind of (non-gui) app.

11:40 PM  
Blogger kyb said...

How is the go code configuring the handler for the server that's instantiated later? It looks like it's doing it with a global static which would immediately make me want to avoid it at all costs.

12:13 PM  

Post a Comment

Links to this post:

Create a Link

<< Home