mita2 database life

主にMySQLに関するメモです

go-sysbench のカスタムシナリオが書きやすくなりました

go-sysbench

go-sysbench は Go 言語で書かれた、sysbench のクローンです。複雑なカスタムシナリオを sysbench より簡単に作れることを目的として作りました。

github.com

sysbench はシンプルで、非常に使いやすいベンチマークツールで、長年愛用してきました。Lua 言語でカスタムシナリオを書くこともできます。 ただ、Lua 言語で複雑なカスタムシナリオを書くのは難しいと感じてました(Lua 言語に慣れていないということもあると思います)。 そこで、使い勝手や出力は sysbench のまま、より柔軟性の高い Go言語で sysbench のクローンを作りました。

$ go-sysbench --help
Usage:
  go-sysbench [options]... [oltp_read_only|oltp_read_write] [prepare|run]

Application Options:
      --threads=                        number of threads to use (default: 1)
      --events=                         limit for total number of events (default: 0)
      --time=                           limit for total execution time in seconds (default: 10)
      --report-interval=                periodically report intermediate statistics with a specified interval in seconds. 0 disables intermediate reports (default: 0)
      --histogram=[on|off]              print latency histogram in report (default: off)
      --percentile=                     percentile to calculate in latency statistics (1-100) (default: 95)
      --tables=                         number of tables (default: 1)
      --table_size=                     number of rows per table (default: 10000)
      --table-size=                     alias of --table_size
      --db-driver=[mysql|pgsql|spanner] specifies database driver to use (default: mysql)
      --db-ps-mode=[auto|disable]       prepared statements usage mode (default: auto)
      --version                         show version

MySQL:
      --mysql-host=                     MySQL server host (default: localhost)
      --mysql-port=                     MySQL server port (default: 3306)
      --mysql-user=                     MySQL user (default: sbtest)
      --mysql-password=                 MySQL password [$MYSQL_PWD]
      --mysql-db=                       MySQL database name (default: sbtest)
      --mysql-ssl=[on|off]              use SSL connections (default: off)
      --mysql-ignore-errors=            list of errors to ignore, or "all" (default: 1213,1020,1205)

PostgreSQL:
      --pgsql-host=                     PostgreSQL server host (default: localhost)
      --pgsql-port=                     PostgreSQL server port (default: 5432)
      --pgsql-user=                     PostgreSQL user (default: sbtest)
      --pgsql-password=                 PostgreSQL password [$PGPASSWORD]
      --pgsql-db=                       PostgreSQL database name (default: sbtest)
      --pgsql-ssl=[on|off]              use SSL connections (default: off)
      --pgsql-ignore-errors=            list of errors to ignore, or "all" (default: 40P01,23505,40001)

Spanner:
      --spanner-project=                Spanner Google Cloud project name
      --spanner-instance=               Spanner instance id
      --spanner-db=                     Spanner database name (default: sbtest)

Help Options:
  -h, --help                            Show this help message

今回修正した内容

go-sysbench の初期実装は、シナリオとベンチマーカー本体のロジックが十分整理できておらず、少しカスタムしにくい実装でした。 今回、インターフェイスを整理して、完全に独立したライブラリとして使えるようにしました。

使い方

github.com/samitani/go-sysbench を import して、計測したい処理だけ記述すればOKです。

  • Init()でDBの接続など、準備を行い、Event() 関数に計測したい処理を記述します。
  • Event() 関数は Read/Write/Others/IgnoreError 数を返す必要があります、これらの数は集計され、ベンチマーク結果に出力されます。
  • Event() 関数が1回実行されると、1回トランザクションが成功したとみなされます(結果のtransactions がインクリメントされます)。
package main

import (
        "context"
        "fmt"
        "os"

        "database/sql"

        _ "github.com/go-sql-driver/mysql"

        "github.com/samitani/go-sysbench"
)

type CustomBenchmark struct {
        db *sql.DB
}

// when Runner.Prepare(), Runner.Run() is called, Init() is called once in advance.
func (b *CustomBenchmark) Init(ctx context.Context) error {
        db, err := sql.Open("mysql", "root:password@/my_database")
        if err != nil {
                return err
        }

        err = db.Ping()
        if err != nil {
                return err
        }

        b.db = db
        return nil
}

// when Runner.Prepare(), Runner.Run() is called, Done() is called once at the end.
func (b *CustomBenchmark) Done() error {
        b.db.Close()
        return nil
}

