From 45f56eda6d266011ea3ed8a18c0383b627657168 Mon Sep 17 00:00:00 2001 From: username-from-outer-space Date: Mon, 20 Jun 2016 12:38:02 +0500 Subject: [PATCH] This is pretty much v0.2. Now there are two modes in which you can run the tool. First is the timed one, which is default(10s test with 200 conns). Second is the good old "How many times do I shoot that thing?". Also, I thought it would be useful to be able to supply request's method and body. Oh, and the code was reorganized. That's it. For more details just read the diff. --- LICENSE | 2 +- README.md | 50 ++++++++------ bombardier.go | 157 +++++++++++++++++++----------------------- completion_barrier.go | 77 +++++++++++++++++++++ config.go | 99 ++++++++++++++++++++++++++ flags.go | 47 +++++++++++++ 6 files changed, 323 insertions(+), 109 deletions(-) create mode 100644 completion_barrier.go create mode 100644 config.go create mode 100644 flags.go diff --git a/LICENSE b/LICENSE index aa09e1d..1123f8e 100644 --- a/LICENSE +++ b/LICENSE @@ -5,7 +5,7 @@ Copyright (c) 2016 Максим Федосеев Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +to use, copy, modify, merge, publish, distribute, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index 64a2e2d..8cba19b 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ bombardier is a HTTP(S) benchmarking tool. It's written in Go programming language and uses excellent [fasthttp](https://github.com/valyala/fasthttp) instead of Go's default http library, because of it's lightning fast performance. ##Installation -You are encourages to grab the latest version of the tool in the [releases](https://github.com/bugsenberg/bombardier/releases) section and test the tool by yourself. +You cat grab the latest version in the [releases](https://github.com/codesenberg/bombardier/releases) section. #### If you can't find your OS/ARCH combo(aka build from the source) -This one is actually pretty straightforward. Just run `go get github.com/bugsenberg/bombardier`(but you may need to get the deps first). +This one is actually pretty straightforward. Just run `go get github.com/codesenberg/bombardier`. ##Usage Run it like: @@ -15,13 +15,19 @@ bombardier Also, you can supply these options: ``` -H value - HTTP headers to use (default []) - -c int + HTTP headers to use + -c uint Maximum number of concurrent connections (default 200) + -n value + Number of requests + -d value + Duration of test + -data string + Request body -latencies Print latency statistics - -n int - Number of requests (default 10000) + -m string + Request method (default "GET") -timeout duration Socket/request timeout (default 2s) ``` @@ -29,30 +35,30 @@ You should see something like this if you done everything correctly: ``` > bombardier -c 200 -n 10000000 http://localhost:8080 Bombarding http://localhost:8080 with 10000000 requests using 200 connections -10000000 / 10000000 [============================================] 100.00 % 55s +10000000 / 10000000 [============================================] 100.00 % 47s Done! Statistics Avg Stdev Max - Reqs/sec 181631.00 13494.01 197924 - Latency 1.10ms 319.69us 82.51ms + Reqs/sec 209655.00 9914.22 216847 + Latency 0.95ms 292.09us 37.00ms HTTP codes: 1xx - 0, 2xx - 10000000, 3xx - 0, 4xx - 0, 5xx - 0 errored - 0 - Throughput: 201.11MB/s + Throughput: 232.12MB/s ``` Or, on a realworld server(with latency distribution): ``` -> bombardier -c 200 -n 10000 --latencies http://google.com -Bombarding http://google.com with 10000 requests using 200 connections -10000 / 10000 [===================================================] 100.00 % 2s +> bombardier -c 200 -d 10s --latencies http://google.com +Bombarding http://google.com for 10s using 200 connections +[==========================================================================]10s Done! Statistics Avg Stdev Max - Reqs/sec 4165.00 1382.95 4939 - Latency 43.14ms 26.01ms 394.05ms + Reqs/sec 5384.00 789.97 5699 + Latency 36.96ms 19.58ms 1.44s Latency Distribution - 50% 38.50ms - 75% 44.01ms - 90% 47.01ms - 99% 113.01ms + 50% 34.00ms + 75% 41.00ms + 90% 42.00ms + 99% 45.00ms HTTP codes: - 1xx - 0, 2xx - 0, 3xx - 9994, 4xx - 0, 5xx - 0 - errored - 6 - Throughput: 1.95MB/s + 1xx - 0, 2xx - 0, 3xx - 54083, 4xx - 0, 5xx - 0 + errored - 2 + Throughput: 2.51MB/s ``` \ No newline at end of file diff --git a/bombardier.go b/bombardier.go index 6dcbae5..61c27a3 100644 --- a/bombardier.go +++ b/bombardier.go @@ -1,11 +1,9 @@ package main import ( - "errors" "flag" "fmt" "io/ioutil" - "net/url" "os" "sync" "sync/atomic" @@ -21,20 +19,15 @@ const ( ) type bombardier struct { - numReqs int - numConns int - url string + conf config requestHeaders *fasthttp.RequestHeader - timeout time.Duration - - reqsDone uint64 + barrier completionBarrier bytesWritten int64 timeTaken time.Duration latencies *stats requests *stats - jobs sync.WaitGroup client *fasthttp.Client done chan bool @@ -55,56 +48,50 @@ type bombardier struct { bar *pb.ProgressBar } -func newBombardier(numConns, numReqs int, url string, headers *headersList, timeout time.Duration) (*bombardier, error) { - b := new(bombardier) - b.numReqs = numReqs - b.numConns = numConns - b.url = url - b.timeout = timeout - if err := b.checkArgs(); err != nil { +func newBombardier(c config) (*bombardier, error) { + if err := c.checkArgs(); err != nil { return nil, err } - b.latencies = newStats(b.timeout.Nanoseconds() / 1000) + b := new(bombardier) + b.conf = c + b.latencies = newStats(c.timeoutMillis()) b.requests = newStats(maxRps) - b.jobs.Add(b.numReqs) + if b.conf.testType == counted { + b.bar = pb.New64(int64(*b.conf.numReqs)) + b.barrier = newCountingCompletionBarrier(*c.numReqs, func() { + b.bar.Increment() + }) + } else if b.conf.testType == timed { + b.bar = pb.New(int(b.conf.duration.Seconds())) + b.bar.ShowCounters = false + b.bar.ShowPercent = false + b.barrier = newTimedCompletionBarrier(int(c.numConns), *c.duration, func() { + b.bar.Increment() + }) + } b.client = &fasthttp.Client{ - MaxConnsPerHost: b.numConns, + MaxConnsPerHost: int(c.numConns), } b.done = make(chan bool) - b.requestHeaders = headers.toRequestHeader() + b.requestHeaders = c.requestHeaders() return b, nil } -func (b *bombardier) checkArgs() error { - if b.numReqs < 1 { - return errors.New("Invalid number of requests(must be > 0)") - } - if b.numConns < 1 { - return errors.New("Invalid number of connections(must be > 0)") - } - if b.timeout < 0 { - return errors.New("Timeout can't be negative") - } - if b.timeout > 10*time.Second { - return errors.New("Timeout is too big(more that 10s)") - } - return nil -} - -func (b *bombardier) prepareRequest(headers *fasthttp.RequestHeader) (*fasthttp.Request, *fasthttp.Response) { +func (b *bombardier) prepareRequest() (*fasthttp.Request, *fasthttp.Response) { req := fasthttp.AcquireRequest() resp := fasthttp.AcquireResponse() - if headers != nil { - headers.CopyTo(&req.Header) + if b.requestHeaders != nil { + b.requestHeaders.CopyTo(&req.Header) } - req.Header.SetMethod("GET") - req.SetRequestURI(b.url) + req.Header.SetMethod(b.conf.method) + req.SetRequestURI(b.conf.url) + req.SetBodyString(b.conf.body) return req, resp } func (b *bombardier) fireRequest(req *fasthttp.Request, resp *fasthttp.Response) (bytesWritten int64, code int, msTaken uint64) { start := time.Now() - err := b.client.DoTimeout(req, resp, b.timeout) + err := b.client.DoTimeout(req, resp, b.conf.timeout) if err != nil { code = 0 } else { @@ -146,23 +133,13 @@ func (b *bombardier) writeStatistics(bytesWritten int64, code int, msTaken uint6 atomic.AddUint64(counter, 1) } -func (b *bombardier) grabWork() bool { - reqID := atomic.AddUint64(&b.reqsDone, 1) - return reqID <= uint64(b.numReqs) -} - -func (b *bombardier) reportDone() { - b.bar.Increment() - b.jobs.Done() -} - -func (b *bombardier) Worker(headers *fasthttp.RequestHeader) { - for b.grabWork() { - req, resp := b.prepareRequest(headers) +func (b *bombardier) Worker() { + for b.barrier.grabWork() { + req, resp := b.prepareRequest() bytesWritten, code, msTaken := b.fireRequest(req, resp) b.releaseRequest(req, resp) b.writeStatistics(bytesWritten, code, msTaken) - b.reportDone() + b.barrier.jobDone() } } @@ -192,31 +169,35 @@ func (b *bombardier) recordRps() { } func (b *bombardier) bombard() { - fmt.Printf("Bombarding %v with %v requests using %v connections\n", - b.url, b.numReqs, b.numConns) - b.bar = pb.StartNew(b.numReqs) + b.printIntro() + b.bar.Start() bombardmentBegin := time.Now() b.start = time.Now() - for i := 0; i < b.numConns; i++ { - var headers *fasthttp.RequestHeader - if b.requestHeaders != nil { - headers = new(fasthttp.RequestHeader) - b.requestHeaders.CopyTo(headers) - } - go b.Worker(headers) + for i := uint64(0); i < b.conf.numConns; i++ { + go b.Worker() } go b.rateMeter() - b.jobs.Wait() + b.barrier.wait() b.timeTaken = time.Since(bombardmentBegin) b.done <- true <-b.done - b.bar.Finish() + b.bar.FinishPrint("Done!") } func (b *bombardier) throughput() float64 { return float64(b.bytesWritten) / b.timeTaken.Seconds() } +func (b *bombardier) printIntro() { + if b.conf.testType == counted { + fmt.Printf("Bombarding %v with %v requests using %v connections\n", + b.conf.url, *b.conf.numReqs, b.conf.numConns) + } else if b.conf.testType == timed { + fmt.Printf("Bombarding %v for %v using %v connections\n", + b.conf.url, *b.conf.duration, b.conf.numConns) + } +} + func (b *bombardier) printLatencyStats() { percentiles := []float64{50.0, 75.0, 90.0, 99.0} fmt.Println(" Latency Distribution") @@ -242,14 +223,21 @@ func (b *bombardier) printStats() { fmt.Printf(" %-10v %10v/s\n", "Throughput:", formatBinary(b.throughput())) } -var headers = new(headersList) -var numConns = flag.Int("c", 200, "Maximum number of concurrent connections") -var numReqs = flag.Int("n", 10000, "Number of requests") -var timeout = flag.Duration("timeout", 2*time.Second, "Socket/request timeout") -var latencies = flag.Bool("latencies", false, "Print latency statistics") +var ( + numReqs = new(nullableUint64) + duration = new(nullableDuration) + headers = new(headersList) + numConns = flag.Uint64("c", 200, "Maximum number of concurrent connections") + timeout = flag.Duration("timeout", 2*time.Second, "Socket/request timeout") + latencies = flag.Bool("latencies", false, "Print latency statistics") + method = flag.String("m", "GET", "Request method") + body = flag.String("data", "", "Request body") +) func main() { flag.Var(headers, "H", "HTTP headers to use") + flag.Var(numReqs, "n", "Number of requests") + flag.Var(duration, "d", "Duration of test") flag.Parse() if flag.NArg() == 0 { fmt.Println("No URL supplied") @@ -260,20 +248,17 @@ func main() { fmt.Println("Too many arguments are supplied") os.Exit(1) } - rawurl := flag.Args()[0] - url, err := url.ParseRequestURI(rawurl) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - if url.Host == "" || (url.Scheme != "http" && url.Scheme != "https") { - fmt.Println("No hostname or invalid scheme") - os.Exit(1) - } bombardier, err := newBombardier( - *numConns, *numReqs, - url.String(), headers, - *timeout) + config{ + numConns: *numConns, + numReqs: numReqs.val, + duration: duration.val, + url: flag.Arg(0), + headers: headers, + timeout: *timeout, + method: *method, + body: *body, + }) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/completion_barrier.go b/completion_barrier.go new file mode 100644 index 0000000..af5c350 --- /dev/null +++ b/completion_barrier.go @@ -0,0 +1,77 @@ +package main + +import ( + "sync" + "sync/atomic" + "time" +) + +type completionBarrier interface { + grabWork() bool + jobDone() + wait() +} + +type countingCompletionBarrier struct { + numReqs, reqsDone uint64 + doneCallback func() + wg sync.WaitGroup +} + +func newCountingCompletionBarrier(numReqs uint64, callback func()) completionBarrier { + c := new(countingCompletionBarrier) + c.reqsDone, c.numReqs = 0, numReqs + c.doneCallback = callback + c.wg.Add(int(numReqs)) + return completionBarrier(c) +} + +func (c *countingCompletionBarrier) grabWork() bool { + return atomic.AddUint64(&c.reqsDone, 1) <= c.numReqs +} + +func (c *countingCompletionBarrier) jobDone() { + c.doneCallback() + c.wg.Done() +} + +func (c *countingCompletionBarrier) wait() { + c.wg.Wait() +} + +type timedCompletionBarrier struct { + wg sync.WaitGroup + tickCallback func() + done int64 +} + +func newTimedCompletionBarrier(parties int, duration time.Duration, callback func()) completionBarrier { + c := new(timedCompletionBarrier) + c.tickCallback = callback + c.done = 0 + c.wg.Add(parties) + go func() { + secs := int(duration.Seconds()) + for i := 1; i <= secs; i++ { + c.tickCallback() + time.Sleep(1 * time.Second) + } + atomic.CompareAndSwapInt64(&c.done, 0, 1) + }() + return completionBarrier(c) +} + +func (c *timedCompletionBarrier) grabWork() bool { + done := atomic.LoadInt64(&c.done) + if done == 1 { + c.wg.Done() + } + return done == 0 +} + +func (c *timedCompletionBarrier) jobDone() { +} + +func (c *timedCompletionBarrier) wait() { + c.wg.Wait() +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..3f9dd3e --- /dev/null +++ b/config.go @@ -0,0 +1,99 @@ +package main + +import ( + "errors" + "fmt" + "net/url" + "sort" + "time" + + "github.com/valyala/fasthttp" +) + +const ( + none = iota + timed + counted +) + +var ( + defaultTestDuration = 10 * time.Second + httpMethods = []string{"GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS"} + cantHaveBody = []string{"GET", "HEAD"} +) + +func init() { + sort.Strings(httpMethods) + sort.Strings(cantHaveBody) +} + +type config struct { + numConns uint64 + numReqs *uint64 + duration *time.Duration + url, method, body string + headers *headersList + timeout time.Duration + testType int +} + +func (c *config) checkArgs() error { + url, err := url.ParseRequestURI(c.url) + if err != nil { + return err + } + if url.Host == "" || (url.Scheme != "http" && url.Scheme != "https") { + return errors.New("No hostname or invalid scheme") + } + if c.numConns < uint64(1) { + return errors.New("Invalid number of connections(must be > 0)") + } + testType := none + if c.numReqs != nil { + testType = counted + } else if c.duration != nil { + testType = timed + } + c.testType = testType + if c.testType == none { + c.testType = timed + c.duration = &defaultTestDuration + } + if c.testType == counted && *c.numReqs < uint64(1) { + return errors.New("Invalid number of requests(must be > 0)") + } + if c.testType == timed && *c.duration < time.Second { + return errors.New("Invalid test duration(must be >= 1s)") + } + if c.timeout < 0 { + return errors.New("Timeout can't be negative") + } + if c.timeout > 10*time.Second { + return errors.New("Timeout is too big(more that 10s)") + } + if allowedHttpMethod(c.method) { + return errors.New(fmt.Sprintf("Unknown HTTP method: %v", c.method)) + } + if !canHaveBody(c.method) && len(c.body) > 0 { + return errors.New("GET and HEAD requests cannot have body") + } + return nil +} + +func (c *config) timeoutMillis() int64 { + return c.timeout.Nanoseconds() / 1000 +} + +func (c *config) requestHeaders() *fasthttp.RequestHeader { + return c.headers.toRequestHeader() +} + +func allowedHttpMethod(method string) bool { + i := sort.SearchStrings(httpMethods, method) + return !(i < len(httpMethods) && httpMethods[i] == method) +} + +func canHaveBody(method string) bool { + i := sort.SearchStrings(cantHaveBody, method) + return !(i < len(cantHaveBody) && cantHaveBody[i] == method) +} diff --git a/flags.go b/flags.go new file mode 100644 index 0000000..7649773 --- /dev/null +++ b/flags.go @@ -0,0 +1,47 @@ +package main + +import ( + "strconv" + "time" +) + +type nullableUint64 struct { + val *uint64 +} + +func (n *nullableUint64) String() string { + if n.val == nil { + return "nil" + } + return strconv.FormatUint(*n.val, 10) +} + +func (n *nullableUint64) Set(value string) error { + res, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return err + } + n.val = new(uint64) + *n.val = res + return nil +} + +type nullableDuration struct { + val *time.Duration +} + +func (n *nullableDuration) String() string { + if n.val == nil { + return "nil" + } + return n.val.String() +} + +func (n *nullableDuration) Set(value string) error { + res, err := time.ParseDuration(value) + if err != nil { + return err + } + n.val = &res + return nil +}