bender: github.com/pinterest/bender Index | Files | Directories

package bender

import "github.com/pinterest/bender"

Package bender makes it easy to build load testing applications for services using protocols like HTTP, Thrift, Protocol Buffers and many others.

Bender provides two different approaches to load testing. The first, LoadTestThroughput, gives the tester control over the throughput (QPS), but not over the concurrency (number of goroutines). The second, LoadTestConcurrency, gives the tester control over the concurrency, but not over the throughput.

LoadTestThroughput simulates the load caused by concurrent clients sending requests to a service. It can be used to simulate a target throughput (QPS) and to measure the request latency and error rate at that throughput. The load tester will keep spawning goroutines to send requests, even if the service is sending errors or hanging, making this a good way to test the actual behavior of the service under heavy load. This is the same approach used by Twitter's Iago library, and is nearly always the right place to start when load testing services exposed (directly or indirectly) to the Internet.

LoadTestConcurrency simulates a fixed number of clients, each of which sends a request, waits for a response and then repeats. The downside to this approach is that increased latency from the service results in decreased throughput from the load tester, as the simulated clients are all waiting for responses. That makes this a poor way to test services, as real-world traffic doesn't behave this way. The best use for this function is to test services that need to handle a lot of concurrent connections, and for which you need to simulate many connections to test resource limits, latency and other metrics. This approach is used by load testers like the Grinder and JMeter, and has been critiqued well by Gil Tene in his talk "How Not To Measure Latency".

The next two sections provide more detail on the implementations of LoadTestThroughput and LoadTestConcurrency. The following sections provide descriptions for the common arguments to the load testing functions, and how they work, including the interval generators, request generators, request executors and event recorders.

LoadTestThroughput

The LoadTestThroughput function takes four arguments. The first is a function that generates nanosecond intervals which are used as request arrival times. The second is a channel of requests. The third is a function that knows how to send a request and validate the response. The inner loop of LoadTestThroughput looks like this:

