Showing posts with label queue. Show all posts
Showing posts with label queue. Show all posts

Sunday, October 26, 2014

Benchmarking NSQ

Benchmarking NSQ
In this article, I’ll demonstrate:
  1. How to properly write tests that needs external services
  2. How to write a parallel benchmark
  3. NSQ performances

Properly test services

Self contained

The idea is to write self-contained tests that do not leak one into an other. It is particularly useful when testing databases, http server or in our case, nsqd.
For http tests, we use httptest.NewServer() which can be closed in a defer. That way, you have a brand new server each time. For databases, the best would be to mock, but if you can’t, then create a new connection object, empty or create a new database, then defer destruction or empty.
In our case, for nsqd, we will simply spawn a new daemon, disable all logging, disable the dump of the queue on disc and destroy that daemon when the test is finished.
We could have done something much more simple and spawn an nsqd first then run our tests against that instance, but then the tests would not be consistent as what has been done by one test can affect the result of an other test.

Other note

A pattern which I like is to create helpers in test that instead of returning an error, take the testing object.
Go provides the testing.TB interface which allow to receive either *testing.B or *testing.T. This is nice when writing helper for both tests and benchmarks.
Instead of having:
func newObject(addr string) (*object, error) {
    if false {
        return nil, err
    }
    return &object{}, nil
}

func TestObject(t *testing.T) {
    obj, err := newObject("localhost")
    if err != nil {
        t.Fatal(err)
    }
    _ = obj
}
You would have:
func newObject(t testing.TB, addr string) (*object, error) {
    if false {
        t.Fatal(err)
    }
    return &object{}, nil
}

func TestObject(t *testing.T) {
    obj := newObject(t, "localhost")
    _ = obj
}

Parallel Benchmark

It is very easy in Go to write parallel benchmarks:
From the Go documentation:
package main

import (
    "bytes"
    "testing"
    "text/template"
)

func BenchmarkTemplate(b *testing.B) {
    // Parallel benchmark for text/template.Template.Execute on a single object.
    templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
    // RunParallel will create GOMAXPROCS goroutines
    // and distribute work among them.
    b.RunParallel(func(pb *testing.PB) {
        // Each goroutine has its own bytes.Buffer.
        var buf bytes.Buffer
        for pb.Next() {
            // The loop body is executed b.N times total across all goroutines.
            buf.Reset()
            templ.Execute(&buf, "World")
        }
    })
}
So, instead of the “regular” benchmark where you do something like
func BenchmarkSomething(b *testing.B) {
    for i := 0; i < b.N; i++ {
        doSomething()
    }
}
you simply do:
func BenchmarkSomething(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            doSomething()
        }
    }
}
You will note that we do note use b.N anymore.
Good to know as well: b.SetParallel(int) will specify the amount of goroutines allowed.

NSQ performances

Write benchmark for nsqd

Nothing is better than an example: Playground
package main

import (
    "io/ioutil"
    "log"
    "os"
    "runtime"
    "testing"
)

import (
    nsq "github.com/bitly/go-nsq"
    "github.com/bitly/nsq/nsqd"
)

// nopLogger simply discard any logs it receives
type nopLogger struct{}

func (*nopLogger) Output(int, string) error {
    return nil
}

// newDaemon creates a quiet, stripped down daemon and start it
func newDaemon() *nsqd.NSQD {
    opts := nsqd.NewNSQDOptions()

    // Disable http/https
    opts.HTTPAddress = ""
    opts.HTTPSAddress = ""
    // Disable logging
    opts.Logger = &nopLogger{}
    // Do not create on disc queue
    opts.DataPath = "/dev/null"

    nsqd := nsqd.NewNSQD(opts)
    nsqd.Main()
    return nsqd
}

// Wrap nsq.Consumer so we have control over Stop behavior
type consumer struct{ *nsq.Consumer }

func (c *consumer) Stop() {
    c.Consumer.Stop()
    <-c.Consumer.StopChan
}