// when Runner.Prepare() is called, Prepare() is called once.
func (b *CustomBenchmark) Prepare(ctx context.Context) error {
        // nothing to do
        return nil
}

// when Runner.Run() is called, PreEvent() is called once before event loop.
func (b *CustomBenchmark) PreEvent(ctx context.Context) error {
        // nothing to do
        return nil
}

// when Runner.Run() is called, Event() is called in a loop
func (b *CustomBenchmark) Event(ctx context.Context) (numReads, numWrites, numOthers, numIgnoredErros uint64, err error) {
        var readCount uint64 = 0
        var writeCount uint64 = 0

        // something you want to measure
        for i := 0; i < 5; i++ {
                rows, err := b.db.QueryContext(ctx, "SELECT NOW()")
                if err != nil {
                        return readCount, 0, 0, 0, err
                }
                defer rows.Close()

                // fetch rows from server
                for rows.Next() {
                }

                readCount = readCount + 1
        }

        return readCount, writeCount, 0, 0, nil
}

func main() {
        bench := &CustomBenchmark{}

        r := sysbench.NewRunner(&sysbench.RunnerOpts{
                Threads:        10,
                Events:         0,
                Time:           60,
                ReportInterval: 1,
                Histogram:      "on",
                Percentile:     95,
        }, bench)

        if err := r.Run(); err != nil {
                fmt.Println(err)
                os.Exit(1)
        }
}

そして、結果は見慣れた sysbench の形式で出力されます。

$ ./main
Running the test with following options:
Number of threads: 10
Report intermediate results every 1 second(s)

[ 1s ] thds: 10 tps: 5072.00 qps: 25360.00 (r/w/o: 25360.00/0.00/0.00) lat (ms,95%): 2.91 err/s 0.00 reconn/s: N/A
[ 2s ] thds: 10 tps: 5162.00 qps: 25810.00 (r/w/o: 25810.00/0.00/0.00) lat (ms,95%): 2.91 err/s 0.00 reconn/s: N/A
[ 3s ] thds: 10 tps: 5180.00 qps: 25900.00 (r/w/o: 25900.00/0.00/0.00) lat (ms,95%): 2.86 err/s 0.00 reconn/s: N/A
[ 4s ] thds: 10 tps: 5387.00 qps: 26935.00 (r/w/o: 26935.00/0.00/0.00) lat (ms,95%): 2.76 err/s 0.00 reconn/s: N/A
[ 5s ] thds: 10 tps: 5340.00 qps: 26700.00 (r/w/o: 26700.00/0.00/0.00) lat (ms,95%): 2.86 err/s 0.00 reconn/s: N/A
[ 6s ] thds: 10 tps: 5199.00 qps: 25995.00 (r/w/o: 25995.00/0.00/0.00) lat (ms,95%): 2.86 err/s 0.00 reconn/s: N/A
[ 7s ] thds: 10 tps: 5422.00 qps: 27110.00 (r/w/o: 27110.00/0.00/0.00) lat (ms,95%): 2.76 err/s 0.00 reconn/s: N/A
[ 8s ] thds: 10 tps: 5193.00 qps: 25965.00 (r/w/o: 25965.00/0.00/0.00) lat (ms,95%): 2.86 err/s 0.00 reconn/s: N/A
[ 9s ] thds: 10 tps: 5283.00 qps: 26415.00 (r/w/o: 26415.00/0.00/0.00) lat (ms,95%): 2.81 err/s 0.00 reconn/s: N/A
<snip>
Latency histogram (values are in milliseconds)
       value  ------------- distribution ------------- count
       0.127 |                                         1
       0.312 |                                         1
       0.318 |                                         1
       0.448 |                                         1
       0.546 |                                         1
       0.576 |                                         1
SQL statistics:
    queries performed:
        read:                            1572640
        write:                           0
        other:                           0
        total:                           1572640
    transactions:                        314535 (5242.20 per sec.)
    queries:                             1572640 (26210.44 per sec.)
    ignored errors:                      0      (0.00 per sec.)
    reconnects:                          N/A    (N/A per sec.)

General statistics:
    total time:                          60.0005s
    total number of events:              314535

Latency (ms):
         min:                                    0.13
         avg:                                    1.91
         max:                                    8.12
         95th percentile:                        2.86
         sum:                               599624.51

Threads fairness (Event distribution by threads):
    events (avg/stddev):           31453.5000/69.05
    execution time (avg/stddev):   59.9625/0.00

より、実用的な例は oltp_read_only, oltp_read_write の実装をみてください。