Skip to content

Commit 8ed6567

Browse files
committed
all: add rate limiting feature, refactor a bit and fix rounding errors
This commit: - adds rate limiting feature; - refactors and moves around some stuff; - cleans up README.md; - fixes two rounding errors in bombardier.go and stats.go. After a little research rate limiting were implemented with little to no impact on performance. Updates #14
1 parent 8ab90b7 commit 8ed6567

25 files changed

+1283
-165
lines changed

README.md

+4-27
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ bombardier is a HTTP(S) benchmarking tool. It is written in Go programming langu
44
Tested on go1.6 and higher. Use go1.7+ for best performance.
55

66
##Installation
7-
You can grab the latest version in the [releases](https://github.com/codesenberg/bombardier/releases) section.
8-
Alternatively, just run:
7+
You can grab binaries in the [releases](https://github.com/codesenberg/bombardier/releases) section.
8+
Alternatively, to get latest and greatest run:
99

1010
`go get -u github.com/codesenberg/bombardier`
1111

@@ -14,32 +14,9 @@ Alternatively, just run:
1414
bombardier [<flags>] <url>
1515
```
1616

17-
Flags:
18-
```
19-
--help Show context-sensitive help (also try
20-
--help-long and --help-man).
21-
-c, --connections=125 Maximum number of concurrent connections
22-
-t, --timeout=2s Socket/request timeout
23-
-l, --latencies Print latency statistics
24-
-m, --method=GET Request method
25-
-b, --body="" Request body
26-
--cert="" Path to the client's TLS Certificate
27-
--key="" Path to the client's TLS Certificate Private Key
17+
For a more detailed information about flags consult [GoDoc](http://godoc.org/github.com/codesenberg/bombardier).
2818

29-
-k, --insecure Controls whether a client verifies the server's
30-
certificate chain and host name
31-
-H, --headers=[] ... HTTP headers to use(can be repeated)
32-
-n, --requests=[<pos. int.>] Number of requests
33-
-d, --duration=10s Duration of test
34-
```
35-
Args:
36-
```
37-
<url> Target's URL
38-
```
39-
To set multiple headers just repeat the `H` flag, like so:
40-
```
41-
bombardier -H 'First: Value1' -H 'Second: Value2' -H 'Third: Value3' http://somehost:8080
42-
```
19+
##Examples
4320
Example of running `bombardier` against [this server](https://godoc.org/github.com/codesenberg/bombardier/cmd/utils/simplebenchserver):
4421
```
4522
> bombardier -c 125 -n 10000000 http://localhost:8080

args_parser.go

+10-11
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,6 @@ import (
88
"github.com/alecthomas/kingpin"
99
)
1010

11-
const (
12-
decBase = 10
13-
)
14-
15-
var (
16-
emptyConf = config{}
17-
parser = newKingpinParser()
18-
)
19-
2011
type argsParser interface {
2112
parse([]string) (config, error)
2213
}
@@ -37,6 +28,7 @@ type kingpinParser struct {
3728
body string
3829
certPath string
3930
keyPath string
31+
rate *nullableUint64
4032
}
4133

4234
func newKingpinParser() argsParser {
@@ -53,6 +45,7 @@ func newKingpinParser() argsParser {
5345
keyPath: "",
5446
insecure: false,
5547
url: "",
48+
rate: new(nullableUint64),
5649
}
5750

5851
app := kingpin.New("", "Fast cross-platform HTTP benchmarking tool").
@@ -90,18 +83,23 @@ func newKingpinParser() argsParser {
9083
BoolVar(&kparser.insecure)
9184

9285
app.Flag("header", "HTTP headers to use(can be repeated)").
93-
PlaceHolder("[]").
86+
PlaceHolder("\"K: V\"").
9487
Short('H').
9588
SetValue(kparser.headers)
9689
app.Flag("requests", "Number of requests").
97-
PlaceHolder("[<pos. int.>]").
90+
PlaceHolder("[pos. int.]").
9891
Short('n').
9992
SetValue(kparser.numReqs)
10093
app.Flag("duration", "Duration of test").
10194
PlaceHolder(defaultTestDuration.String()).
10295
Short('d').
10396
SetValue(kparser.duration)
10497

98+
app.Flag("rate", "Rate limit in requests per second").
99+
PlaceHolder("[pos. int.]").
100+
Short('r').
101+
SetValue(kparser.rate)
102+
105103
app.Arg("url", "Target's URL").Required().
106104
StringVar(&kparser.url)
107105

@@ -128,5 +126,6 @@ func (k *kingpinParser) parse(args []string) (config, error) {
128126
certPath: k.certPath,
129127
printLatencies: k.latencies,
130128
insecure: k.insecure,
129+
rate: k.rate.val,
131130
}, nil
132131
}

bombardier.go

+21-13
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type bombardier struct {
1818
conf config
1919
requestHeaders *fasthttp.RequestHeader
2020
barrier completionBarrier
21+
ratelimiter limiter
2122
workers sync.WaitGroup
2223

2324
bytesTotal int64
@@ -75,6 +76,13 @@ func newBombardier(c config) (*bombardier, error) {
7576
} else {
7677
b.barrier = newTimedCompletionBarrier(*b.conf.duration)
7778
}
79+
80+
if b.conf.rate != nil {
81+
b.ratelimiter = newBucketLimiter(*b.conf.rate)
82+
} else {
83+
b.ratelimiter = &nooplimiter{}
84+
}
85+
7886
b.out = os.Stdout
7987

8088
tlsConfig, err := generateTLSConfig(c)
@@ -168,8 +176,13 @@ func (b *bombardier) performSingleRequest() {
168176
}
169177

170178
func (b *bombardier) worker() {
179+
done := b.barrier.done()
171180
for b.barrier.tryGrabWork() {
181+
if b.ratelimiter.pace(done) == brk {
182+
break
183+
}
172184
b.performSingleRequest()
185+
b.barrier.jobDone()
173186
}
174187
}
175188

@@ -194,6 +207,11 @@ func (b *bombardier) barUpdater() {
194207
}
195208

196209
func (b *bombardier) rateMeter() {
210+
requestsInterval := 10 * time.Millisecond
211+
if b.conf.rate != nil {
212+
requestsInterval, _ = estimate(*b.conf.rate, rateLimitInterval)
213+
}
214+
requestsInterval += 10 * time.Millisecond
197215
ticker := time.NewTicker(requestsInterval)
198216
defer ticker.Stop()
199217
tick := ticker.C
@@ -219,7 +237,9 @@ func (b *bombardier) recordRps() {
219237
b.reqs = 0
220238
b.start = time.Now()
221239
b.rpl.Unlock()
222-
b.requests.record(uint64(float64(reqs) / duration.Seconds()))
240+
241+
reqsf := float64(reqs) / duration.Seconds()
242+
b.requests.record(round(reqsf))
223243
}
224244

225245
func (b *bombardier) bombard() {
@@ -299,18 +319,6 @@ func (b *bombardier) disableOutput() {
299319
b.bar.NotPrint = true
300320
}
301321

302-
const (
303-
maxRps = 10000000
304-
requestsInterval = 100 * time.Millisecond
305-
defaultTimeout = 2 * time.Second
306-
307-
exitFailure = 1
308-
)
309-
310-
var (
311-
version = "unspecified"
312-
)
313-
314322
func main() {
315323
cfg, err := parser.parse(os.Args)
316324
if err != nil {

bombardier_performance_test.go

+47-29
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
// +build go1.7
2-
31
package main
42

53
import (
@@ -11,35 +9,55 @@ import (
119

1210
var serverPort = flag.String("port", "8080", "port to use for benchmarks")
1311

14-
func BenchmarkBombardier(b *testing.B) {
12+
var (
13+
longDuration = 9001 * time.Hour
14+
highRate = uint64(1000000)
15+
)
16+
17+
func BenchmarkBombardierSingleReqPerf(b *testing.B) {
1518
addr := "localhost:" + *serverPort
16-
b.Run("single-req-perf", benchmarkFireRequest(addr))
19+
benchmarkFireRequest(config{
20+
numConns: defaultNumberOfConns,
21+
numReqs: nil,
22+
duration: &longDuration,
23+
url: "http://" + addr,
24+
headers: new(headersList),
25+
timeout: defaultTimeout,
26+
method: "GET",
27+
body: "",
28+
printLatencies: false,
29+
}, b)
1730
}
1831

19-
func benchmarkFireRequest(addr string) func(bm *testing.B) {
20-
return func(bm *testing.B) {
21-
longDuration := 9001 * time.Hour
22-
b, e := newBombardier(config{
23-
numConns: defaultNumberOfConns,
24-
numReqs: nil,
25-
duration: &longDuration,
26-
url: "http://" + addr,
27-
headers: new(headersList),
28-
timeout: defaultTimeout,
29-
method: "GET",
30-
body: "",
31-
printLatencies: false,
32-
})
33-
if e != nil {
34-
bm.Error(e)
35-
}
36-
b.disableOutput()
37-
bm.SetParallelism(int(defaultNumberOfConns) / runtime.NumCPU())
38-
bm.ResetTimer()
39-
bm.RunParallel(func(pb *testing.PB) {
40-
for pb.Next() {
41-
b.performSingleRequest()
42-
}
43-
})
32+
func BenchmarkBombardierRateLimitPerf(b *testing.B) {
33+
addr := "localhost:" + *serverPort
34+
benchmarkFireRequest(config{
35+
numConns: defaultNumberOfConns,
36+
numReqs: nil,
37+
duration: &longDuration,
38+
url: "http://" + addr,
39+
headers: new(headersList),
40+
timeout: defaultTimeout,
41+
method: "GET",
42+
body: "",
43+
printLatencies: false,
44+
rate: &highRate,
45+
}, b)
46+
}
47+
48+
func benchmarkFireRequest(c config, bm *testing.B) {
49+
b, e := newBombardier(c)
50+
if e != nil {
51+
bm.Error(e)
4452
}
53+
b.disableOutput()
54+
bm.SetParallelism(int(defaultNumberOfConns) / runtime.NumCPU())
55+
bm.ResetTimer()
56+
bm.RunParallel(func(pb *testing.PB) {
57+
done := b.barrier.done()
58+
for pb.Next() {
59+
b.ratelimiter.pace(done)
60+
b.performSingleRequest()
61+
}
62+
})
4563
}

bombardier_test.go

+35-8
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ func TestBombardierShouldFireSpecifiedNumberOfRequests(t *testing.T) {
2929
b, e := newBombardier(config{
3030
numConns: defaultNumberOfConns,
3131
numReqs: &numReqs,
32-
duration: nil,
3332
url: s.URL,
3433
headers: noHeaders,
3534
timeout: defaultTimeout,
@@ -60,7 +59,6 @@ func TestBombardierShouldFinish(t *testing.T) {
6059
desiredTestDuration := 1 * time.Second
6160
b, e := newBombardier(config{
6261
numConns: defaultNumberOfConns,
63-
numReqs: nil,
6462
duration: &desiredTestDuration,
6563
url: s.URL,
6664
headers: noHeaders,
@@ -106,7 +104,6 @@ func TestBombardierShouldSendHeaders(t *testing.T) {
106104
b, e := newBombardier(config{
107105
numConns: defaultNumberOfConns,
108106
numReqs: &numReqs,
109-
duration: nil,
110107
url: s.URL,
111108
headers: &requestHeaders,
112109
timeout: defaultTimeout,
@@ -143,7 +140,6 @@ func TestBombardierHttpCodeRecording(t *testing.T) {
143140
b, e := newBombardier(config{
144141
numConns: defaultNumberOfConns,
145142
numReqs: &numReqs,
146-
duration: nil,
147143
url: s.URL,
148144
headers: new(headersList),
149145
timeout: defaultTimeout,
@@ -217,7 +213,6 @@ func TestBombardierThroughputRecording(t *testing.T) {
217213
b, e := newBombardier(config{
218214
numConns: defaultNumberOfConns,
219215
numReqs: &numReqs,
220-
duration: nil,
221216
url: s.URL,
222217
headers: new(headersList),
223218
timeout: defaultTimeout,
@@ -255,7 +250,6 @@ func TestBombardierStatsPrinting(t *testing.T) {
255250
b, e := newBombardier(config{
256251
numConns: defaultNumberOfConns,
257252
numReqs: &numReqs,
258-
duration: nil,
259253
url: s.URL,
260254
headers: new(headersList),
261255
timeout: defaultTimeout,
@@ -284,7 +278,6 @@ func TestBombardierErrorIfFailToReadClientCert(t *testing.T) {
284278
_, e := newBombardier(config{
285279
numConns: defaultNumberOfConns,
286280
numReqs: &numReqs,
287-
duration: nil,
288281
url: "http://localhost",
289282
headers: new(headersList),
290283
timeout: defaultTimeout,
@@ -350,7 +343,6 @@ func TestBombardierClientCerts(t *testing.T) {
350343
b, e := newBombardier(config{
351344
numConns: defaultNumberOfConns,
352345
numReqs: &numReqs,
353-
duration: nil,
354346
url: "https://localhost:8080/",
355347
headers: new(headersList),
356348
timeout: defaultTimeout,
@@ -377,3 +369,38 @@ func TestBombardierClientCerts(t *testing.T) {
377369
t.Error(err)
378370
}
379371
}
372+
373+
func TestBombardierRateLimiting(t *testing.T) {
374+
responseSize := 1024
375+
response := bytes.Repeat([]byte{'a'}, responseSize)
376+
s := httptest.NewServer(
377+
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
378+
_, err := rw.Write(response)
379+
if err != nil {
380+
t.Error(err)
381+
}
382+
}),
383+
)
384+
rate := uint64(10000)
385+
testDuration := 1 * time.Second
386+
b, e := newBombardier(config{
387+
numConns: defaultNumberOfConns,
388+
duration: &testDuration,
389+
url: s.URL,
390+
headers: new(headersList),
391+
timeout: defaultTimeout,
392+
method: "GET",
393+
body: "",
394+
rate: &rate,
395+
})
396+
if e != nil {
397+
t.Error(e)
398+
return
399+
}
400+
b.disableOutput()
401+
b.bombard()
402+
if float64(b.req2xx) < float64(rate)*0.75 ||
403+
float64(b.req2xx) > float64(rate)*1.25 {
404+
t.Error(rate, b.req2xx)
405+
}
406+
}

client_cert_test.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package main
22

3-
import "testing"
3+
import (
4+
"testing"
5+
)
46

57
func TestGenerateTLSConfig(t *testing.T) {
68
expectations := []struct {

0 commit comments

Comments
 (0)