// newConsumer creates a quiet connected Consumer
func newConsumer(t testing.TB, tcpAddr, topicName, channelName string, hdlr nsq.HandlerFunc) *consumer {
    // Create the configuration object and set the maxInFlight
    cfg := nsq.NewConfig()
    cfg.MaxInFlight = 8

    // Create the consumer with the given topic and chanel names
    r, err := nsq.NewConsumer(topicName, channelName, cfg)
    if err != nil {
        t.Fatal(err)
    }

    // Disable logging
    r.SetLogger(&nopLogger{}, 0)

    // Set the handler
    r.AddHandler(hdlr)

    // Connect to the NSQ daemon
    if err := r.ConnectToNSQD(tcpAddr); err != nil {
        t.Fatal(err)
    }

    return &consumer{Consumer: r}
}

// newProducer creates a quiet connected Producer
func newProducer(t testing.TB, tcpAddr string) *nsq.Producer {
    // Create the configuration object and set the maxInFlight
    cfg := nsq.NewConfig()
    cfg.MaxInFlight = 8

    // Create the producer
    p, err := nsq.NewProducer(tcpAddr, cfg)
    if err != nil {
        t.Fatal(err)
    }

    // Disable logging
    p.SetLogger(&nopLogger{}, 0)

    return p
}

func BenchmarkPubSub(b *testing.B) {
    // Disable general logging
    log.SetOutput(ioutil.Discard)
    defer func() { log.SetOutput(os.Stderr) }()

    // Start NSQD and make sure to shut it down when leaving
    nsqd := newDaemon()
    defer nsqd.Exit()

    // Create the consumer and send every message to the chan
    msgs := make(chan []byte)
    hdlr := func(msg *nsq.Message) error { msgs <- msg.Body; return nil }
    consumer := newConsumer(b, "localhost:4150", "mytopic", "mychan1", hdlr)
    defer consumer.Stop()

    // Create producer
    producer := newProducer(b, "localhost:4150")
    defer producer.Stop()

    // reset Go's timer.
    b.ResetTimer()

    // Run in Parallel
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            // Send "hello world" to NSQ and wait for it to arrive.
            if err := producer.Publish("mytopic", []byte("hello world")); err != nil {
                b.Fatal(err)
            }
            if msg, ok := <-msgs; !ok {
                b.Fatal("Message chan closed.")
            } else if expect, got := "hello world", string(msg); expect != got {
                b.Fatalf("Unexpected message. Expected: %s, Got: %s", expect, got)
            }
        }
    })
}

Results

I am testing natively (darwin) on a MacbookPro i7 8 cores 2.3Ghz, 16GB Ram:
$> GOMAXPROCS=8 go test -v  -bench . .
PASS
BenchmarkPubSub-8    50000           37167 ns/op
ok      github.com/bitly/nsq/apps/test/bench    4.133s
So this is roughly 27K messages per seconds or 0.0004ms per send/receive operation.
Now let’s see the impact of GOMAXPROCS:
$> for i in {1..8}; do GOMAXPROCS=$i go test -v -bench . .; done
BenchmarkPubSub             5         271720061 ns/op
BenchmarkPubSub-2          10         135813612 ns/op
BenchmarkPubSub-3          20          92693480 ns/op
BenchmarkPubSub-4          20          67964795 ns/op
BenchmarkPubSub-5          50          52089680 ns/op
BenchmarkPubSub-6          50          47146900 ns/op
BenchmarkPubSub-7          50          32083808 ns/op
BenchmarkPubSub-8       50000             37167 ns/op

Conclusion

It is the first queueing service I am trying so I don’t really know if it is good or bad, it would be interesting to compare with 0mq, redis, rabbitmq to see the difference.
In any case, it is a nice example for self-contained test leveraging the parallel capabilities of Go.

Sunday, October 19, 2014

Play with NSQ in Go

