LoginSignup
0
0

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

Last updated at Posted at 2023-12-19

この記事は カバー株式会社 Advent Calendar 2023 の20日目です。


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

本記事では、 tsenart/vegeta をライブラリとしてGoで用いての負荷試験について記載します。

前回の記事は @kura_cvr による ホロライブアプリのアーキテクチャ その3 ~これまでとこれから~ です。こちらも是非ご覧ください。

はじめに

tsenart/vegeta は、HTTP負荷テストツールです。
CLIおよびGoライブラリとして利用可能です。

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

Vegetaの利用

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

go get github.com/tsenart/vegeta

CLIとして利用する場合

使ってみる

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

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)
}

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

❯ 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 を出力してみます。

vegeta.NewTextReporter(&metrics).Report(os.Stdout)
❯ 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 を参照してください。

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-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を読み込む

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コマンド 等で利用することができます。

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()
}
❯ 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 による、 AWS CDKで定義したAレコードを変更する です。
こちらも是非ご覧ください。

参考文献

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0