13.4K Views
November 23, 23
スライド概要
Golangを使ったDB(MySQL/PostgreSQL)用の負荷テストツールの作り方について発表します。
DB用の負荷テストツールはあまり多くはないのですが、並列処理を書きやすいGolangを使うことで、シンプルな負荷テストツールを簡単に作れることを紹介します。
https://gravatar.com/sgwrdts
Golangを使ったDB用負荷テストツールの開発 Go Conference mini 2023 Winter IN KYOTO Genki Sugawara
自己紹介 Genki Sugawara Kanmu, Inc., SRE github.com/winebarrel twitter.com/sgwr_dts Blog so-wh.at
DBの負荷テストについて
DB用の負荷テストツール(一部) • sysbench • mysqlslap • pgbench • HammerDB • JdbcRunner
DB負荷テストのやり方 • 定型のシナリオを実行する ◦ TPC、各ツールの独自シナリオ • 任意の数クエリを実行する ◦ mysqlslap --query 'SELECT 1' • 任意のシナリオを実行する ◦ HammarDB (TCL)
WebサービスでのDB負荷テスト • 定型のシナリオ ◦ サービスの特性と合わない • 数クエリを実行 ◦ サービスのSQLに対して少ない • 任意のシナリオを実行 ◦ シナリオを書くのが手間
WebサービスでのDB負荷テスト • クエリログをテストのシナリオに使う ◦ サービスのSQLを模倣できる • しかし、クエリのリピーターはあんまりない
DB用負荷テストツールを自作しよう
Goを使ったDB用負荷テストツール • goroutineで並列化しやすい • contextでgoroutineを制御しやすい • channelでgoroutine間のデータの受け渡しがやりやすい、 • select/defaultループ処理に割り込みを入れやすい • 準標準パッケージにレート制限パッケージがある
サンプルツール github.com/winebarrel/qube • MySQL・PostgreSQL対応 • NDJSON形式のSQLをDBに実行
Demo
構成図 Recorder (goroutine) chan []DataPoint Report Start Agent (goroutine) SQL DB Data SQL (NDJSON)
コードの説明
main.go func main() { options := parseArgs() task := qube.NewTask(options) // タスクの実行 report, err := task.Run() if err != nil { log.Fatal(err) } // レポートの出力 report.Print(os.Stdout) }
タスクの開始
func (task *Task) Run() (*Report, error) {
// エージェントの作成
agents, rec, err := task.makeAgents()
// ...
eg, ctx := errgroup.WithContext(context.Background())
ctx, cancel := context.WithCancel(ctx)
// タイムアウトの設定
if task.Time > 0 {
ctx, cancel = context.WithTimeout(ctx, task.Time)
}
// ...
fire := make(chan struct{})
for _, v := range agents {
agent := v
// errgroupでエージェントを実行
eg.Go(func() error {
<-fire
return agent.Start(ctx)
})
}
// errgroupでエージェントを待つ
close(fire)
err = eg.Wait()
}
エージェントの生成
func (task *Task) makeAgents() ([]*Agent, *Recorder, error) {
agents := make([]*Agent, task.Nagents)
// レコーダーとリミッターを生成
rec := NewRecorder(task.ID, task.Options)
limiter := rate.NewLimiter(rate.Limit(task.Rate), 1)
for i := 0; i < task.Nagents; i++ {
var err error
// agentにレコーダーとリミッターを渡す
agents[i], err = NewAgent(task.ID, i, task.Options, rec, limiter)
if err != nil {
return nil, nil, err
}
}
return agents, rec, nil
}
エージェント - SQLの実行
func (agent *Agent) Start(ctx context.Context) error {
for { // 無限ループでクエリ実行
agent.limiter.Wait(ctx)
// select/defaultでcontextの割り込み
// 一定時間ごとのデータ送信
select {
case <-ctx.Done():
break L
case <-tkrec.C:
agent.rec.Add(dps)
default:
}
// ファイルからSQLを読み込んで実行
q, err := agent.data.Next()
dur, err := agent.execQuery(ctx, q)
}
}
レコーダー - データポイントの収集
func (rec *Recorder) Start() {
push := func(dps []DataPoint) {
rec.Lock()
defer rec.Unlock()
rec.DataPoints = append(rec.DataPoints, dps...)
}
go func() {
// チャンネルからデータポイントを受信
for dps := range rec.ch {
push(dps)
}
}()
// ...
}
func (rec *Recorder) Add(dps []DataPoint) {
// チャンネルでデータポイントを送信
rec.ch <- dps
}
mysqlslapとの性能比較 mysqlslap (C) qube (Go) 100 75 50 25 0 1 2 4 8 16 24 32 40 48 56 64 並列数 110000 100000 90000 80000 70000 60000 50000 40000 30000 20000 10000 0 CPU使用率 qps 100 75 50 25 0 1 2 4 8 16 24 32 40 48 56 64 並列数 110000 100000 90000 80000 70000 60000 50000 40000 30000 20000 10000 0 CPU使用率 qps
まとめ • Golangを使うと簡単にDB用の負荷テストツールを作れます • 同じやり方で別のミドルウェアの負荷テストツールも作れると思います • 簡単なので、自作負荷テストツールおすすめです