feat: Initial release

Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
Julien Riou 2022-10-09 09:25:28 +02:00
parent 37d5c2b1f7
commit d1e3da5189
No known key found for this signature in database
GPG key ID: FF42D23B580C89F7
13 changed files with 442 additions and 0 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
bin bin
config.yml

18
Makefile Normal file
View file

@ -0,0 +1,18 @@
APPVERSION := $(shell cat ./VERSION)
GOVERSION := $(shell go version | awk '{print $$3}')
GITCOMMIT := $(shell git log -1 --oneline | awk '{print $$1}')
LDFLAGS = -X main.AppVersion=${APPVERSION} -X main.GoVersion=${GOVERSION} -X main.GitCommit=${GITCOMMIT}
PLATFORM := $(shell uname -s)
ARCH := $(shell uname -m)
.PHONY: clean
build:
go build -ldflags "${LDFLAGS}" -o bin/benchito *.go
release:
go build -ldflags "${LDFLAGS}" -o bin/benchito-${APPVERSION}-${PLATFORM}-${ARCH} *.go
sha256sum bin/benchito-${APPVERSION}-${PLATFORM}-${ARCH}
clean:
rm -rf bin

36
README.md Normal file
View file

@ -0,0 +1,36 @@
# benchito
Like [pgbench](https://www.postgresql.org/docs/current/pgbench.html) or [sysbench](https://github.com/akopytov/sysbench) but only for testing maximum number of connections. `benchito` will start multiple threads to issue very simple queries in order to avoid CPU or memory starvation.
## Requirements
* `make`
* go 1.18
## Setup
Compile the `benchito` binary:
```
make
```
Start a PostgreSQL instance:
```
docker-compose pull
docker-compose up -d
```
## Usage
```
./bin/benchito -help
```
## Cleanup
```
docker-compose down -v
```

1
VERSION Normal file
View file

@ -0,0 +1 @@
1.0.0

53
benchmark.go Normal file
View file

@ -0,0 +1,53 @@
package main
import (
"sync"
"time"
)
// Benchmark to store benchmark state
type Benchmark struct {
duration time.Duration
connections int
databases []*Database
}
// NewBenchmark connects to the database then creates a Benchmark struct
func NewBenchmark(connections int, duration time.Duration, driver string, dsn string, query string, reconnect bool) (*Benchmark, error) {
var databases []*Database
for i := 0; i < connections; i++ {
database, err := NewDatabase(driver, dsn, query, reconnect)
if err != nil {
return nil, err
}
databases = append(databases, database)
}
return &Benchmark{
duration: duration,
connections: connections,
databases: databases,
}, nil
}
// Run performs the benchmark by runing queries for a duration
func (b *Benchmark) Run() {
wg := new(sync.WaitGroup)
wg.Add(b.connections)
for _, database := range b.databases {
go database.Run(b.duration, wg)
}
wg.Wait()
}
// Queries returns the number of executed queries during the benchmark
func (b *Benchmark) Queries() (queries float64) {
for _, database := range b.databases {
queries = queries + database.Queries()
}
return
}
// QueriesPerSecond returns the number of executed queries per second during the benchmark
func (b *Benchmark) QueriesPerSecond() float64 {
return b.Queries() / b.duration.Seconds()
}

110
config.go Normal file
View file

@ -0,0 +1,110 @@
package main
import (
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"time"
"github.com/go-sql-driver/mysql"
"gopkg.in/yaml.v2"
)
// Config to store all configurations (from command line, from file, etc)
type Config struct {
Driver string `yaml:"driver"`
Connections int `yaml:"connections"`
Query string `yaml:"query"`
Duration time.Duration `yaml:"duration"`
Reconnect bool `yaml:"reconnect"`
DSN string `yaml:"dsn"`
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
Password string `yaml:"password"`
Database string `yaml:"database"`
TLS string `yaml:"tls"`
ConnectTimeout int `yaml:"connect_timeout"`
}
// NewConfig creates a Config struct
func NewConfig() *Config {
return &Config{}
}
// Read YaML configuration file from disk
func (c *Config) Read(file string) error {
file, err := filepath.Abs(file)
if err != nil {
return err
}
yamlFile, err := ioutil.ReadFile(file)
if err != nil {
return err
}
err = yaml.Unmarshal(yamlFile, &c)
if err != nil {
return err
}
return nil
}
// ParseDSN detects the database driver then creates the DSN accordingly
func (c *Config) ParseDSN() {
if c.DSN == "" {
switch c.Driver {
case "postgres":
c.DSN = c.parsePostgresDSN()
case "mysql":
c.DSN = c.parseMysqlDSN()
}
}
}
func (c *Config) parsePostgresDSN() string {
var parameters []string
if c.Host != "" {
parameters = append(parameters, fmt.Sprintf("host=%s", c.Host))
}
if c.Port != 0 {
parameters = append(parameters, fmt.Sprintf("port=%d", c.Port))
}
if c.User != "" {
parameters = append(parameters, fmt.Sprintf("user=%s", c.User))
}
if c.Password != "" {
parameters = append(parameters, fmt.Sprintf("password=%s", c.Password))
}
if c.Database != "" {
parameters = append(parameters, fmt.Sprintf("database=%s", c.Database))
}
if c.ConnectTimeout != 0 {
parameters = append(parameters, fmt.Sprintf("connect_timeout=%d", c.ConnectTimeout))
}
if AppName != "" {
parameters = append(parameters, fmt.Sprintf("application_name=%s", AppName))
}
if c.TLS != "" {
parameters = append(parameters, fmt.Sprintf("sslmode=%s", c.TLS))
}
return strings.Join(parameters, " ")
}
func (c *Config) parseMysqlDSN() (dsn string) {
config := mysql.NewConfig()
config.Addr = c.Host
if c.Port != 0 {
config.Addr += fmt.Sprintf(":%d", c.Port)
}
config.User = c.User
config.Passwd = c.Password
config.DBName = c.Database
config.TLSConfig = c.TLS
return config.FormatDSN()
}

91
database.go Normal file
View file

@ -0,0 +1,91 @@
package main
import (
"database/sql"
"log"
"sync"
"time"
)
// Database to store a single connection to the database and its statistics
type Database struct {
db *sql.DB
driver string
dsn string
query string
reconnect bool
queries float64
}
// NewDatabase creates a single connection to the database then returns the struct
func NewDatabase(driver string, dsn string, query string, reconnect bool) (*Database, error) {
database := &Database{
driver: driver,
dsn: dsn,
query: query,
reconnect: reconnect,
}
err := database.connect()
if err != nil {
return nil, err
}
return database, nil
}
func (d *Database) connect() error {
db, err := sql.Open(d.driver, d.dsn)
if err != nil {
return err
}
if err = db.Ping(); err != nil {
return err
}
d.db = db
return nil
}
func (d *Database) disconnect() error {
if d.db != nil {
return d.db.Close()
}
return nil
}
// Run to perform benchmark on a single connection for a duration
func (d *Database) Run(duration time.Duration, wg *sync.WaitGroup) error {
defer wg.Done()
defer d.disconnect()
// Run single benchmark
start := time.Now()
end := start.Add(duration)
for {
if end.Before(time.Now()) {
break
}
result, err := d.db.Query(d.query)
if err != nil {
log.Fatalf("Failed query the database: %v", err)
}
if err = result.Close(); err != nil {
log.Fatalf("Failed to close query: %v", err)
}
d.queries++
if d.reconnect {
if err = d.disconnect(); err != nil {
return err
}
if err = d.connect(); err != nil {
return err
}
}
}
return nil
}
// Queries returns the number of performed queries in the benchmark
func (d *Database) Queries() float64 {
return d.queries
}

32
docker-compose.yml Normal file
View file

@ -0,0 +1,32 @@
---
version: "3"
services:
postgresql:
container_name: benchito-postgresql
env_file:
- ./postgresql.env
image: postgres:14
ports:
- "5432:5432"
volumes:
- benchito-postgresql:/var/lib/postgresql/data
restart: always
mysql:
container_name: benchito-mysql
env_file:
- ./mysql.env
image: mysql:8.0
ports:
- "3306:3306"
volumes:
- benchito-mysql:/var/lib/mysql
restart: always
volumes:
benchito-postgresql:
driver: local
benchito-mysql:
driver: local

10
go.mod Normal file
View file

@ -0,0 +1,10 @@
module benchito
go 1.18
require (
github.com/go-sql-driver/mysql v1.6.0
github.com/lib/pq v1.10.7
)
require gopkg.in/yaml.v2 v2.4.0

8
go.sum Normal file
View file

@ -0,0 +1,8 @@
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

73
main.go Normal file
View file

@ -0,0 +1,73 @@
package main
import (
"flag"
"fmt"
"log"
"time"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
)
// AppName to store application name
var AppName string = "benchito"
// AppVersion to set version at compilation time
var AppVersion string = "9999"
// GitCommit to set git commit at compilation time (can be empty)
var GitCommit string
// GoVersion to set Go version at compilation time
var GoVersion string
func main() {
config := NewConfig()
version := flag.Bool("version", false, "Print version and exit")
configFile := flag.String("config", "", "Configuration file")
flag.StringVar(&config.Driver, "driver", "postgres", "Database driver (postgres or mysql)")
flag.IntVar(&config.Connections, "connections", 1, "Number of concurrent connections to the database")
flag.StringVar(&config.Query, "query", "SELECT /* "+AppName+" */ NOW();", "Query to execute for the benchmark")
flag.DurationVar(&config.Duration, "duration", 1*time.Second, "Duration of the benchmark")
flag.BoolVar(&config.Reconnect, "reconnect", false, "Force database reconnection between each queries")
flag.StringVar(&config.DSN, "dsn", "", "Database cpnnection string")
flag.StringVar(&config.Host, "host", "", "Host address of the database")
flag.IntVar(&config.Port, "port", 0, "Port of the database")
flag.StringVar(&config.User, "user", "", "Username of the database")
flag.StringVar(&config.Password, "password", "", "Password of the database")
flag.StringVar(&config.Database, "database", "", "Database name")
flag.StringVar(&config.TLS, "tls", "", "TLS configuration")
flag.Parse()
if *version {
showVersion()
return
}
if *configFile != "" {
err := config.Read(*configFile)
if err != nil {
log.Fatalf("Failed to read configuration file: %v", err)
}
}
config.ParseDSN()
benchmark, err := NewBenchmark(config.Connections, config.Duration, config.Driver, config.DSN, config.Query, config.Reconnect)
if err != nil {
log.Fatalf("Cannot perform benchmark: %v", err)
}
benchmark.Run()
log.Printf("Queries: %.0f", benchmark.Queries())
log.Printf("Queries per second: %.0f", benchmark.QueriesPerSecond())
}
func showVersion() {
if GitCommit != "" {
AppVersion = fmt.Sprintf("%s-%s", AppVersion, GitCommit)
}
fmt.Printf("%s version %s (compiled with %s)\n", AppName, AppVersion, GoVersion)
}

5
mysql.env Normal file
View file

@ -0,0 +1,5 @@
MYSQL_RANDOM_ROOT_PASSWORD=1
MYSQL_ONETIME_PASSWORD=1
MYSQL_DATABASE=benchito
MYSQL_USER=benchito
MYSQL_PASSWORD=benchito

4
postgresql.env Normal file
View file

@ -0,0 +1,4 @@
POSTGRES_PASSWORD=benchito
POSTGRES_USER=benchito
POSTGRES_DB=benchito
POSTGRES_INITDB_ARGS="--data-checksums"