locust+launce+goでgRPCの負荷試験をする
locust
WebとAPI向けの負荷試験ツールです。
LocustはシナリオをPython で実装し、ユーザー数を設定すると簡単に負荷テストができます。
launce
launce は Locust のワーカーの仕組みを提供するライブラリです。
マスターの機能は持っていないので、 Locust でマスタープロセスを起動し、 Go で実装したワーカーをマスターに接続するという形で実行します。
今回はこの二つのツール、ライブラリを組み合わせてgolangで負荷試験のシナリオテストを書き、GUIで負荷試験を実施してみたいと思います!
作業環境の準備
locust version: 2.29.1
python version: 3.12.4
Locustはpythonで動作するのでpythonの実行環境が必要です。
今回はpyenvを使ってpython環境を作成します。
pyenvのインストール
$ brew install pyenv
インストールできるpythonのリストを確認
$ pyenv install --list
最新の3.12.4をインストールする
$ pyenv install 3.8.2
インストールされたpythonのバージョンを確認
$ pyenv versions
pyenvを使用して、現在のシェルまたはグローバルに使用するPythonのバージョンを切り替えるには、以下のコマンドを使用します。
$ pyenv local <バージョン>
or
$ pyenv global <バージョン>
pythonのバージョンを確認して指定したバージョンになっていればok!
$ python --version
作業ディレクトリを作成
任意でディレクトリを作成し、locustをインストールします
$ pip3 install locust
ディレクトリツリーはこちら
.
├── cmd
│ ├── launce
│ │ └── main.go
│ └── server
│ └── main.go
├── go.mod
├── go.sum
├── grpc
│ └── helloworld.pb.go
├── launce
├── locustfile.py
├── proto
│ ├── hello.pb.go
│ ├── hello.proto
│ └── hello_grpc.pb.go
└── server
作業環境の準備が終わったのでprotoファイルを作成して今回負荷試験を行うgRPCのサーバーを作成していきます!
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;
}
公式チュートリアルにもある簡単なサンプルファイルを利用します
protoファイルをパースする
$ protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
proto/hello.proto
hello_grpc.pb.goとhello.pb.goが自動生成されます
serverを作成する
package main
import (
"context"
"fmt"
pb "locust-boomer/proto"
"log"
"math/rand"
"net"
"time"
"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.Name}, nil
}
func main() {
fmt.Println("Hello, gRPC!")
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
SayHello関数ではランダムの秒数待機後、レスポンスを返すようにしています
launceライブラリを使ってgoで負荷試験のシナリオを書いていきます
package main
import (
"context"
"log"
"os/signal"
"syscall"
"time"
pb "locust-boomer/proto"
"github.com/qitoi/launce"
"google.golang.org/grpc"
)
const (
target = "localhost:50051"
)
var (
_ launce.BaseUser = (*MyUser)(nil)
)
type MyUser struct {
launce.BaseUserImpl
}
func (u *MyUser) WaitTime() launce.WaitTimeFunc {
return launce.Constant(1 * time.Second)
}
func (u *MyUser) Process(ctx context.Context) error {
conn, err := grpc.DialContext(ctx, target, grpc.WithInsecure())
if err != nil {
log.Fatalf("grpc.DialContext: %v", err)
}
defer conn.Close()
client := pb.NewGreeterClient(conn)
start := time.Now()
response, err := client.SayHello(ctx, &pb.HelloRequest{Name: "test"})
responseTime := time.Since(start)
if err != nil {
// report as failure
u.Report(
"grpc",
"/SayHello",
responseTime,
int64(len(response.String())),
err,
)
}
// report as successful
u.Report(
"grpc",
"SayHello",
responseTime,
int64(len(response.String())),
nil,
)
if err := u.Wait(ctx); err != nil {
return err
}
return nil
}
func main() {
transport := launce.NewZmqTransport("localhost", 5557)
worker, err := launce.NewWorker(transport)
if err != nil {
log.Fatal(err)
}
worker.RegisterUser("MyUser", func() launce.User {
return &MyUser{}
})
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
go func() {
<-ctx.Done()
worker.Quit()
}()
if err := worker.Join(); err != nil {
log.Fatal(err)
}
}
SayHelloのRPCを呼び出すだけの単純なシナリオです!
locustfile.py
from locust import User
class MyUser(User):
...
Locust に読み込ませるダミーの locustfile.py を作成します。
実際のユーザーの振る舞いは Go で記述するので、実装は空のクラスを定義するだけです。
ここまで出来たら後は負荷試験を実行するだけです!
実行ファイルの準備
$ go build -o server ./cmd/server/main.go
$ go build -o launce ./cmd/launce/main.go
gRPCサーバーを起動
$ ./server
作成したダミーファイルを指定してlocustのmasterを起動します
$ locust --master -f locustfile.py
続いてlaunceを起動します
$ ./launce
reported as ready. 1 workers connected. が出ていればokです!
[2024-07-30 00:05:44,658] CA-20018906/INFO/locust.main: Starting web interface at http://0.0.0.0:8089
[2024-07-30 00:05:44,675] CA-20018906/INFO/locust.main: Starting Locust 2.29.1
[2024-07-30 00:05:53,301] CA-20018906/WARNING/locust.runners: A worker (CA-20018906_211c4a4758b74a97ad79e599164e5549) running a different version (2.28.0.launce-1.1.0) connected, master version is 2.29.1
[2024-07-30 00:05:53,301] CA-20018906/INFO/locust.runners: Worker CA-20018906_211c4a4758b74a97ad79e599164e5549 (index 0) reported as ready. 1 workers connected.
LocustのWebUIに接続します
http://0.0.0.0:8089
負荷をかけるユーザー数、リクエストの秒間隔を指定してStartを押すとワーカーでlaunceで描いたシナリオが開始され、負荷試験が行うことができます!
エントリーにSayHelloが表示されていればok!