gRPC + Locust +  boomerで負荷試験をしてみた

# はじめに

この記事は[ミクシィアドベントカレンダー2021](https://qiita.com/advent-calendar/2021/mixi) 8日目の記事です。

こんにちは。
サービスのリリース前や大きなイベント前には皆さんも負荷試験をしているかと思います。

今回はgRPCを使った新規サービスでboomerというツールを使って負荷試験をしたのですが、なかなか使い心地がよかったのでご紹介したいと思います。

またboomerの選定については先日までアルバイトで来て頂いていた方にメインやって頂いたのですが、非常に助かりました。
この場をお借り致しまして，お礼申し上げます。

# boomerとは

LocustのシナリオをGoで書けるWorker Runnerです。
LocustのWorkerとして動作するため、別途Locust Masterは必要になります。
gRPCに特化したツールというわけではありません。

https://github.com/myzhan/boomer

# ツールの選定理由

Locust + boomerは以下の理由で採用しました。

- [Locust](https://github.com/locustio/locust)
	- Web UI がついている
	- 社内で実績/知見がある
	- 情報が多い
	- Workerのスケールがしやすい
- [boomer](https://github.com/myzhan/boomer)
	- シナリオがGoで書ける
	- サーバーもGoなのでpb.go含めた既存コードを使いまわせる
  - k8s上でのmulti worker構成も問題なく動作する

# 使ってみる

それでは実際にgRPCサーバーに対して Locust + boomerで負荷試験を実施してみたいと思います。
今回は同一のマシン上にgRPC Server / Locust Master / boomer(Locust Worker) を立てて実行しています。

## バージョン

この記事では以下のバージョンを使っています。

|Tool  |Version  |
|---|---|
|Locust  |2.5.0|
|boomer  |1.6.1|

※ boomerの1.6.1より前のバージョンだとLocustの1系しか対応していないので注意です

## 今回使うコード
### proto

[公式のサンプル](https://grpc.io/docs/languages/go/quickstart/#update-the-grpc-service) とほとんど一緒です。

```proto:proto/hello.proto
syntax = "proto3";

option go_package = "pb/";

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
```

### サーバー

ランダムでsleepしてレスポンスを返すUnary RPCを定義したgRPCサーバーです。

```go:cmd/server/main.go
package main

import (
	"context"
	"log"
	"math/rand"
	"net"
	"time"

	"sample/boomer/internal/pb"
	"google.golang.org/grpc"
)

const (
	port = ":50051"
)

type server struct {
	pb.UnimplementedGreeterServer
}

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	r := rand.Intn(3)
	time.Sleep(time.Duration(r) * time.Second)

	return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
	lis, err := net.Listen("tcp", port)
	if err != nil {
		log.Fatalf("%+v", err)
	}

	s := grpc.NewServer()
	pb.RegisterGreeterServer(s, &server{})

	if err := s.Serve(lis); err != nil {
		log.Fatalf("%+v", err)
	}
}
```


### シナリオ

SayHelloのRPCを呼び出すだけの単純なシナリオになっています。
gRPCサーバーへのリクエストは特別なことはしておらず、通常のgRPC Clientと同じ方法で呼んでいます。

boomerが行う処理として、RPCの呼び出しに成功した場合はRecordSuccessを、失敗した場合はRecordFailureを呼び出してリクエストの結果を記録しています。

```go:cmd/boomer/main.go
package main

import (
	"context"
	"fmt"
	"github.com/myzhan/boomer"
	"google.golang.org/grpc"
	"log"
	"sample/boomer/internal/pb"
	"time"
)

const (
	target = "localhost:50051"
)

func hello() {
	ctx := context.Background()
	conn, err := grpc.DialContext(ctx, target, grpc.WithInsecure())
	if err != nil {
		log.Fatalf("%+v", err)
	}
	defer conn.Close()

	client := pb.NewGreeterClient(conn)

	// レスポンスタイムを計測するために、リクエストを投げる前の時刻を取得する
	start := time.Now()
	response, err := client.SayHello(ctx, &pb.HelloRequest{Name: "ミクシィ太郎"})

	if err != nil {
		// リクエストに失敗した場合はRecordFailureを呼びます
		boomer.RecordFailure(
			"grpc",     // リクエストの種別
			"SayHello", // rpc名
			time.Since(start).Nanoseconds()/int64(time.Millisecond), // レスポンスタイム
			fmt.Sprintf("failed: %+v", err),                         // エラー理由
		)
	} else {
		// リクエストに成功した場合はRecordSuccessを呼びます
		boomer.RecordSuccess(
			"grpc",     // リクエストの種別
			"SayHello", // rpc名
			time.Since(start).Nanoseconds()/int64(time.Millisecond), // レスポンスタイム
			int64(len(response.String())),                           // レスポンスサイズ
		)
	}
}

func main() {
	task := &boomer.Task{
		Name:   "sample",
		Weight: 10,
		Fn:     hello,
	}

	boomer.Run(task)
}
```

## 環境準備
サーバーとシナリオをビルドして実行ファイルを作成しておきます。

```sh
$ go build -o server ./cmd/server/main.go
$ go build -o boomer ./cmd/boomer/main.go
```

サーバーを起動します。

```sh
$ ./server
```

Locust Masterの起動にダミーのシナリオが必要なため、[dummy.py](https://github.com/myzhan/boomer/blob/master/dummy.py)を作成し、Locust Masterを起動します。

```sh
$ locust --master -f dummy.py
```

続いてboomerを起動します。

```sh
$ ./boomer
```

boomerに `boomer is connected to master(tcp://127.0.0.1:5557) press Ctrl+c to quit.`
Locust Masterに `xxxxx/INFO/locust.runners: Client 'xxxx' reported as ready. Currently 1 clients ready to swarm.` 
のようなログがでていればboomerの起動は成功です。

## Let's boomer

それでは準備ができたのでLocustのweb画面にアクセスして負荷をかけてみます。
Number of usersを10、Spawn rateを1にしてStart swarmingをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2334273/07be87dc-34d6-5303-f431-b851dcabd073.png)

シナリオに書いた通りSayHelloへのリクエストが記録されました。
SayHelloはランダムでSleepされる処理が入っていますが、レスポンスタイムもそれに合わせて記録されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2334273/73c08be3-3058-ecc8-f19c-57a303a48339.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2334273/ed9ed98f-5c61-325e-39f3-7a186d691b15.png)


今度はエラー時の動作を確認するため、SayHelloからエラーが返されるようにして再度負荷をかけてみます。
SayHelloからエラーが返されているため、Failsのカウントが増加していることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2334273/43ad2478-26c2-fcd5-429e-902b84fd1d09.png)

また、Failuresタブを開くとエラー理由が記録されていることが確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2334273/87fe7897-2b38-6dd9-5efb-678701b7a1e4.png)


# まとめ

このようにboomerを使うことでGoでシナリオを書け、gRPCサーバーへの負荷試験を行うことができました。
(もちろん通常のhttpサーバーへの負荷試験もできます）
Pythonを使った場合でも簡単にシナリオは書けますし同様のことはできますが、サーバーでGoを使っている場合は同じ言語でシナリオを書けるのはメリットだと思いますので、是非boomerを使ってみてください。

また今回はboomerの紹介のみとなりましたが、今度はboomerを使って実際に行った負荷試験の内容についてもご紹介できればと思います。