Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Benchmarking With Go

DZone's Guide to

Benchmarking With Go

Ahende Rahien scratches his Go itch with some benchmarking using the GoBench library.

· Performance Zone
Free Resource

Every now and then, you really need to get out of your comfort zone, so I decided that wanted to play a bit with Go which I hadn’t done yet. Oh, I have read Go code, quite a lot of it, but it isn’t the same as writing and actually using it.

We are doing a lot of performance work recently, and while some of that is based entirely on micro benchmarks and focused on low-level details such as the number of retired instructions at the CPU level, we also need to see the impact of larger changes. We have been using WRK to do that, but it is hard to get it running on Windows and we had to do some nasty things in Lua scripting to get what we wanted.

I decided that I’ll take the GoBench tool and transform it into a dedicated tool for benchmarking RavenDB.

Here is what we want to test:

  1. Read document by id
  2. Query documents by index
  3. Query map/reduce results
  4. Write new documents (single document)
  5. Write new documents (multiple documents in tx)
  6. Update existing documents

This is intended to be a RavenDB tool, so we won’t be trying to do anything generic - we’ll be writing specialized code.

In terms of the interface, GoBench is using command line parameters to control itself, but I think that I’ll pass a configuration file instead . I started to write about the format of the configuration file when I realized that I’m being absolutely stupid.

I don’t need to do that, I already have a good way to specify what I want to do in the code. It is called the code. The actual code to run the HTTP requests is here. But this is basically just getting a configuration object and using it to generate requests.

Of far more interest to me is the code that actually generates the requests themselves. Here is the piece that tests read requests:

func readRandomDocs(opts BenchOpts) {

    if opts.clients > opts.reads {
        opts.clients = opts.reads
    }
    done.Add(opts.clients) // sync.WaitGroup

    fmt.Printf("%s random doc reads from %s with %s clients\n",
    humanize.Comma(int64(opts.reads)),
    baseUrl,
    humanize.Comma(int64(opts.clients)))

    for i := 0; i < opts.clients; i++ {
        configuration := &Configuration{
            nextUrl: func() string {
                var id int64
                id = rand.Int63n(3594302)
                return baseUrl + "/docs?id=disks/" + strconv.FormatInt(id, 10)
            },
            method:             "GET",
            expectedStatucCode: 200,
            requests:           int64(opts.reads / opts.clients),
        }
        results = append(results, &Result{})
        go client(configuration, results[len(results)-1], &done)
    }
}

We just spin off a number of Go routines, and each does a portion of the work. This gives us concurrent clients and the ability to hammer the server. And the amount of code that we need to write for this is minimal.

To compare, here is the code for writing to the databases:

func writeNewDocs(opts BenchOpts) {

    if opts.clients > opts.reads {
        opts.clients = opts.reads
    }
    done.Add(opts.clients) // sync.WaitGroup

    data, err := ioutil.ReadFile("data.json")
    if err != nil {
        color.Set(color.FgMagenta, color.Bold)

        fmt.Println("Could not read data file", err)
        color.Unset()

        os.Exit(0)
    }

    fmt.Printf("%s new doc writes from %s with %s clients\n",
    humanize.Comma(int64(opts.reads)),
    baseUrl,
    humanize.Comma(int64(opts.clients)))

    for i := 0; i < opts.clients; i++ {
        configuration := &Configuration{
            nextUrl: func() string {
                return baseUrl + "/docs?id=disks/"
            },
            method:             "PUT",
            expectedStatucCode: 201,
            postData:           data,
            requests:           int64(opts.reads / opts.clients),
        }
        results = append(results, &Result{})
        go client(configuration, results[len(results)-1], &done)
    }
}

And then we are left with just deciding on a particular benchmark configuration. For example, here is us running simple load test for both reads and writes.

image

I think that this matches the low overhead for configuration, readability and high degree of flexibility quite well.

Topics:
golang ,go

Published at DZone with permission of Oren Eini, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}