Goとvegetaで行うHTTP負荷テスト


この記事は [カバー株式会社 Advent Calendar 2023](https://qiita.com/advent-calendar/2023/cover) の20日目です。

---

こんにちは。カバー株式会社エンジニアのDです。
よろしくお願いいたします。

本記事では、 [tsenart/vegeta](https://github.com/tsenart/vegeta) をライブラリとしてGoで用いての負荷試験について記載します。

前回の記事は [@kura_cvr](https://qiita.com/kura_cvr) による [ホロライブアプリのアーキテクチャ その3 ～これまでとこれから～](https://qiita.com/kura_cvr/items/67529e8e50b5283369b9) です。こちらも是非ご覧ください。

# はじめに

[tsenart/vegeta](https://github.com/tsenart/vegeta) は、HTTP負荷テストツールです。
CLIおよびGoライブラリとして利用可能です。

ライブラリとして利用する場合、ビルドしてワンバイナリにしてしまえば実行環境の用意が簡単になります。

# Vegetaの利用

ライブラリとして利用する場合

```
go get github.com/tsenart/vegeta
```

[CLIとして利用する場合](https://github.com/tsenart/vegeta#install)

# 使ってみる

READMEにUsageがあるので、まずはこちらを引用。
https://github.com/tsenart/vegeta#usage-library

```go
package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
	"time"

	vegeta "github.com/tsenart/vegeta/v12/lib"
)

func main() {
  port := ":9100"
  frequency := 5
	duration := time.Second * 5

	go func() {
		http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
			w.WriteHeader(http.StatusOK)
		})
		log.Fatal(http.ListenAndServe(port, nil))
	}()
	time.Sleep(time.Second)

  // Freq request / per Sec
	rate := vegeta.Rate{Freq: frequency, Per: time.Second}
	targeter := vegeta.NewStaticTargeter(vegeta.Target{
		Method: "GET",
		URL:    "http://localhost:9100",
	})
	attacker := vegeta.NewAttacker()

	var metrics vegeta.Metrics
	for res := range attacker.Attack(targeter, rate, duration, "Big Bang!") {
		metrics.Add(res)
	}
	metrics.Close()
	fmt.Printf("99th percentile: %s\n", metrics.Latencies.P99)
}
```

対象となるサーバも立ち上げています。
実行結果はこちら。

```shell
❯ go run main.go
99th percentile: 7.131042ms
```

99th percentileの出力に成功しました。

Vegetaは高機能なので、他にも色々出力が可能です。
metricsにある内容を出力すると確認ができます。
https://pkg.go.dev/github.com/tsenart/vegeta/lib#Metrics

Metricsの中身を掘って出力したり整形したりは面倒ですが、 `Reporter` を使えばいい感じに出力が可能となります。
(CLIで使う際の `report` コマンドと同様の出力が可能です。)

# Reporterを使って出力してみる

`report -type=text` を出力してみます。

```go
vegeta.NewTextReporter(&metrics).Report(os.Stdout)
```

```shell
❯ go run main.go
99th percentile: 3.805417ms
Requests      [total, rate, throughput]         25, 5.21, 5.21
Duration      [total, attack, wait]             4.801s, 4.801s, 930.709µs
Latencies     [min, mean, 50, 90, 95, 99, max]  239.334µs, 646.808µs, 506.25µs, 930.709µs, 1.818ms, 3.805ms, 3.805ms
Bytes In      [total, mean]                     0, 0.00
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:25  
Error Set:
```

先ほどよりもより詳細に結果を確認することができました。
他にもReporterはあるので、CLIの `report` コマンドと同等のことはできそうです。


# 過程を残す

vegetaを扱って負荷テストを行う過程を残す方法を記載します。

* ターゲットファイル（-format json）
* 実行結果（results.bin）

## ターゲットファイルの作成

vegetaは実行時に対象をファイルで渡すことが可能です。
そのファイルをGoで作成する方法になります。

フォーマットは [vegetaのREADME](https://github.com/tsenart/vegeta#-format) を参照してください。

```go
func generateTargetFile() {
  frequency := 5
  duration := time.Minute

  // ターゲットの準備
  targets := make([]vegeta.Target, 0, frequency*int(duration.Seconds()))
  for i := 0; i < frequency*int(duration.Seconds()); i++ {
    v := vegeta.Target{
      Method: "GET",
      URL: "http://localhost:9100",
      Header: http.Header{
        "Authorization": []string{fmt.Sprintf("Bearer %d", i)},
      },
    }
    targets = append(targets, v)
  }

  // ターゲットをファイルに保存
  var buf bytes.Buffer
  for _, t := range targets {
    b, err := json.Marshal(t)
    if err != nil {
      log.Fatalln(err)
    }
    buf.Write(b)
    buf.Write([]byte("\n"))
  }
  os.WriteFile("json-format-targets.txt", buf.Bytes(), 0755)
}
```

```json:json-format-targets.txt
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 0"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 1"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 2"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 3"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 4"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 5"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 6"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 7"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 8"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 9"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 10"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 11"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 12"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 13"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 14"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 15"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 16"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 17"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 18"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 19"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 20"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 21"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 22"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 23"]}}
{"method":"GET","url":"http://localhost:9100","header":{"Authorization":["Bearer 24"]}}
```

このファイルをvegetaのCLIでtargetに指定するなり、vegetaをライブラリ利用したツールで読み込むなりで、再利用が可能となります。

## targetを読み込む

```go
func newTargeterFromFile(fileName string) vegeta.Targeter{
  b, err := os.ReadFile(fileName)
  if err != nil {
    log.Fatalln(err)
  }
  return vegeta.NewJSONTargeter(bytes.NewReader(b), []byte{}, nil)
}
```

`vegeta.NewJSONTargeter` などを用いることで、適したフォーマットのファイルを読み込んで Targeterを作成することとが可能です。
このTargeterをAttackerに渡してあげればテストを実施できます。

## ライブラリ利用でも、実行結果をファイルに残す

実行結果をファイルに残しておけば、後で結果を分析する際に役立ちます。
結果ファイルは、CLIでの実行結果の出力と同じなので、 [vegetaのreportコマンド](https://github.com/tsenart/vegeta#report-command) 等で利用することができます。


```go
func main() {
	frequency := 5
	duration := time.Second * 5

  targeter := newTargeterFromFile("json-format-targets.txt")
  attacker := vegeta.NewAttacker()
  // 結果の書き込み先を準備
  out, err := os.OpenFile("result.bin", os.O_RDWR|os.O_CREATE, 0755)
  if err != nil {
    log.Fatalln(err)
  }
  defer out.Close()
  enc := vegeta.NewEncoder(out)

  var metrics vegeta.Metrics
  rate := vegeta.Rate{Freq: frequency, Per: time.Second}
  for res := range attacker.Attack(targeter, rate, dur, "Big Bang!") {
    metrics.Add(res)
    enc.Encode(res)
  }
  metrics.Close()
}
```

```shell
❯ vegeta report result.bin
Requests      [total, rate, throughput]         25, 5.21, 5.21
Duration      [total, attack, wait]             4.801s, 4.801s, 740.5µs
Latencies     [min, mean, 50, 90, 95, 99, max]  271.833µs, 958.885µs, 565.292µs, 838.334µs, 3.434ms, 10.969ms, 10.969ms
Bytes In      [total, mean]                     0, 0.00
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:25  
Error Set:
```

# おわりに

vegetaをライブラリで利用し、Golangで負荷テストを行ってみました。
vegetaは高機能で他にも出力方法があったり、分散実行したり、prometheusにexportしたりなど、様々な機能があります。
少しでもvegetaを利用する際やツールの選定時の参考になれば幸いです。

次回は [si-cover](https://qiita.com/si-cover) による、 `AWS CDKで定義したAレコードを変更する` です。
こちらも是非ご覧ください。

# 参考文献

* [tsenart/vegeta: HTTP load testing tool and library. It's over 9000!](https://github.com/tsenart/vegeta)
* [vegeta package - github.com/tsenart/vegeta/lib - Go Packages](https://pkg.go.dev/github.com/tsenart/vegeta/lib)