はじめに
この記事はミクシィアドベントカレンダー2021 8日目の記事です。
こんにちは。
サービスのリリース前や大きなイベント前には皆さんも負荷試験をしているかと思います。
今回はgRPCを使った新規サービスでboomerというツールを使って負荷試験をしたのですが、なかなか使い心地がよかったのでご紹介したいと思います。
またboomerの選定については先日までアルバイトで来て頂いていた方にメインやって頂いたのですが、非常に助かりました。
この場をお借り致しまして,お礼申し上げます。
boomerとは
LocustのシナリオをGoで書けるWorker Runnerです。
LocustのWorkerとして動作するため、別途Locust Masterは必要になります。
gRPCに特化したツールというわけではありません。
ツールの選定理由
Locust + boomerは以下の理由で採用しました。
-
Locust
- Web UI がついている
- 社内で実績/知見がある
- 情報が多い
- Workerのスケールがしやすい
-
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
公式のサンプル とほとんど一緒です。
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サーバーです。
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を呼び出してリクエストの結果を記録しています。
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)
}
環境準備
サーバーとシナリオをビルドして実行ファイルを作成しておきます。
$ go build -o server ./cmd/server/main.go
$ go build -o boomer ./cmd/boomer/main.go
サーバーを起動します。
$ ./server
Locust Masterの起動にダミーのシナリオが必要なため、dummy.pyを作成し、Locust Masterを起動します。
$ locust --master -f dummy.py
続いてboomerを起動します。
$ ./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をクリックします。
シナリオに書いた通りSayHelloへのリクエストが記録されました。
SayHelloはランダムでSleepされる処理が入っていますが、レスポンスタイムもそれに合わせて記録されています。
今度はエラー時の動作を確認するため、SayHelloからエラーが返されるようにして再度負荷をかけてみます。
SayHelloからエラーが返されているため、Failsのカウントが増加していることが確認できます。
また、Failuresタブを開くとエラー理由が記録されていることが確認できました。
まとめ
このようにboomerを使うことでGoでシナリオを書け、gRPCサーバーへの負荷試験を行うことができました。
(もちろん通常のhttpサーバーへの負荷試験もできます)
Pythonを使った場合でも簡単にシナリオは書けますし同様のことはできますが、サーバーでGoを使っている場合は同じ言語でシナリオを書けるのはメリットだと思いますので、是非boomerを使ってみてください。
また今回はboomerの紹介のみとなりましたが、今度はboomerを使って実際に行った負荷試験の内容についてもご紹介できればと思います。