やあ!
場合によっては、ローカル環境でもテスト プラットフォームでも、迅速な負荷テストが必要になることがあります。通常、このようなタスクは、事前の完全な理解を必要とする特殊なツールを使用して取り組まれます。しかし、市場投入までの時間を短縮し、仮説を迅速に検証することが最も重要である企業や新興企業では、ツールを過度に使いこなすことが贅沢になってしまいます。
この記事は、開発者中心のソリューションに焦点を当てることを目的としています。これにより、深い関与の必要性がなくなり、ドキュメントを何ページも読み込むことなく初歩的なテストが可能になります。
ローカル実行中
インストールする必要があります::
Docker — すべてのサービスとツールが必要です。
Java 19+ — kotlin サービス用。また、Java 8 バージョンを使用してみることもできます。動作するはずですが、Gradle 設定を変更する必要があります。
Golang — サービスの 1 つは golang サービスです =)
Python 3+ — Yandex タンク用。
作業を開始する前に、テスト目的の説明例として役立つサービスをいくつか生成することをお勧めします。
スタック: Kotlin + webflux。 r2dbc + postgres。
私たちのサービスには次のような特徴があります。
– すべてのストックを取得 (制限 10) GET
– 名前で株式を取得 GET__ /api/v1/stock
– 在庫を保存 POST /
負荷テストに集中する必要があるので、簡単なサービスになるはずです =)
まずは、内部に基本的なロジックを備えた小さなサービスを作成しましょう。この目的のためにモデルを準備します。
@Table("stocks") data class Stock( @field:Id val id: Long?, val name: String, val price: BigDecimal, val description: String )
シンプルなルーター:
@Configuration @EnableConfigurationProperties(ServerProperties::class) class StockRouter( private val properties: ServerProperties, private val stockHandler: StockHandler ) { @Bean fun router() = coRouter { with(properties) { main.nest { contentType(APPLICATION_JSON).nest { POST(save, stockHandler::save) } GET(find, stockHandler::find) GET(findAll, stockHandler::findAll) } } } }
およびハンドラー:
@Service class StockHandlerImpl( private val stockService: StockService ) : StockHandler { private val logger = KotlinLogging.logger {} private companion object { const val DEFAULT_SIZE = 10 const val NAME_PARAM = "name" } override suspend fun findAll(req: ServerRequest): ServerResponse { logger.debug { "Processing find all request: $req" } val stocks = stockService.getAll(DEFAULT_SIZE) return ServerResponse.ok() .contentType(MediaType.APPLICATION_JSON) .body(stocks, StockDto::class.java) .awaitSingle() } override suspend fun find(req: ServerRequest): ServerResponse { logger.debug { "Processing find all request: $req" } val name = req.queryParam(NAME_PARAM) return if (name.isEmpty) { ServerResponse.badRequest().buildAndAwait() } else { val stocks = stockService.find(name.get()) ServerResponse.ok() .contentType(MediaType.APPLICATION_JSON) .body(stocks, StockDto::class.java) .awaitSingle() } } override suspend fun save(req: ServerRequest): ServerResponse { logger.debug { "Processing save request: $req" } val stockDto = req.awaitBodyOrNull(StockDto::class) return stockDto?.let { dto -> stockService.save(dto) ServerResponse .ok() .contentType(MediaType.APPLICATION_JSON) .bodyValue(dto) .awaitSingle() } ?: ServerResponse.badRequest().buildAndAwait() } }
完全なコードはここにあります:
docker ファイルを作成します。
FROM openjdk:20-jdk-slim VOLUME /tmp COPY build/libs/*.jar app.jar ENTRYPOINT ["java", "-Dspring.profiles.active=stg", "-jar", "/app.jar"]
次に、Docker イメージを構築して調整します 🤤
docker build -t ere/stock-service . docker run -p 8085:8085 ere/stock-service
ただし、今のところは、すべてを Docker コンテナーを通じて実行するという考えに固執し、サービスを Docker Compose セットアップに移行することをお勧めします。
version: '3.1' services: db: image: postgres container_name: postgres-stocks ports: - "5432:5432" environment: POSTGRES_PASSWORD: postgres adminer: image: adminer ports: - "8080:8080" stock-service: image: ere/stock-service container_name: stock-service ports: - "8085:8085" depends_on: - db
今後: テストをどのように進めればよいでしょうか?具体的には、最近開発したサービスに対して適度な負荷テストを開始するにはどうすればよいでしょうか?テスト ソリューションはインストールが簡単で、ユーザーフレンドリーであることが不可欠です。
時間の制約を考えると、広範なドキュメントや記事を詳しく調べることは現実的な選択肢ではありません。幸いなことに、実行可能な代替手段があります - Yandex Tank を入力してください。タンクはテスト用の強力な機器であり、以下の重要な統合が行われています。
ドキュメント:
テスト用のフォルダーを作成することから始めましょう。構成ファイルとその他の重要なファイル (幸いなことに、そのうちの 2 つだけ) を配置したら、準備は完了です。
私たちのサービスでは、「get-all」メソッドと「save」メソッドをテストする必要があります。 find メソッドの最初の構成。
phantom: address: localhost port: "8085" load_profile: load_type: rps schedule: line(100, 250, 30s) writelog: all ssl: false connection_test: true uris: - /api/v1/stocks overload: enabled: false telegraf: enabled: false autostop: autostop: - time(1s,10s) # if request average > 1s - http(5xx,100%,1s) # if 500 errors > 1s - http(4xx,25%,10s) # if 400 > 25% - net(xx,25,10) # if amount of non-zero net-codes in every second of last 10s period is more than 25
構成のための主要な設定:
bash スクリプト (tank sh) をコピーして貼り付けます。
docker run \ -v $(pwd):/var/loadtest \ --net="host" \ -it yandex/yandex-tank
そして走れ!
その結果、何が見えるでしょうか? Yandex Tank は、テスト中に価値があると判断したすべてを記録します。 99 パーセンタイルや 1 秒あたりのリクエスト数 (rps) などのメトリクスを観察できます。
それで、私たちは今ターミナルに行き詰まっていますか? GUIが欲しい!心配しないでください、Yandex Tank にはそれに対する解決策もあります。オーバーロード プラグインの 1 つを利用できます。追加方法の例を次に示します。
overload: enabled: true package: yandextank.plugins.DataUploader job_name: "save docs" token_file: "env/token.txt"
トークンを追加する必要があります。ここにアクセスして、GitHub でロジックを作成するだけです: https://overload.yandex.net
GET リクエストの処理は簡単ですが、POST の場合はどうでしょうか?リクエストをどのように構成すればよいでしょうか?問題は、リクエストを単にタンクに投げ込むことはできないということです。そのパターンを作成する必要があります。これらのパターンは何ですか?それは簡単です。小さなスクリプトを作成する必要があります。これをドキュメントから取得し、ニーズに合わせて少し調整することができます。
そして、独自の本文とヘッダーを追加する必要があります。
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys import json # http request with entity body template req_template_w_entity_body = ( "%s %s HTTP/1.1\r\n" "%s\r\n" "Content-Length: %d\r\n" "\r\n" "%s\r\n" ) # phantom ammo template ammo_template = ( "%d %s\n" "%s" ) method = "POST" case = "" headers = "Host: test.com\r\n" + \ "User-Agent: tank\r\n" + \ "Accept: */*\r\n" + \ "Connection: Close\r\n" def make_ammo(method, url, headers, case, body): """ makes phantom ammo """ req = req_template_w_entity_body % (method, url, headers, len(body), body) return ammo_template % (len(req), case, req) def generate_json(): body = { "name": "content", "price": 1, "description": "description" } url = "/api/v1/stock" h = headers + "Content-type: application/json" s1 = json.dumps(body) ammo = make_ammo(method, url, h, case, s1) sys.stdout.write(ammo) f2 = open("ammo/ammo-json.txt", "w") f2.write(ammo) if __name__ == "__main__": generate_json()
結果:
212 POST /api/v1/stock HTTP/1.1 Host: test.com User-Agent: tank Accept: */* Connection: Close Content-type: application/json Content-Length: 61 {"name": "content", "price": 1, "description": "description"}
それでおしまい!スクリプトを実行するだけで、ammo-json.txt が作成されます。新しいパラメータを config に設定し、URL を削除するだけです。
phantom: address: localhost:9001 ammo_type: phantom ammofile: ammo-json.txt
そしてもう一度実行してください!
HTTP メソッドの読み込みについて理解したので、GRPC のシナリオを考えるのは自然なことです。戦車のシンプルさに似た、GRPC 用の同様にアクセス可能なツールがあるのは幸運でしょうか?答えは肯定的です。 「ghz」についてご紹介します。ちょっと見てください:
しかし、その前に、優れたテスト サービスとして Go と GRPC を使用して小さなサービスを作成する必要があります。
小さな proto ファイルを準備します。
syntax = "proto3"; option go_package = "stock-grpc-service/stocks"; package stocks; service StocksService { rpc Save(SaveRequest) returns (SaveResponse) {} rpc Find(FindRequest) returns (FindResponse) {} } message SaveRequest { Stock stock = 1; } message SaveResponse { string code = 1; } message Stock { string name = 1; float price = 2; string description = 3; } message FindRequest { enum Type { INVALID = 0; BY_NAME = 1; } message ByName { string name = 1; } Type type = 1; oneof body { ByName by_name = 2; } } message FindResponse { Stock stock = 1; }
そしてそれを生成してください! (また、 protocをインストールする必要があります)
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative stocks.proto
私たちの結果:
次のステップ: できるだけ早くサービスを作成します。
dto (db レイヤーのストック エンティティ) を作成します。
package models // Stock – base dto type Stock struct { ID *int64 `json:"Id"` Price float32 `json:"Price"` Name string `json:"Name"` Description string `json:"Description"` }
サーバーの実装
// Server is used to implement stocks.UnimplementedStocksServiceServer. type Server struct { pb.UnimplementedStocksServiceServer stockUC stock.UseCase } // NewStockGRPCService stock gRPC service constructor func NewStockGRPCService(emailUC stock.UseCase) *Server { return &Server{stockUC: emailUC} } func (e *Server) Save(ctx context.Context, request *stocks.SaveRequest) (*stocks.SaveResponse, error) { model := request.Stock stockDto := &models.Stock{ ID: nil, Price: model.Price, Name: model.Name, Description: model.Description, } err := e.stockUC.Create(ctx, stockDto) if err != nil { return nil, err } return &stocks.SaveResponse{Code: "ok"}, nil } func (e *Server) Find(ctx context.Context, request *stocks.FindRequest) (*stocks.FindResponse, error) { code := request.GetByName().GetName() model, err := e.stockUC.GetByID(ctx, code) if err != nil { return nil, err } response := &stocks.FindResponse{Stock: &stocks.Stock{ Name: model.Name, Price: model.Price, Description: model.Description, }} return response, nil }
完全なコードはここにあります:クリックしてください!
ここで、少し変更する必要があります。
proto ファイルのあるフォルダーに移動します。
メソッドを追加: Stocks.StocksService.Save 。
単純な本文を追加します: '{"株": { "名前":"APPL", "価格": "1.3", "説明": "リンゴ株"} }'。
10
の接続が20
goroutine ワーカー間で共有されます。 2
のゴルーチンの各ペアは 1 つの接続を共有します。
サービスのポートを設定する
そして結果:
cd .. && cd stock-grpc-service/proto ghz --insecure \ --proto ./stocks.proto \ --call stocks.StocksService.Save \ -d '{"stock": { "name":"APPL", "price": "1.3", "description": "apple stocks"} }' \ -n 2000 \ -c 20 \ --connections=10 \ 0.0.0.0:5007
それを実行します!
Summary: Count: 2000 Total: 995.93 ms Slowest: 30.27 ms Fastest: 3.11 ms Average: 9.19 ms Requests/sec: 2008.16 Response time histogram: 3.111 [1] | 5.827 [229] |∎∎∎∎∎∎∎∎∎∎∎ 8.542 [840] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎ 11.258 [548] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎ 13.973 [190] |∎∎∎∎∎∎∎∎∎ 16.689 [93] |∎∎∎∎ 19.405 [33] |∎∎ 22.120 [29] |∎ 24.836 [26] |∎ 27.551 [6] | 30.267 [5] | Latency distribution: 10 % in 5.68 ms 25 % in 6.67 ms 50 % in 8.27 ms 75 % in 10.49 ms 90 % in 13.88 ms 95 % in 16.64 ms 99 % in 24.54 ms Status code distribution: [OK] 2000 responses
それで、ターミナル内のすべてをもう一度見つめてください。いいえ、ghz を使用してもレポートを生成できますが、Yandex とは異なり、レポートはローカルに生成され、ブラウザーで開くことができます。それを設定するだけです:
ghz --insecure -O html -o reports_find.html \ ...
-O + html → 出力形式
-o ファイル名
要約すると、1 秒あたり 100 以上のリクエストの負荷を処理するサービスの能力を迅速に評価する必要がある場合、または潜在的な弱点を特定する必要がある場合、チームが関与する複雑なプロセスを開始したり、AQA に支援を求めたり、インフラストラクチャ チームに頼ったりする必要はありません。
多くの場合、開発者は小規模な負荷テストを実行できる高性能のラップトップやコンピューターを持っています。それでは、時間を節約して、ぜひ試してみてください。
この短い記事が有益であると感じていただけたと思います。
Yandex Tank: ドキュメントリンク
Yandex Tank GitHub: GitHub リンク
Yandex タンク設定: リンク
ghz公式ページ:リンク
改めて感謝します。そして幸運を祈ります! 🍀🕵🏻