Load testing with random data per request in Vegeta

2021-03-02

負荷試験ツール Vegeta で毎回ランダムな値を使って HTTP POST する方法のメモ

Vegeta 自体はコマンドラインからも使えます。 よくある jq で生成した JSON を食わせるワンライナーがベースだと、ランダムな値を生成しつつ変数展開する方法がわからなかったので今回はライブラリとして使ってなんとかします。

README にあるサンプルでは vegeta.NewStaticTargeter() を利用して targeter を生成しています。

// https://github.com/tsenart/vegeta#usage-library

func main() {
    rate := vegeta.Rate{Freq: 100, Per: time.Second}
    duration := 4 * 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)
}

この NewStaticTargeter は、引数として受け取った []vegeta.Target を呼び出しごとに先頭から順にループしながら、別のポインタに代入する Targeter を返します。 名前の通り Static なので、リクエストの中身はあらかじめ渡した vegeta.Target から変化しません。

仕組み上ここを置き換える何かだけで動きそうということがわかりました。

// commit ref: d73edf2bc2663d83848da2a97a8401a7ed1440bc
// https://github.com/tsenart/vegeta/blob/master/lib/targets.go#L106
type Targeter func(*Target) error

// https://github.com/tsenart/vegeta/blob/master/lib/targets.go#L212
func NewStaticTargeter(tgts ...Target) Targeter {
    i := int64(-1)
    return func(tgt *Target) error {
        if tgt == nil {
            return ErrNilTarget
        }
        *tgt = tgts[atomic.AddInt64(&i, 1)%int64(len(tgts))]
        return nil
    }
}

以下のように毎回 payload を作る genTargeter() を書き、これを NewStaticTargeter() の代わりに使うことで実行ごとに内容を変化させつつビッグバンアタックできます。 Target の生成コストがかかるのでもしかするとテスト自体のパフォーマンスに多少影響があるかもしれませんが特に考慮していません。

package main

import (
    "encoding/json"
    "net/http"

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

type requestBody struct {
    UUID  string `json:"uuid"`
    Value string `json:"value"`
}

func genTarget() vegeta.Target {
    u, _ := uuid.NewRandom()
    uuid := u.String()

    header := make(http.Header)
    header.Set("Content-Type", "application/json")

    encoded, _ := json.Marshal(requestBody{
        UUID:  uuid,
        Value: "some value",
    })

    return vegeta.Target{
        Method: "POST",
        URL:    "http://localhost:9100/",
        Body:   encoded,
        Header: header,
    }
}

func genTargeter() vegeta.Targeter {
    return func(tgt *vegeta.Target) error {
        *tgt = genTarget()
        return nil
    }
}

※負荷試験を行う際のリクエスト先やリクエスト数等には十分ご注意ください。このプログラムを使用して生じたいかなる損害・問題等について、筆者は一切の責任を負いかねます。