Vegetaで自社サービスを攻撃する（負荷テスト）

# はじめに

この記事は[Vegeta](https://github.com/tsenart/vegeta)という負荷テストツールに興味がある方向けの記事となっています。この記事を読むと、CLIでの実行方法とVegetaをライブラリとして利用して好きな処理に組み込むところまでがわかります。ギャリック砲から始まりビッグバンアタックを放つまでくらいの感じ？です。（すいません、うまく例えようと思いましたが思いつきませんでした）

`vegeta attack`コマンドで負荷をかけれるのですが、不思議とエンターキーを押すときに力が入ります。

# 経緯
[Vantiq](https://vantiq.co.jp)という会社で働いているのですが、そのままの名前のVantiqというサービスを扱っています。この記事の本筋とズレるので細かい話は省きますが、簡単に言うとストリームデータを好きなように統合・加工・集計しながらその結果を好きな方法で別システムに連携したりして使うようなサービスです。

そもそも大量のストリームデータを捌く前提のサービスなので負荷をかけようとしても負荷テストツール側が先にダウンすることが良くあるため、なんとかテストできそうなものがないか探していました。後はシンプルに使えるものが良いなと思っていました。

そんな中、今回`Vegeta`を使ってみたのは以下の理由からです。
- **送信内容を動的に変えることができること**
    - 特にIDなど連番にしたりユニークにしたりしたくなると思います
- **テスト期間と、１秒あたりのリクエスト数が指定できること**
- **分散して実行できること**
    - vegetaは複数のマシンにインストールして一斉に攻撃`vegeta attack`できる
    - 詳細はこちら→[Distributed Attack](https://github.com/tsenart/vegeta#usage-distributed-attacks)
- **セットアップから結果の表示まで簡単にできること**

この辺りが叶います。
VegetaはGoで書かれており、ライブラリとしても使用できます。複雑なことをやりたい時は、Goを書く必要がありますが、IDを連番にするくらいだったらGoの経験がない私でも簡単にできました。

Vegetaは`CLIで使う方法`と上記の`ライブラリとして使う方法`がありますのでそれぞれ見ていきます。

> M1 Mac(Ventra ver. 13.1)で作業してます

## CLIで使う

まずは、CLIの方から。インストールは`brew update && brew install vegeta`。

基本形はこんな感じです。`vegeta attack`という放ちたくなるコマンドです。
```sh
vegeta attack -rate=10 -duration=5s -targets=targets.txt > result.bin | vegeta report 
```
|オプション|内容|
|-|-|
|-rate|秒間あたりのリクエスト数|
|-duration|リクエストし続ける時間（テストする時間）|
|-targets|負荷をかける相手と送信内容の情報（詳細は後述）|

上の例では、1秒に10リクエストを5秒間続けて行う、ということになります。

`result.bin`にはテストの結果が入っています。`vegeta report`でその結果を表示します。

`targets.txt`も例がないとわかりにくいので書いてみます。
```txt:targets.txt
POST https://your-site.com
Content-Type: application/json
Authorization: Bearer xxxxxx
@events.json
```
メソッド、送信先、ヘッダーの中身などリクエストに必要な情報をこのフォーマットで記載します。JSONでも書けますがその方法はここでは割愛します。

送信時のBodyの内容がここでは`@events.json`となります。今回用意した`events.json`の中身はこちらです。このJSONオブジェクトをBodyに含んだリクエストが送信されることになります。
```json:events.json
{
    "id": 1
}
```
ここまでできたら後はコマンドを放つのみです。
````sh
vegeta attack -rate=10 -duration=5s -targets=targets.txt > result.bin | vegeta report 
````
```:結果
Requests      [total, rate, throughput]         50, 10.20, 9.95
Duration      [total, attack, wait]             5.027s, 4.9s, 127.398ms
Latencies     [min, mean, 50, 90, 95, 99, max]  126.304ms, 145.723ms, 128.136ms, 163.354ms, 266.097ms, 466.085ms, 466.085ms
Bytes In      [total, mean]                     200, 4.00
Bytes Out     [total, mean]                     750, 15.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:50  
Error Set:
```
こんな形でテスト結果がわかります。この表示方法だけでなくHTMLやJSONで出したり色々できますので興味のある方はチェックしてみてください。


CLI編の最後にシェルスクリプトを用意してみました。使う場合は先のtargets.txt、events.jsonと合わせて使ってください。`REQUEST_RATE_SEC`と`DURATION`は適宜、必要な値にしてください。

:::note alert
送信先、送信量の設定はくれぐれもご注意ください
:::

```shell:attack_vantiq.sh

REQUEST_RATE_SEC=1000
DURATION=5s
RESULT_DIR="results/"

if [ ! -d $RESULT_DIR ]; then
    mkdir $RESULT_DIR
fi

echo "Attack Vantiq with $REQUEST_RATE_SEC request/sec for $DURATION"

vegeta attack \
-rate=$REQUEST_RATE_SEC \
-duration=$DURATION \
-targets=targets.txt \
> "${RESULT_DIR}result.bin"

echo "Done"
cat "${RESULT_DIR}result.bin" | vegeta report
cat "${RESULT_DIR}result.bin" | vegeta report -type=text > "${RESULT_DIR}result.txt"
cat "${RESULT_DIR}result.bin" | vegeta plot > "${RESULT_DIR}plot.html"
```

`results/`ディレクトリに結果（result.txt, plot.html）が格納されます。
実物も貼っておきます。result.txtは先ほどと同じ内容です。
```shell:result.txt
Requests      [total, rate, throughput]         5000, 1000.25, 816.11
Duration      [total, attack, wait]             6.127s, 4.999s, 1.128s
Latencies     [min, mean, 50, 90, 95, 99, max]  104.173ms, 255.733ms, 190.052ms, 430.111ms, 559.268ms, 1.551s, 3.489s
Bytes In      [total, mean]                     20000, 4.00
Bytes Out     [total, mean]                     75000, 15.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:5000  
Error Set:
```
**plot.html**
![vegeta-plot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/45b22acc-613a-07ae-27dd-7f19dd14f948.png)

## ライブラリとして使う
ライブラリとしても使えます。Goで実装されてますのでGoで書いてみます。

送る内容はこちら↓
```json
{
   "sensor_id": 1 ←ここは連番になる
}
```

ソースコードはこちらです。

```golang:attack_vantiq.go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"sync/atomic"
	"time"

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

// Configurations
const (
	HOST              = "https://your-site.com"
	TOKEN             = "Bearer xxxxxx"
	REQUEST_FREQ_SEC  = 2000
	LOAD_DURATION_SEC = 10
)

func main() {
	// Define target
	target := func(id uint64) vegeta.Targeter {
		type entity struct {
			ID uint64 `json:"sensor_id"`
		}
		return func(t *vegeta.Target) (err error) {
			t.Method = "POST"
			t.URL = HOST

			t.Body, err = json.Marshal(&entity{
				ID: atomic.AddUint64(&id, 1),
			})
			t.Header = http.Header{
				"Content-Type":  []string{"application/json"},
				"Authorization": []string{TOKEN},
			}
			return err
		}
	}(0)
	// Create attacker
	rate := vegeta.Rate{Freq: REQUEST_FREQ_SEC, Per: time.Second}
	duration := LOAD_DURATION_SEC * time.Second
	var metrics vegeta.Metrics
	attacker := vegeta.NewAttacker()
	// Attack
	fmt.Print("Attacking Vantiq...\n")
	for res := range attacker.Attack(target, rate, duration, "Big Bang!") {
		metrics.Add(res)
	}
	fmt.Print("Done!\n")
	metrics.Close()
	// Show results
	fmt.Print("- Show results -\n")
	fmt.Printf("Requests      [total, rate, throughput]: [%d, %0.2f, %0.2f]\n",
		metrics.Requests, metrics.Rate, metrics.Throughput)
	fmt.Printf("Duration      [total, attack, wait]: [%s, %s, %s]\n",
		metrics.Duration.String(), metrics.Duration-metrics.Wait, metrics.Wait)
	fmt.Printf("Latency       [mean, P50, P95, P99, max]: [%s, %s, %s, %s, %s]\n",
		metrics.Latencies.Mean, metrics.Latencies.P50, metrics.Latencies.P95, metrics.Latencies.P99, metrics.Latencies.Max)
	fmt.Printf("Status Codes  [code:count]: ")
	for code, count := range metrics.StatusCodes {
		fmt.Printf("[%s:%d]", code, count)
	}
	fmt.Println()
}
```

:::note alert
繰り返しですが、もし使う場合は送信先と送信量の設定にご注意ください。
（上のソースコードでは秒間2000リクエスト送るようになっています）
:::

ここの部分は好きに変更してください、
```golang
const (
	HOST              = "https://your-site.com"
	TOKEN             = "Bearer xxxxxx"
	REQUEST_FREQ_SEC  = 2000
	LOAD_DURATION_SEC = 10
)
```

>このソースコードはこちらを参考にして書いてみました
[Usage (Library)](https://github.com/tsenart/vegeta#usage-library)
[[QUESTION] Sending requests with dynamic body](https://github.com/tsenart/vegeta/issues/330)


実行までの手順はこちらです。
```shell
go mod init <YOUR DIR>
go get -u github.com/tsenart/vegeta
go run ./attack_vantiq.go
```
実行結果
```
Attacking Vantiq...
Done!
- Show results -
Requests      [total, rate, throughput]: [20000, 2000.02, 1973.39]
Duration      [total, attack, wait]: [9.999898917s, 9.864941084s, 134.957833ms]
Latency       [mean, P50, P95, P99, max]: [232.234749ms, 131.723067ms, 715.269472ms, 1.070837017s, 1.841236917s]
Status Codes  [code:count]: [200:20000]
```

`Status Codes  [code:count]: [200:20000]`はステータスコード200が20000回ということですので、特にエラーもなく2000リクエスト * 10秒のリクエストを受け切ったようです。

# まとめ

今回は基本的な使い方を書いた時点で長くなってしまったのでここまでにします。この記事の内容はほんの一部で分散アタックなど他にもできることがたくさんあります。また試していきたいと思います。