見出し画像

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!

いいなと思ったら応援しよう!