In my previous article, we saw what was NSQ, now, let’s try to play with it a bit more.

Consumer

As we saw previously, we can directly publish on NSQD via http using curl, so let’s focus for now on the consumer part.
NSQ provides a sample app that does the bridge between http and consumer: https://github.com/bitly/nsq/blob/master/apps/nsq_pubsub/nsq_pubsub.go
As the goal is to use it in an application, we don’t need that HTTP overhead. Here is what is looks like stripped down: playground
package main

import (
        "fmt"

        nsq "github.com/bitly/go-nsq"
)

func nsqSubscribe(tcpAddr, topicName, channelName string, hdlr nsq.HandlerFunc) error {
        fmt.Printf("Subscribe on %s/%s\n", topicName, channelName)

        // Create the configuration object and set the maxInFlight
        cfg := nsq.NewConfig()
        cfg.MaxInFlight = 8

        // Create the consumer with the given topic and chanel names
        r, err := nsq.NewConsumer(topicName, channelName, cfg)
        if err != nil {
                return err
        }

        // Set the handler
        r.AddHandler(hdlr)

        // Connect to the NSQ daemon
        if err := r.ConnectToNSQD(tcpAddr); err != nil {
                return err
        }

        // Wait for the consumer to stop.
        <-r.StopChan
        return nil
}

func main() {
        nsqSubscribe("localhost:4150", "mytopic", "mychan1", func(msg *nsq.Message) error {
                fmt.Printf("%s\n", msg.Body)
                return nil
        })
}
It is very similar to the HTTP package, in a sense that you define your handler and give it to the NSQ consumer. You can try this example by starting nsqd locally, run this code and publish messages via curl:
curl -d 'hello world' 'localhost:4151/pub?topic=mytopic'

Producer

Now let’s see the Producer. It is nice for testing to be able to use curl, however in an actual application, the HTTP overhead can be a burden.
The Go implementation is even simpler than the consumer: playground
package main

import nsq "github.com/bitly/go-nsq"

func nsqPublish(tcpAddr, topicName string, message []byte) error {
        // Create the configuration object and set the maxInFlight
        cfg := nsq.NewConfig()
        cfg.MaxInFlight = 8

        // Create the producer
        p, err := nsq.NewProducer(tcpAddr, cfg)
        if err != nil {
                return err
        }
        return p.Publish(topicName, message)
}

func main() {
        nsqPublish("localhost:4150", "mytopic", []byte(`hello world`))
}
You can now run the subscriber and N publisher to make sure it works. Note that nsqd need to be up and running and they both connect to it.

Conclusion

NSQ is very easy to use and even more easy to integrate. In a next article, we’ll see how it performs with Benchmark.

Sunday, October 12, 2014

First look at NSQ

I finally took the time to take a look at NSQ and it is pretty neat!

What is NSQ?