for {
    interval := intervals(time.Now().UnixNanos())
    time.Sleep(time.Duration(interval)
    request := <-requests
    go func() {
      err := requestExec(time.Now().UnixNano(), request)
    }()
}

The fourth argument to LoadTestThroughput is a channel which is used to output events. There are events for the start and end of the load test, the sending of each request and the receiving of each response and the wait time between sending requests. The wait message includes an "overage" time which is useful for monitoring the health of the load test program and underlying OS and host. The overage time measures the difference between the expected wait time (the interval time) and the actual wait time. On a heavily loaded host, or when there are long GC pauses, that difference can be large. Bender attempts to compensate for the overage by reducing the subsequent wait times, but under heavy load, the overage will continue to increase until it cannot be compensated for. At that point the wait events will report a monotonically increasing overage which means the load test isn't keeping up with the desired throughput.

A load test ends when the request channel is closed and all remaining requests in the channel have been executed.

LoadTestConcurrency

The LoadTestConcurrency function takes four arguments. The first is a semaphore that controls the maximum number of concurrently executing requests, and makes it possible to dynamically control that number over the lifetime of the load test. The second, third and fourth arguments are identical to those for LoadTestThroughput. The inner loop of LoadTestConcurrency does something like this:

for {
    workerSem.Wait(1)
    request := <-requests
    go func() {
        err := requestExec(time.Now().UnixNano(), request)
        workerSem.Signal(1)
    }
}

Reducing the semaphore count will reduce the number of running connections as existing connections complete, so there can be some lag between calling workerSem.Wait(n) and the number of running connections actually decreasing by n. The worker semaphore does not protect you from reducing the number of workers below zero, which will cause undefined behavior from the load tester.

As with LoadTestThroughput, the load test ends when the request channel is closed and all remaining requests have been executed.

Interval Generators

An IntervalGenerator is a function that takes the current Unix epoch time (in nanoseconds) and returns a non-negative time (also in nanoseconds) until the next request should be sent. Bender provides functions to create interval generators for uniform and exponential distributions, each of which takes the target throughput (requests per second) and returns an IntervalGenerator. Neither of the included generators makes use of the function argument, but it is there for cases in which the simulated intervals are time dependent (you want to simulate the daily traffice variation of a web site, for example).

Request Channels

The request channel decouples creation of requests from execution of requests and allows them to run concurrently. A typical approach to creating a request channel is code like this:

 c := make(chan *Request)
 go func() {
     for {
		 // create service request r with request ID rid
		 c <- &Request{rid, r}
     }
     close(c)
 }()

Requests can be generated randomly, read from files (like access logs) or generated any other way you like. The important part is that the request generation be done in a separate goroutine that communicates with the load tester via a channel. In addition, the channel must be closed to indicate that the load test is done.

The requests channel should almost certainly be buffered, unless you can generate requests much faster than they are sent (and not just on average). The easiest way to miss your target throughput with LoadTestThroughput is to be blocked waiting for requests to be generated, particularly when testing a large throughput.

Request Executors

A request executor is a function that takes the current Unix Epoch time (in nanoseconds) and a *Request, sends the request to the service, waits for the response, optionally validates it and returns an error or nil. This function is timed by the load tester, so it should do as little else as possible, and everything it does will be added to the reported service latency. Here, for example, is a very simple request executor for HTTP requests:

func HttpRequestExecutor(_ int64, request *Request) error {
    url := request.Request.(string)
    _, err := http.Get(url)
    return err
}

The http package in Bender provides a function that generates executors that make use of the http packages Transport and Client classes and provide an easy way to validate the body of the http request.

RequestExecutors are called concurrently from multiple goroutines, and must be concurrency-safe.

Event Messages

The LoadTestThroughput and LoadTestConcurrency functions both take a channel of events (represented as interface{}) as a parameter. This channel is used to output events as they happen during the load test, including the following events:

StartEvent: sent once at the start of the load test.

EndEvent: sent once at the end of the load test, no more events are sent after this.

WaitEvent: sent only for LoadTestThroughput, see below for details.

StartRequestEvent: sent before a request is sent to the service, includes the request and the event time. Note that the event time is not the same as the start time for the request for stupid performance reasons. If you need to know the actual start time, see the EndRequestEvent.

EndRequestEvent: sent after a request has finished, includes the response, the actual start and end times for the request and any error returned by the RequestExecutor.

The WaitEvent includes the time until the next request is sent (in nanoseconds) and an "overage" time. When the inner loop sleeps, it subtracts the total time slept from the time it intended to sleep, and adds that to the overage. The overage, therefore, is a good proxy for how overloaded the load testing host is. If it grows over time, that means the load test is falling behind, and can't start enough goroutines to run all the requests it needs to. In that case you will need a more powerful load testing host, or need to distribute the load test across more hosts.

The event channel doesn't need to be buffered, but it may help if you find that Bender isn't sending as much throughput as you expect. In general, this depends a lot on how quickly you are consuming events from the channel, and how quickly the load tester is running. It is a good practice to proactively buffer this channel.

Index

Package Files

bender.go doc.go intervals.go recorders.go

func LoadTestConcurrency Uses

func LoadTestConcurrency(workers *WorkerSemaphore, requests chan interface{}, requestExec RequestExecutor, recorder chan interface{})

LoadTestConcurrency starts a load test in which the caller controls the number of goroutines that are sending requests. See the package documentation for details on the arguments to this function.

func LoadTestThroughput Uses

func LoadTestThroughput(intervals IntervalGenerator, requests chan interface{}, requestExec RequestExecutor, recorder chan interface{})

LoadTestThroughput starts a load test in which the caller controls the interval between requests being sent. See the package documentation for details on the arguments to this function.

func Record Uses

func Record(c chan interface{}, recorders ...Recorder)

Record records messages from a channel using the given recorders.

type EndEvent Uses

type EndEvent struct {
    // The Unix epoch times in nanoseconds at which the load test started and ended.
    Start, End int64
}

EndEvent is sent once at the end of the load test, after which no more events are sent.

type EndRequestEvent Uses

type EndRequestEvent struct {
    // The Unix epoch times (in nanoseconds) at which the request was started and finished
    Start, End int64
    // The response data returned by the request executor
    Response interface{}
    // An error or nil if there was no error
    Err error
}

EndRequestEvent is sent after a request has completed.

type IntervalGenerator Uses

type IntervalGenerator func(int64) int64

An IntervalGenerator is a function that takes the current Unix epoch time (in nanoseconds) and returns a non-negative time (also in nanoseconds) until the next request should be sent. Bender provides functions to create interval generators for uniform and exponential distributions, each of which takes the target throughput (requests per second) and returns an IntervalGenerator. Neither of the included generators makes use of the function argument, but it is there for cases in which the simulated intervals are time dependent (you want to simulate the daily traffice variation of a web site, for example).

func ExponentialIntervalGenerator Uses

func ExponentialIntervalGenerator(rate float64) IntervalGenerator

ExponentialIntervalGenerator creates an IntervalGenerator that outputs exponentially distributed intervals. The resulting arrivals constitute a Poisson process. The rate parameter is the average queries per second for the generator, and corresponds to the reciprocal of the lambda parameter to an exponential distribution. In English, if you want to generate 30 QPS on average, pass 30 as the value of rate.

func UniformIntervalGenerator Uses

func UniformIntervalGenerator(rate float64) IntervalGenerator

UniformIntervalGenerator creates and IntervalGenerator that outputs 1/rate every time it is called. Boring, right?

type Recorder Uses

type Recorder func(interface{})

A Recorder records a message.

func NewHistogramRecorder Uses

func NewHistogramRecorder(h *hist.Histogram) Recorder

NewHistogramRecorder creates a new hist.Histogram-based recorder.

func NewLoggingRecorder Uses

func NewLoggingRecorder(l *log.Logger) Recorder

NewLoggingRecorder creates a new log.Logger-based recorder.

type RequestExecutor Uses

type RequestExecutor func(int64, interface{}) (interface{}, error)

RequestExecutor is a function that takes the current Unix Epoch time (in nanoseconds) and a *Request, sends the request to the service, waits for the response, optionally validates it and returns an error or nil. This function is timed by the load tester, so it should do as little else as possible, and everything it does will be added to the reported service latency.

type StartEvent Uses

type StartEvent struct {
    // The Unix epoch time in nanoseconds at which the load test started.
    Start int64
}

StartEvent is sent once at the start of the load test.

type StartRequestEvent Uses

type StartRequestEvent struct {
    // The Unix epoch time (in nanoseconds) at which this event was created, which will be earlier
    // than the sending of the associated request (for performance reasons)
    Time int64
    // The request that will be sent, nothing good can come from modifying it
    Request interface{}
}

StartRequestEvent is sent before a request is executed. The sending of this event happens before the timing of the request starts, to avoid potential issues, so it contains the timestamp of the event send, and not the timestamp of the request start.

type WaitEvent Uses

type WaitEvent struct {
    // The next wait time (in nanoseconds) and the accumulated overage time (the difference between
    // the actual wait time and the intended wait time).
    Wait, Overage int64
}

WaitEvent is sent once for each request before sleeping for the given interval.

type WorkerSemaphore Uses

type WorkerSemaphore struct {
    // contains filtered or unexported fields
}

WorkerSemaphore controls the number of "workers" that can be running as part of a load test using LoadTestConcurrency.

func NewWorkerSemaphore Uses

func NewWorkerSemaphore() *WorkerSemaphore

NewWorkerSemaphore creates an empty WorkerSemaphore (no workers).

func (WorkerSemaphore) Signal Uses

func (s WorkerSemaphore) Signal(n int)

Signal adds a worker to the pool of workers that are currently sending requests. If no requests are outstanding, this will block until a request is ready to send.

func (WorkerSemaphore) Wait Uses

func (s WorkerSemaphore) Wait(n int) bool

Wait removes a worker from the pool. If all workers are busy, then this will wait until the next worker is finished, and remove it.

Directories

PathSynopsis
dhcpv4
dhcpv6
dns
hist
http
tftp
thrift

Package bender imports 6 packages (graph) and is imported by 22 packages. Updated 2019-03-14. Refresh now. Tools for package owners.