From 444bd9144b6007b36be1fa3a899331f43c51f142 Mon Sep 17 00:00:00 2001 From: vesem Date: Sat, 25 Dec 2021 00:49:07 -0500 Subject: [PATCH] Initial Commit --- .gitignore | 105 +++++++++++++++++++++++++++++++ Dockerfile | 29 +++++++++ Makefile | 32 ++++++++++ README.md | 10 +++ go.mod | 10 +++ go.sum | 14 +++++ redisLoadTest.go | 157 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 357 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 redisLoadTest.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..085f9fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,105 @@ +# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig + +# Created by https://www.gitignore.io/api/macos,visualstudiocode,terraform,windows +# Edit at https://www.gitignore.io/?templates=macos,visualstudiocode,terraform,windows + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Terraform ### +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* +*.plan + +# Crash log files +crash.log + +# Ignore any .tfvars files that are generated automatically for each Terraform run. Most +# .tfvars files are managed as part of configuration and so should be included in +# version control. +# +# example.tfvars + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json +secrets.tfvars + +# Include override files you do wish to add to version control using negated pattern +# +# !example_override.tf + +### VisualStudioCode ### +.vscode/* +#!.vscode/settings.json +#!.vscode/tasks.json +#!.vscode/launch.json +#!.vscode/extensions.json + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.gitignore.io/api/macos,visualstudiocode,terraform,windows + +# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) +.vscode/* + +.idea \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1f03a75 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +FROM golang:1.17.5-bullseye AS builder +COPY go.mod . +COPY redisLoadTest.go . + +RUN set -ex \ + && go build -o redisLoadTest redisLoadTest.go + +FROM golang:1.17.5-bullseye AS redisloadtest + +ENV USER=redisloadtest +ENV FILEPATH=/usr/local/bin/ +ENV FILENAME=redisLoadTest +ENV HOME=/home/${USER} + +RUN useradd ${USER} --home-dir ${HOME} + +WORKDIR /tmp + +# Copy the binary from the builder stage +COPY --from=builder ${FILENAME} ./ + +RUN mv /tmp/${FILENAME} ${FILEPATH} \ + && chown ${USER}:${USER} ${FILEPATH}${FILENAME} \ + && chmod 4555 ${FILENAME}${FILEPATH} \ + && rm -rf -- /tmp/* \ + +WORKDIR ${HOME} + +CMD ["${FILEPATH}${FILENAME}"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..83e58ad --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +#################################################### +# Build Container +#################################################### +build: + docker build -t redisloadtest . +#################################################### +# Local Build +#################################################### +build-local: + go build -o redisLoadTest redisLoadTest.go +#################################################### +# Prune docker environment of stale resources +#################################################### +clean-docker: + docker system prune -a -f +#################################################### +# Perform nothing +#################################################### +all: + @echo "Nothing to do." +#################################################### +# help feature +#################################################### +help: + @echo '' + @echo 'Usage: make [TARGET]' + @echo 'Targets:' + @echo ' build build docker --image--' + @echo ' build-local go build -o redisLoadTest redisLoadTest.go' + @echo ' clean-docker shortcut for down and docker system prune -af. Cleanup inactive containers and cache.' + @echo ' all Nothing to do.' + @echo '' \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d02cfe --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +#Redis Load Test +A simple program to perform load testing against a redis instance + +## Requirements +- go compiler + +## Install + +## Commands + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b620a8d --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module redis + +go 1.17 + +require github.com/gomodule/redigo v1.8.6 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f7d767d --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gomodule/redigo v1.8.6 h1:h7kHSqUl2kxeaQtVslsfUCPJ1oz2pxcyzLy4zezIzPw= +github.com/gomodule/redigo v1.8.6/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/redisLoadTest.go b/redisLoadTest.go new file mode 100644 index 0000000..57518c1 --- /dev/null +++ b/redisLoadTest.go @@ -0,0 +1,157 @@ +package main + +import ( + "context" + "flag" + "fmt" + "github.com/gomodule/redigo/redis" + "math" + "math/rand" + "sync" + "time" +) + +var ctx = context.Background() + +/** +buildTestData iterates through a goroutine of setRedis to build test data into the redis db +@param redisPtr connection to redis +@param size int number of entries into the database +*/ +func buildTestData(rdbPtr *redis.Conn, size int) { + fmt.Println("Initializing Test Data...") + var waitGroup sync.WaitGroup + for i := 0; i < size; i++ { + key := i + value := "ThisIsATestStringThatShouldBeReplacedWithSomethingMoreRandomMaybeFromAnInputOrCSV" + go setRedis(rdbPtr, key, value, &waitGroup) + waitGroup.Add(1) + } + waitGroup.Wait() + fmt.Println("Database Initialized...") +} + +/** +getRateTest calculates closest value for requests per milliseconds from inputted requests per second +Then starts goroutines for getRedis at that interval +@param rdbPtr connection is a pointer to the redis connection +@param rate int requests per second for the test +@param keyMaxValue int max value to query the database and expect a return +*/ +func getRateTest(rdbPtr *redis.Conn, rate int, keyMaxValue int) { + x := int(math.Abs(60 / float64(rate) * 1000)) + rateLimiter := time.Tick(time.Millisecond * time.Duration(x)) + + fmt.Println("Starting Test at", rateLimiter, "milliseconds per request...") + //todo add end to test + for { + <-rateLimiter + key := rand.Intn(keyMaxValue) + go getRedis(rdbPtr, key) + } + fmt.Println("Rate test concluded...") +} + +/** +getBurstTest starts goroutines for getRedis at the burstRateLimit per second +@param rdbPtr connection is a pointer to the redis connection +@param rate int requests per second for the test +@param keyMaxValue int max value to query the database and expect a return +*/ +func getBurstTest(rdbPtr *redis.Conn, rate int, keyMaxValue int) { + burstRateLimiter := time.Tick(time.Second) + //todo add end to test + fmt.Println("Starting burst test at", rate, "requests per second...") + for { + <-burstRateLimiter + for i := 0; i < rate; i++ { + key := rand.Intn(keyMaxValue) + go getRedis(rdbPtr, key) + } + } + fmt.Println("Burst test concluded...") +} + +/** +setRateTest sets key/value into database +@param rdbPtr connection is a pointer to the redis connection +@param key int redis key for the storage +@param value string string for the value to store +*/ +func setRedis(rdbPtr *redis.Conn, key int, value string, waitGroup *sync.WaitGroup) { + rdb := *rdbPtr + _, err := rdb.Do("Set", key, value) + if err != nil { + defer waitGroup.Done() + panic(err) + } + defer waitGroup.Done() +} + +/** +getRedis queries the redis database for value of key +@param rdbPtr connection is a pointer to the redis connection +@param key int is key value to query in database +*/ +func getRedis(rdbPtr *redis.Conn, key int) { + rdb := *rdbPtr + _, err := rdb.Do("Get", key) + if err != nil { + fmt.Println("Unhandled error:", err) + } +} + +/** +newPool builds redis database connection +@param host string host:port of redis server +returns redis.Connection of connection to database +returns err from database connection +*/ +func newPool(host string) *redis.Pool { + return &redis.Pool{ + MaxIdle: 80, + MaxActive: 12000, + Dial: func() (redis.Conn, error) { + c, err := redis.Dial("tcp", host) + if err != nil { + panic(err.Error()) + } + return c, err + }, + } +} + +func main() { + hostPtr := flag.String("host", "", "Redis Server FQDN:port") + //usernamePtr := flag.String("username", "", "Redis Server username") + //passwordPtr := flag.String("password", "", "Redis user password") + //dbPtr := flag.Int("db", 0, "Redis db, default 0") + initializeDBPtr := flag.Bool("initialize", false, "Boolean initialize db, default false") + ratePtr := flag.Int("rate", 50, "Test rate limit, default 50/sec") + burstPtr := flag.Bool("burst", true, "Boolean burst test default true") + //todo this should be off that same csv rather than an input + maxEntriesPtr := flag.Int("dbEntries", 50000, "Test rate limit, default 50/sec") + + host := *hostPtr + //todo add username/password support + //username := *usernamePtr + //password := *passwordPtr + //db := *dbPtr + initializeDB := *initializeDBPtr + rate := *ratePtr + burst := *burstPtr + keyMaxValue := *maxEntriesPtr + + pool := newPool(host) + client := pool.Get() + defer client.Close() + + if initializeDB { + buildTestData(&client, 50000) + } + getRateTest(&client, rate, keyMaxValue) + if burst { + getBurstTest(&client, rate, keyMaxValue) + } + fmt.Println("Tests completed...") +}