From their website (http://nsq.io):
NSQ is a realtime distributed messaging platform designed to operate at scale, handling billions of messages per day.
Basically, it is a Queue the scales. It has been created by bitly and they use it in production, so it has proven its performances. Since, a lot of other companies adopted it.34
It provides an HTTP api and easy way to create producer/consumers.
The code is available on github: https://github.com/bitly/nsq. They provide a set of helpers in the “apps” directory.

Components

NSQ is composed of several compenent: nsq, nsqd, nsqlookupd, nsqadmin, but today, we’ll talk only about nsq (client) and nsqd (queue daemon)

Concept of the Queue

The idea of a Queue is to “queue” (who would have guessed? ;) messages. So we will have a procuder that sends messages to the queue, and in the other side, we will have a consumer that pops messages. A nice feature of most queuing systems is what we call pub/sub. I.e. multiple consumers can “subscribe” to the queue and a message published by a producer will be received by 1 or N consumers

Topic and Channels

NSQ allows two mode of communication: broadcast and balancing.
  • broadcast: A message published will be received by all subscribers
  • balancing: A message published will be received by (at least) one subscriber.
In order to control this, NSQ provides two concepts: topic and channel.
When a consumer is created, it will subscribe to a topic/channel pair. However, when a producer is created, it will only publish to a topic.
A message published on a topic will be copied (broadcast) to each channels, then distributed within this channel.
Let’s see an example:

Balancing

  • Consumer1 subscribe to mytopic / mychannel
  • Consumer2 subscribe to mytopic / mychannel
  • Consumer3 subscribe to mytopic / mychannel
  • Producer1 published to mytopic
In this scenario, each message published will be received only by a single consumer. In a perfect world, if Producer1 publishes 3 messages, each consumer will receive one. (In reality, it is random, but you get the idea).

Broadcast

  • Consumer1 subscribe to mytopic / mychannel1
  • Consumer2 subscribe to mytopic / mychannel2
  • Consumer3 subscribe to mytopic / mychannel3
  • Producer1 published to mytopic
In this scenario, each message published will be received by all the consumers, because they all subscribed to a different channel. If Producer1 publishes 3 messages, each consumer will receive all 3 messages.

Broadcast + Balancing

Example from nsq.io:
  • Consumer1 subscribe to clicks / metrics
  • Consumer2 subscribe to clicks / metrics
  • Consumer3 subscribe to clicks / metrics
  • Consumer4 subscribe to clicks / spam_analytics
  • Consumer5 subscribe to clicks / archives
  • Producer1 published to clicks
In this scenario, if Producer1 publishes 3 messages:
  • Consumer1 receives 1 message (because balanced on the channel)
  • Consumer2 receives 1 message
  • Consumer3 receives 1 message
  • Consumer4 receives 3 message (each message is copied to all the different channels)
  • Consumer5 receives 3 message
Once you get the concept, the schema on their website makes a lot of sense:
nsq design

Usage

Now let’s try by ourselve!

NSQD

NSQD is the queue “deamon” (not really a daemon as any go apps), it handles all the queuing logic and can be started with no particular configuration.
It is go gettable and can be installed with go get github.com/bitly/nsq/apps/nsqd
Now, simply start it:
$> nsqd
It will listen on port 4150 (tcp) and 4151 (http). We will see in a bit what those are for.

Test application

NSQD provides an HTTP API, however, we can only publish on it. In order to subscribe, we need to use the tcp API. Thankfully, they provide an app that does just that an expose subscribe over http.
You can install this adaptor by doing go get github.com/bitly/nsq/apps/nsq_pubsub
Now, simply start it by providing the address of the daemon:
$> nsq_pubsub --nsqd-tcp-address localhost:4150
It will listen on 8080 (http)

Demo time

Now that we have nsqd and nsq_pubsub up and running, we can simply try the queue with curl:
# Consumer on the adaptor
curl 'http://localhost:8080/sub?topic=mytopic&channel=mychan'
# Producer on nsqd
curl -d 'message' 'http://localhost:4151/pub?topic=mytopic
Let’s open multiple terminal and reproduce the bitly example (tmux is your friend ;):
One consumer per terminal as it is blocking:
# Consumer1
curl 'http://localhost:8080/sub?topic=clicks&channel=metric'
# Consumer2
curl 'http://localhost:8080/sub?topic=clicks&channel=metric'
# Consumer3
curl 'http://localhost:8080/sub?topic=clicks&channel=metric'
# Consumer4
curl 'http://localhost:8080/sub?topic=clicks&channel=spam_analytics'
# Consumer5
curl 'http://localhost:8080/sub?topic=clicks&channel=archive'
# Producer
for i in {1..100}; do curl -d "message $i" 'http://localhost:4151/pub?topic=clicks'; done

Conclusion

NSQ seems like a good alternative to legacy systems like RabbitMQ, in a next post, I’ll try to do some benchmarks to assess performances and reliability.
I really like how easy it is to use and get started, the Broadcast/Distributed modes allow for nice and powerful scenarios.