mirror of https://github.com/tidwall/tile38.git
vendored redbench
This commit is contained in:
parent
c8b44b47cc
commit
6008a78281
|
@ -1,12 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"math"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
@ -15,6 +10,7 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/tidwall/redbench"
|
||||||
"github.com/tidwall/tile38/core"
|
"github.com/tidwall/tile38/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -134,19 +130,22 @@ func parseArgs() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func fillOpts() *Options {
|
func fillOpts() *redbench.Options {
|
||||||
return &Options{
|
opts := *redbench.DefaultOptions
|
||||||
JSON: json,
|
opts.CSV = csv
|
||||||
CSV: csv,
|
opts.Clients = clients
|
||||||
Clients: clients,
|
opts.Pipeline = pipeline
|
||||||
Pipeline: pipeline,
|
opts.Quiet = quiet
|
||||||
Quiet: quiet,
|
opts.Requests = requests
|
||||||
Requests: requests,
|
opts.Stderr = os.Stderr
|
||||||
}
|
opts.Stdout = os.Stdout
|
||||||
|
return &opts
|
||||||
}
|
}
|
||||||
|
|
||||||
func randPoint() (lat, lon float64) {
|
func randPoint() (lat, lon float64) {
|
||||||
return rand.Float64()*180 - 90, rand.Float64()*360 - 180
|
return rand.Float64()*180 - 90, rand.Float64()*360 - 180
|
||||||
}
|
}
|
||||||
|
|
||||||
func randRect() (minlat, minlon, maxlat, maxlon float64) {
|
func randRect() (minlat, minlon, maxlat, maxlon float64) {
|
||||||
for {
|
for {
|
||||||
minlat, minlon = randPoint()
|
minlat, minlon = randPoint()
|
||||||
|
@ -156,6 +155,14 @@ func randRect() (minlat, minlon, maxlat, maxlon float64) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func prepFn(conn net.Conn) bool {
|
||||||
|
if json {
|
||||||
|
conn.Write([]byte("output json\r\n"))
|
||||||
|
resp := make([]byte, 100)
|
||||||
|
conn.Read(resp)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
func main() {
|
func main() {
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
if !parseArgs() {
|
if !parseArgs() {
|
||||||
|
@ -165,35 +172,35 @@ func main() {
|
||||||
for _, test := range strings.Split(tests, ",") {
|
for _, test := range strings.Split(tests, ",") {
|
||||||
switch strings.ToUpper(strings.TrimSpace(test)) {
|
switch strings.ToUpper(strings.TrimSpace(test)) {
|
||||||
case "PING":
|
case "PING":
|
||||||
RedisBench("PING", addr, fillOpts(),
|
redbench.Bench("PING", addr, fillOpts(), prepFn,
|
||||||
func(buf []byte, _ ServerType) []byte {
|
func(buf []byte) []byte {
|
||||||
return AppendCommand(buf, "PING")
|
return redbench.AppendCommand(buf, "PING")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
case "SET":
|
case "SET":
|
||||||
if redis {
|
if redis {
|
||||||
RedisBench("SET", addr, fillOpts(),
|
redbench.Bench("SET", addr, fillOpts(), prepFn,
|
||||||
func(buf []byte, _ ServerType) []byte {
|
func(buf []byte) []byte {
|
||||||
return AppendCommand(buf, "SET", "key:__rand_int__", "xxx")
|
return redbench.AppendCommand(buf, "SET", "key:__rand_int__", "xxx")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
var i int64
|
var i int64
|
||||||
RedisBench("SET (point)", addr, fillOpts(),
|
redbench.Bench("SET (point)", addr, fillOpts(), prepFn,
|
||||||
func(buf []byte, _ ServerType) []byte {
|
func(buf []byte) []byte {
|
||||||
i := atomic.AddInt64(&i, 1)
|
i := atomic.AddInt64(&i, 1)
|
||||||
lat, lon := randPoint()
|
lat, lon := randPoint()
|
||||||
return AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "POINT",
|
return redbench.AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "POINT",
|
||||||
strconv.FormatFloat(lat, 'f', 5, 64),
|
strconv.FormatFloat(lat, 'f', 5, 64),
|
||||||
strconv.FormatFloat(lon, 'f', 5, 64),
|
strconv.FormatFloat(lon, 'f', 5, 64),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
RedisBench("SET (rect)", addr, fillOpts(),
|
redbench.Bench("SET (rect)", addr, fillOpts(), prepFn,
|
||||||
func(buf []byte, _ ServerType) []byte {
|
func(buf []byte) []byte {
|
||||||
i := atomic.AddInt64(&i, 1)
|
i := atomic.AddInt64(&i, 1)
|
||||||
minlat, minlon, maxlat, maxlon := randRect()
|
minlat, minlon, maxlat, maxlon := randRect()
|
||||||
return AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "BOUNDS",
|
return redbench.AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "BOUNDS",
|
||||||
strconv.FormatFloat(minlat, 'f', 5, 64),
|
strconv.FormatFloat(minlat, 'f', 5, 64),
|
||||||
strconv.FormatFloat(minlon, 'f', 5, 64),
|
strconv.FormatFloat(minlon, 'f', 5, 64),
|
||||||
strconv.FormatFloat(maxlat, 'f', 5, 64),
|
strconv.FormatFloat(maxlat, 'f', 5, 64),
|
||||||
|
@ -201,65 +208,65 @@ func main() {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
RedisBench("SET (string)", addr, fillOpts(),
|
redbench.Bench("SET (string)", addr, fillOpts(), prepFn,
|
||||||
func(buf []byte, _ ServerType) []byte {
|
func(buf []byte) []byte {
|
||||||
i := atomic.AddInt64(&i, 1)
|
i := atomic.AddInt64(&i, 1)
|
||||||
return AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "STRING", "xxx")
|
return redbench.AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "STRING", "xxx")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case "GET":
|
case "GET":
|
||||||
if redis {
|
if redis {
|
||||||
RedisBench("GET", addr, fillOpts(),
|
redbench.Bench("GET", addr, fillOpts(), prepFn,
|
||||||
func(buf []byte, _ ServerType) []byte {
|
func(buf []byte) []byte {
|
||||||
return AppendCommand(buf, "GET", "key:__rand_int__")
|
return redbench.AppendCommand(buf, "GET", "key:__rand_int__")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
var i int64
|
var i int64
|
||||||
RedisBench("GET (point)", addr, fillOpts(),
|
redbench.Bench("GET (point)", addr, fillOpts(), prepFn,
|
||||||
func(buf []byte, _ ServerType) []byte {
|
func(buf []byte) []byte {
|
||||||
i := atomic.AddInt64(&i, 1)
|
i := atomic.AddInt64(&i, 1)
|
||||||
return AppendCommand(buf, "GET", "key:bench", "id:"+strconv.FormatInt(i, 10), "POINT")
|
return redbench.AppendCommand(buf, "GET", "key:bench", "id:"+strconv.FormatInt(i, 10), "POINT")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
RedisBench("GET (rect)", addr, fillOpts(),
|
redbench.Bench("GET (rect)", addr, fillOpts(), prepFn,
|
||||||
func(buf []byte, _ ServerType) []byte {
|
func(buf []byte) []byte {
|
||||||
i := atomic.AddInt64(&i, 1)
|
i := atomic.AddInt64(&i, 1)
|
||||||
return AppendCommand(buf, "GET", "key:bench", "id:"+strconv.FormatInt(i, 10), "BOUNDS")
|
return redbench.AppendCommand(buf, "GET", "key:bench", "id:"+strconv.FormatInt(i, 10), "BOUNDS")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
RedisBench("GET (string)", addr, fillOpts(),
|
redbench.Bench("GET (string)", addr, fillOpts(), prepFn,
|
||||||
func(buf []byte, _ ServerType) []byte {
|
func(buf []byte) []byte {
|
||||||
i := atomic.AddInt64(&i, 1)
|
i := atomic.AddInt64(&i, 1)
|
||||||
return AppendCommand(buf, "GET", "key:bench", "id:"+strconv.FormatInt(i, 10), "OBJECT")
|
return redbench.AppendCommand(buf, "GET", "key:bench", "id:"+strconv.FormatInt(i, 10), "OBJECT")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case "SEARCH":
|
case "SEARCH":
|
||||||
if !redis {
|
if !redis {
|
||||||
RedisBench("SEARCH (nearby 1km)", addr, fillOpts(),
|
redbench.Bench("SEARCH (nearby 1km)", addr, fillOpts(), prepFn,
|
||||||
func(buf []byte, _ ServerType) []byte {
|
func(buf []byte) []byte {
|
||||||
lat, lon := randPoint()
|
lat, lon := randPoint()
|
||||||
return AppendCommand(buf, "NEARBY", "key:bench", "COUNT", "POINT",
|
return redbench.AppendCommand(buf, "NEARBY", "key:bench", "COUNT", "POINT",
|
||||||
strconv.FormatFloat(lat, 'f', 5, 64),
|
strconv.FormatFloat(lat, 'f', 5, 64),
|
||||||
strconv.FormatFloat(lon, 'f', 5, 64),
|
strconv.FormatFloat(lon, 'f', 5, 64),
|
||||||
"1000")
|
"1000")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
RedisBench("SEARCH (nearby 10km)", addr, fillOpts(),
|
redbench.Bench("SEARCH (nearby 10km)", addr, fillOpts(), prepFn,
|
||||||
func(buf []byte, _ ServerType) []byte {
|
func(buf []byte) []byte {
|
||||||
lat, lon := randPoint()
|
lat, lon := randPoint()
|
||||||
return AppendCommand(buf, "NEARBY", "key:bench", "COUNT", "POINT",
|
return redbench.AppendCommand(buf, "NEARBY", "key:bench", "COUNT", "POINT",
|
||||||
strconv.FormatFloat(lat, 'f', 5, 64),
|
strconv.FormatFloat(lat, 'f', 5, 64),
|
||||||
strconv.FormatFloat(lon, 'f', 5, 64),
|
strconv.FormatFloat(lon, 'f', 5, 64),
|
||||||
"10000")
|
"10000")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
RedisBench("SEARCH (nearby 100km)", addr, fillOpts(),
|
redbench.Bench("SEARCH (nearby 100km)", addr, fillOpts(), prepFn,
|
||||||
func(buf []byte, _ ServerType) []byte {
|
func(buf []byte) []byte {
|
||||||
lat, lon := randPoint()
|
lat, lon := randPoint()
|
||||||
return AppendCommand(buf, "NEARBY", "key:bench", "COUNT", "POINT",
|
return redbench.AppendCommand(buf, "NEARBY", "key:bench", "COUNT", "POINT",
|
||||||
strconv.FormatFloat(lat, 'f', 5, 64),
|
strconv.FormatFloat(lat, 'f', 5, 64),
|
||||||
strconv.FormatFloat(lon, 'f', 5, 64),
|
strconv.FormatFloat(lon, 'f', 5, 64),
|
||||||
"100000")
|
"100000")
|
||||||
|
@ -269,253 +276,3 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func readResp(rd *bufio.Reader, n int) error {
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
line, err := rd.ReadBytes('\n')
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
switch line[0] {
|
|
||||||
default:
|
|
||||||
return errors.New("invalid server response")
|
|
||||||
case '+', ':':
|
|
||||||
case '-':
|
|
||||||
//panic(string(line))
|
|
||||||
case '$':
|
|
||||||
n, err := strconv.ParseInt(string(line[1:len(line)-2]), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err = io.CopyN(ioutil.Discard, rd, n+2); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case '*':
|
|
||||||
n, err := strconv.ParseInt(string(line[1:len(line)-2]), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readResp(rd, int(n))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServerType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
Redis = 1
|
|
||||||
Tile38 = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
Requests int
|
|
||||||
Clients int
|
|
||||||
Pipeline int
|
|
||||||
Quiet bool
|
|
||||||
CSV bool
|
|
||||||
JSON bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func RedisBench(
|
|
||||||
name string,
|
|
||||||
addr string,
|
|
||||||
opts *Options,
|
|
||||||
fill func(buf []byte, server ServerType) []byte,
|
|
||||||
) {
|
|
||||||
var server ServerType
|
|
||||||
var totalPayload uint64
|
|
||||||
var count uint64
|
|
||||||
var duration int64
|
|
||||||
rpc := opts.Requests / opts.Clients
|
|
||||||
rpcex := opts.Requests % opts.Clients
|
|
||||||
var tstop int64
|
|
||||||
remaining := int64(opts.Clients)
|
|
||||||
errs := make([]error, opts.Clients)
|
|
||||||
durs := make([][]time.Duration, opts.Clients)
|
|
||||||
conns := make([]net.Conn, opts.Clients)
|
|
||||||
|
|
||||||
// create all clients
|
|
||||||
for i := 0; i < opts.Clients; i++ {
|
|
||||||
crequests := rpc
|
|
||||||
if i == opts.Clients-1 {
|
|
||||||
crequests += rpcex
|
|
||||||
}
|
|
||||||
durs[i] = make([]time.Duration, crequests)
|
|
||||||
for j := 0; j < len(durs[i]); j++ {
|
|
||||||
durs[i][j] = -1
|
|
||||||
}
|
|
||||||
conn, err := net.Dial("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
if i == 0 {
|
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
errs[i] = err
|
|
||||||
}
|
|
||||||
conns[i] = conn
|
|
||||||
if conn != nil {
|
|
||||||
if i == 0 {
|
|
||||||
conn.Write([]byte("info server\r\n"))
|
|
||||||
resp := make([]byte, 500)
|
|
||||||
conn.Read(resp)
|
|
||||||
if strings.Contains(string(resp), "redis_version") {
|
|
||||||
if strings.Contains(string(resp), "tile38_version") {
|
|
||||||
server = Tile38
|
|
||||||
} else {
|
|
||||||
server = Redis
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if opts.JSON {
|
|
||||||
conn.Write([]byte("output json\r\n"))
|
|
||||||
resp := make([]byte, 100)
|
|
||||||
conn.Read(resp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tstart := time.Now()
|
|
||||||
for i := 0; i < opts.Clients; i++ {
|
|
||||||
crequests := rpc
|
|
||||||
if i == opts.Clients-1 {
|
|
||||||
crequests += rpcex
|
|
||||||
}
|
|
||||||
|
|
||||||
go func(conn net.Conn, client, crequests int) {
|
|
||||||
defer func() {
|
|
||||||
atomic.AddInt64(&remaining, -1)
|
|
||||||
}()
|
|
||||||
if conn == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err := func() error {
|
|
||||||
var buf []byte
|
|
||||||
rd := bufio.NewReader(conn)
|
|
||||||
for i := 0; i < crequests; i += opts.Pipeline {
|
|
||||||
n := opts.Pipeline
|
|
||||||
if i+n > crequests {
|
|
||||||
n = crequests - i
|
|
||||||
}
|
|
||||||
buf = buf[:0]
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
buf = fill(buf, server)
|
|
||||||
}
|
|
||||||
atomic.AddUint64(&totalPayload, uint64(len(buf)))
|
|
||||||
start := time.Now()
|
|
||||||
_, err := conn.Write(buf)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := readResp(rd, n); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
stop := time.Since(start)
|
|
||||||
for j := 0; j < n; j++ {
|
|
||||||
durs[client][i+j] = stop / time.Duration(n)
|
|
||||||
}
|
|
||||||
atomic.AddInt64(&duration, int64(stop))
|
|
||||||
atomic.AddUint64(&count, uint64(n))
|
|
||||||
atomic.StoreInt64(&tstop, int64(time.Since(tstart)))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}()
|
|
||||||
if err != nil {
|
|
||||||
errs[client] = err
|
|
||||||
}
|
|
||||||
}(conns[i], i, crequests)
|
|
||||||
}
|
|
||||||
var die bool
|
|
||||||
for {
|
|
||||||
remaining := int(atomic.LoadInt64(&remaining)) // active clients
|
|
||||||
count := int(atomic.LoadUint64(&count)) // completed requests
|
|
||||||
real := time.Duration(atomic.LoadInt64(&tstop)) // real duration
|
|
||||||
totalPayload := int(atomic.LoadUint64(&totalPayload)) // size of all bytes sent
|
|
||||||
more := remaining > 0
|
|
||||||
var realrps float64
|
|
||||||
if real > 0 {
|
|
||||||
realrps = float64(count) / (float64(real) / float64(time.Second))
|
|
||||||
}
|
|
||||||
if !opts.CSV {
|
|
||||||
fmt.Printf("\r%s: %.2f", name, realrps)
|
|
||||||
if more {
|
|
||||||
fmt.Printf("\r")
|
|
||||||
} else if opts.Quiet {
|
|
||||||
fmt.Printf(" requests per second\n")
|
|
||||||
} else {
|
|
||||||
fmt.Printf("\r====== %s ======\n", name)
|
|
||||||
fmt.Printf(" %d requests completed in %.2f seconds\n", opts.Requests, float64(real)/float64(time.Second))
|
|
||||||
fmt.Printf(" %d parallel clients\n", opts.Clients)
|
|
||||||
fmt.Printf(" %d bytes payload\n", totalPayload/opts.Requests)
|
|
||||||
fmt.Printf(" keep alive: 1\n")
|
|
||||||
fmt.Printf("\n")
|
|
||||||
var limit time.Duration
|
|
||||||
var lastper float64
|
|
||||||
for {
|
|
||||||
limit += time.Millisecond
|
|
||||||
var hits, count int
|
|
||||||
for i := 0; i < len(durs); i++ {
|
|
||||||
for j := 0; j < len(durs[i]); j++ {
|
|
||||||
dur := durs[i][j]
|
|
||||||
if dur == -1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if dur < limit {
|
|
||||||
hits++
|
|
||||||
}
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
per := float64(hits) / float64(count)
|
|
||||||
if math.Floor(per*10000) == math.Floor(lastper*10000) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
lastper = per
|
|
||||||
fmt.Printf("%.2f%% <= %d milliseconds\n", per*100, (limit-time.Millisecond)/time.Millisecond)
|
|
||||||
if per == 1.0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Printf("%.2f requests per second\n\n", realrps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !more {
|
|
||||||
if opts.CSV {
|
|
||||||
fmt.Printf("\"%s\",\"%.2f\"\n", name, realrps)
|
|
||||||
}
|
|
||||||
for _, err := range errs {
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
|
||||||
die = true
|
|
||||||
if count == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(time.Second / 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
// close clients
|
|
||||||
for i := 0; i < len(conns); i++ {
|
|
||||||
if conns[i] != nil {
|
|
||||||
conns[i].Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if die {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func AppendCommand(buf []byte, args ...string) []byte {
|
|
||||||
buf = append(buf, '*')
|
|
||||||
buf = strconv.AppendInt(buf, int64(len(args)), 10)
|
|
||||||
buf = append(buf, '\r', '\n')
|
|
||||||
for _, arg := range args {
|
|
||||||
buf = append(buf, '$')
|
|
||||||
buf = strconv.AppendInt(buf, int64(len(arg)), 10)
|
|
||||||
buf = append(buf, '\r', '\n')
|
|
||||||
buf = append(buf, arg...)
|
|
||||||
buf = append(buf, '\r', '\n')
|
|
||||||
}
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2017 Josh Baker
|
||||||
|
|
||||||
|
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
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
|
@ -0,0 +1,93 @@
|
||||||
|
# Redbench
|
||||||
|
[![GoDoc](https://img.shields.io/badge/api-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/tidwall/redbench)
|
||||||
|
|
||||||
|
Redbench is a Go package that allows for bootstrapping benchmarks
|
||||||
|
for servers using a custom implementation of the Redis protocol. It provides
|
||||||
|
the same inputs and outputs as the
|
||||||
|
[redis-benchmark](https://redis.io/topics/benchmarks) tool. The purpose of
|
||||||
|
this library is to provide benchmarking for
|
||||||
|
[Redcon](https://github.com/tidwall/redcon) compatible servers such as
|
||||||
|
[Tile38](https://github.com/tidwall/tile38), but also works well for Redis
|
||||||
|
operations that are not covered by the `redis-benchmark` tool such as the
|
||||||
|
`GEO*` commands, custom lua scripts, or [Redis Modules](http://antirez.com/news/106).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Installing
|
||||||
|
|
||||||
|
To start using Redbench, install Go and run `go get`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ go get -u github.com/tidwall/redbench
|
||||||
|
```
|
||||||
|
|
||||||
|
This will retrieve the library.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
The following example will run a benchmark for the `PING,SET,GET,GEOADD,GEORADIUS`
|
||||||
|
commands on a server at 127.0.0.1:6379.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tidwall/redbench"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
redbench.Bench("PING", "127.0.0.1:6379", nil, nil, func(buf []byte) []byte {
|
||||||
|
return redbench.AppendCommand(buf, "PING")
|
||||||
|
})
|
||||||
|
redbench.Bench("SET", "127.0.0.1:6379", nil, nil, func(buf []byte) []byte {
|
||||||
|
return redbench.AppendCommand(buf, "SET", "key:string", "val")
|
||||||
|
})
|
||||||
|
redbench.Bench("GET", "127.0.0.1:6379", nil, nil, func(buf []byte) []byte {
|
||||||
|
return redbench.AppendCommand(buf, "GET", "key:string")
|
||||||
|
})
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
redbench.Bench("GEOADD", "127.0.0.1:6379", nil, nil, func(buf []byte) []byte {
|
||||||
|
return redbench.AppendCommand(buf, "GEOADD", "key:geo",
|
||||||
|
strconv.FormatFloat(rand.Float64()*360-180, 'f', 7, 64),
|
||||||
|
strconv.FormatFloat(rand.Float64()*170-85, 'f', 7, 64),
|
||||||
|
strconv.Itoa(rand.Int()))
|
||||||
|
})
|
||||||
|
redbench.Bench("GEORADIUS", "127.0.0.1:6379", nil, nil, func(buf []byte) []byte {
|
||||||
|
return redbench.AppendCommand(buf, "GEORADIUS", "key:geo",
|
||||||
|
strconv.FormatFloat(rand.Float64()*360-180, 'f', 7, 64),
|
||||||
|
strconv.FormatFloat(rand.Float64()*170-85, 'f', 7, 64),
|
||||||
|
"10", "km")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The operations is like as executing, but without the `GEO*` commands.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ redis-benchmark -t PING,SET,GET
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Options
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Options struct {
|
||||||
|
Requests int
|
||||||
|
Clients int
|
||||||
|
Pipeline int
|
||||||
|
Quiet bool
|
||||||
|
CSV bool
|
||||||
|
Stdout io.Writer
|
||||||
|
Stderr io.Writer
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
Josh Baker [@tidwall](http://twitter.com/tidwall)
|
||||||
|
|
||||||
|
## License
|
||||||
|
Redbench source code is available under the MIT [License](/LICENSE).
|
||||||
|
|
|
@ -0,0 +1,271 @@
|
||||||
|
package redbench
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func readResp(rd *bufio.Reader, n int) error {
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
line, err := rd.ReadBytes('\n')
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch line[0] {
|
||||||
|
default:
|
||||||
|
return errors.New("invalid server response")
|
||||||
|
case '+', ':':
|
||||||
|
case '-':
|
||||||
|
case '$':
|
||||||
|
n, err := strconv.ParseInt(string(line[1:len(line)-2]), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err = io.CopyN(ioutil.Discard, rd, n+2); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case '*':
|
||||||
|
n, err := strconv.ParseInt(string(line[1:len(line)-2]), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
readResp(rd, int(n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options represents various options used by the Bench() function.
|
||||||
|
type Options struct {
|
||||||
|
Requests int
|
||||||
|
Clients int
|
||||||
|
Pipeline int
|
||||||
|
Quiet bool
|
||||||
|
CSV bool
|
||||||
|
Stdout io.Writer
|
||||||
|
Stderr io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultsOptions are the default options used by the Bench() function.
|
||||||
|
var DefaultOptions = &Options{
|
||||||
|
Requests: 100000,
|
||||||
|
Clients: 50,
|
||||||
|
Pipeline: 1,
|
||||||
|
Quiet: false,
|
||||||
|
CSV: false,
|
||||||
|
Stdout: os.Stdout,
|
||||||
|
Stderr: os.Stderr,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bench performs a benchmark on the server at the specified address.
|
||||||
|
func Bench(
|
||||||
|
name string,
|
||||||
|
addr string,
|
||||||
|
opts *Options,
|
||||||
|
prep func(conn net.Conn) bool,
|
||||||
|
fill func(buf []byte) []byte,
|
||||||
|
) {
|
||||||
|
if opts == nil {
|
||||||
|
opts = DefaultOptions
|
||||||
|
}
|
||||||
|
if opts.Stderr == nil {
|
||||||
|
opts.Stderr = ioutil.Discard
|
||||||
|
}
|
||||||
|
if opts.Stdout == nil {
|
||||||
|
opts.Stdout = ioutil.Discard
|
||||||
|
}
|
||||||
|
var totalPayload uint64
|
||||||
|
var count uint64
|
||||||
|
var duration int64
|
||||||
|
rpc := opts.Requests / opts.Clients
|
||||||
|
rpcex := opts.Requests % opts.Clients
|
||||||
|
var tstop int64
|
||||||
|
remaining := int64(opts.Clients)
|
||||||
|
errs := make([]error, opts.Clients)
|
||||||
|
durs := make([][]time.Duration, opts.Clients)
|
||||||
|
conns := make([]net.Conn, opts.Clients)
|
||||||
|
|
||||||
|
// create all clients
|
||||||
|
for i := 0; i < opts.Clients; i++ {
|
||||||
|
crequests := rpc
|
||||||
|
if i == opts.Clients-1 {
|
||||||
|
crequests += rpcex
|
||||||
|
}
|
||||||
|
durs[i] = make([]time.Duration, crequests)
|
||||||
|
for j := 0; j < len(durs[i]); j++ {
|
||||||
|
durs[i][j] = -1
|
||||||
|
}
|
||||||
|
conn, err := net.Dial("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
if i == 0 {
|
||||||
|
fmt.Fprintf(opts.Stderr, "%s\n", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errs[i] = err
|
||||||
|
}
|
||||||
|
if conn != nil && prep != nil {
|
||||||
|
if !prep(conn) {
|
||||||
|
conn.Close()
|
||||||
|
conn = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conns[i] = conn
|
||||||
|
}
|
||||||
|
|
||||||
|
tstart := time.Now()
|
||||||
|
for i := 0; i < opts.Clients; i++ {
|
||||||
|
crequests := rpc
|
||||||
|
if i == opts.Clients-1 {
|
||||||
|
crequests += rpcex
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(conn net.Conn, client, crequests int) {
|
||||||
|
defer func() {
|
||||||
|
atomic.AddInt64(&remaining, -1)
|
||||||
|
}()
|
||||||
|
if conn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := func() error {
|
||||||
|
var buf []byte
|
||||||
|
rd := bufio.NewReader(conn)
|
||||||
|
for i := 0; i < crequests; i += opts.Pipeline {
|
||||||
|
n := opts.Pipeline
|
||||||
|
if i+n > crequests {
|
||||||
|
n = crequests - i
|
||||||
|
}
|
||||||
|
buf = buf[:0]
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
buf = fill(buf)
|
||||||
|
}
|
||||||
|
atomic.AddUint64(&totalPayload, uint64(len(buf)))
|
||||||
|
start := time.Now()
|
||||||
|
_, err := conn.Write(buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := readResp(rd, n); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stop := time.Since(start)
|
||||||
|
for j := 0; j < n; j++ {
|
||||||
|
durs[client][i+j] = stop / time.Duration(n)
|
||||||
|
}
|
||||||
|
atomic.AddInt64(&duration, int64(stop))
|
||||||
|
atomic.AddUint64(&count, uint64(n))
|
||||||
|
atomic.StoreInt64(&tstop, int64(time.Since(tstart)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
errs[client] = err
|
||||||
|
}
|
||||||
|
}(conns[i], i, crequests)
|
||||||
|
}
|
||||||
|
var die bool
|
||||||
|
for {
|
||||||
|
remaining := int(atomic.LoadInt64(&remaining)) // active clients
|
||||||
|
count := int(atomic.LoadUint64(&count)) // completed requests
|
||||||
|
real := time.Duration(atomic.LoadInt64(&tstop)) // real duration
|
||||||
|
totalPayload := int(atomic.LoadUint64(&totalPayload)) // size of all bytes sent
|
||||||
|
more := remaining > 0
|
||||||
|
var realrps float64
|
||||||
|
if real > 0 {
|
||||||
|
realrps = float64(count) / (float64(real) / float64(time.Second))
|
||||||
|
}
|
||||||
|
if !opts.CSV {
|
||||||
|
fmt.Fprintf(opts.Stdout, "\r%s: %.2f", name, realrps)
|
||||||
|
if more {
|
||||||
|
fmt.Fprintf(opts.Stdout, "\r")
|
||||||
|
} else if opts.Quiet {
|
||||||
|
fmt.Fprintf(opts.Stdout, " requests per second\n")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(opts.Stdout, "\r====== %s ======\n", name)
|
||||||
|
fmt.Fprintf(opts.Stdout, " %d requests completed in %.2f seconds\n", opts.Requests, float64(real)/float64(time.Second))
|
||||||
|
fmt.Fprintf(opts.Stdout, " %d parallel clients\n", opts.Clients)
|
||||||
|
fmt.Fprintf(opts.Stdout, " %d bytes payload\n", totalPayload/opts.Requests)
|
||||||
|
fmt.Fprintf(opts.Stdout, " keep alive: 1\n")
|
||||||
|
fmt.Fprintf(opts.Stdout, "\n")
|
||||||
|
var limit time.Duration
|
||||||
|
var lastper float64
|
||||||
|
for {
|
||||||
|
limit += time.Millisecond
|
||||||
|
var hits, count int
|
||||||
|
for i := 0; i < len(durs); i++ {
|
||||||
|
for j := 0; j < len(durs[i]); j++ {
|
||||||
|
dur := durs[i][j]
|
||||||
|
if dur == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if dur < limit {
|
||||||
|
hits++
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
per := float64(hits) / float64(count)
|
||||||
|
if math.Floor(per*10000) == math.Floor(lastper*10000) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lastper = per
|
||||||
|
fmt.Fprintf(opts.Stdout, "%.2f%% <= %d milliseconds\n", per*100, (limit-time.Millisecond)/time.Millisecond)
|
||||||
|
if per == 1.0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintf(opts.Stdout, "%.2f requests per second\n\n", realrps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !more {
|
||||||
|
if opts.CSV {
|
||||||
|
fmt.Fprintf(opts.Stdout, "\"%s\",\"%.2f\"\n", name, realrps)
|
||||||
|
}
|
||||||
|
for _, err := range errs {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(opts.Stderr, "%s\n", err)
|
||||||
|
die = true
|
||||||
|
if count == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second / 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
// close clients
|
||||||
|
for i := 0; i < len(conns); i++ {
|
||||||
|
if conns[i] != nil {
|
||||||
|
conns[i].Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if die {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendCommand will append a Redis command to the byte slice and
|
||||||
|
// returns a modifed slice.
|
||||||
|
func AppendCommand(buf []byte, args ...string) []byte {
|
||||||
|
buf = append(buf, '*')
|
||||||
|
buf = strconv.AppendInt(buf, int64(len(args)), 10)
|
||||||
|
buf = append(buf, '\r', '\n')
|
||||||
|
for _, arg := range args {
|
||||||
|
buf = append(buf, '$')
|
||||||
|
buf = strconv.AppendInt(buf, int64(len(arg)), 10)
|
||||||
|
buf = append(buf, '\r', '\n')
|
||||||
|
buf = append(buf, arg...)
|
||||||
|
buf = append(buf, '\r', '\n')
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
Loading…
Reference in New Issue