feat: Initial release
Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
parent
37d5c2b1f7
commit
d1e3da5189
13 changed files with 442 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
bin
|
bin
|
||||||
|
config.yml
|
18
Makefile
Normal file
18
Makefile
Normal 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
36
README.md
Normal 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
1
VERSION
Normal file
|
@ -0,0 +1 @@
|
||||||
|
1.0.0
|
53
benchmark.go
Normal file
53
benchmark.go
Normal 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
110
config.go
Normal 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
91
database.go
Normal 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
32
docker-compose.yml
Normal 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
10
go.mod
Normal 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
8
go.sum
Normal 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
73
main.go
Normal 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
5
mysql.env
Normal 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
4
postgresql.env
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
POSTGRES_PASSWORD=benchito
|
||||||
|
POSTGRES_USER=benchito
|
||||||
|
POSTGRES_DB=benchito
|
||||||
|
POSTGRES_INITDB_ARGS="--data-checksums"
|
Loading…
Add table
Reference in a new issue