Hallo!
Gelegentlich besteht Bedarf an schnellen Lasttests, sei es in einer lokalen Umgebung oder auf einer Testplattform. Typischerweise werden solche Aufgaben mit speziellen Werkzeugen gelöst, die ein gründliches Vorverständnis erfordern. In Unternehmen und Start-ups, in denen eine schnelle Markteinführung und eine schnelle Hypothesenvalidierung von größter Bedeutung sind, wird eine übermäßige Einarbeitung in die Tools jedoch zum Luxus.
Ziel dieses Artikels ist es, entwicklerzentrierte Lösungen hervorzuheben, die ein tiefgreifendes Engagement überflüssig machen und rudimentäre Tests ermöglichen, ohne sich in seitenweise Dokumentation zu vertiefen.
lokales Laufen
Sie sollten Folgendes installieren:
Docker – alle Dienste und Tools sind dafür erforderlich.
Java 19+ – für Kotlin-Dienst. Sie können auch versuchen, die Java 8-Version zu verwenden; Es sollte funktionieren, aber Sie müssen die Gradle-Einstellungen ändern.
Golang – einer der Dienste ist der Golang-Dienst =)
Python 3+ – für den Yandex-Panzer.
Bevor wir uns auf den Weg machen, empfiehlt es sich, einige Dienste zu generieren, die als anschauliche Beispiele für Testzwecke dienen können.
Stapel: Kotlin + Webflux. r2dbc + postgres.
Unser Service umfasst:
– Alle Aktien abrufen (Limit 10) GET
– Bestand nach Namen abrufen GET__ /api/v1/stock
– Lagerbestand speichern POST /
Es sollte ein einfacher Service sein, da wir uns auf Lasttests konzentrieren müssen =)
Beginnen wir mit der Erstellung eines kleinen Dienstes mit einer grundlegenden Logik darin. Dazu bereiten wir ein Modell vor:
@Table("stocks") data class Stock( @field:Id val id: Long?, val name: String, val price: BigDecimal, val description: String )
Einfacher Router:
@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) } } } }
und Behandler:
@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() } }
Vollständiger Code hier:
Erstellen Sie eine Docker-Datei:
FROM openjdk:20-jdk-slim VOLUME /tmp COPY build/libs/*.jar app.jar ENTRYPOINT ["java", "-Dspring.profiles.active=stg", "-jar", "/app.jar"]
Erstellen Sie dann ein Docker-Image und optimieren Sie es 🤤
docker build -t ere/stock-service . docker run -p 8085:8085 ere/stock-service
Aber vorerst ist es besser, bei der Idee zu bleiben, alles über Docker-Container laufen zu lassen und unseren Service in ein Docker Compose-Setup zu migrieren.
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
Weiter geht es: Wie können wir mit dem Testen fortfahren? Wie können wir konkret einen bescheidenen Auslastungstest für unseren kürzlich entwickelten Dienst einleiten? Es ist wichtig, dass die Testlösung sowohl einfach zu installieren als auch benutzerfreundlich ist.
Angesichts unserer Zeitbeschränkungen ist es keine praktikable Option, sich mit umfangreichen Dokumentationen und Artikeln zu befassen. Glücklicherweise gibt es eine praktikable Alternative: Yandex Tank. Der Tank ist ein leistungsstarkes Instrument zum Testen und verfügt über wichtige Integrationen mit
Dokumente:
Beginnen wir mit der Erstellung eines Ordners für unsere Tests. Sobald wir die Konfigurationen und andere wichtige Dateien platziert haben – zum Glück nur ein paar davon – sind wir startklar.
Für unseren Service müssen wir die Methoden „Get-All“ und „Save“ testen. Die erste Konfiguration für die Suchmethode.
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
Wichtige Einstellungen zur Konfiguration:
Kopieren Sie das Bash-Skript (tank sh) und fügen Sie es ein:
docker run \ -v $(pwd):/var/loadtest \ --net="host" \ -it yandex/yandex-tank
Und Renn!
Was werden wir als Ergebnis sehen? Yandex Tank protokolliert während des Tests alles, was es für wertvoll hält. Wir können Metriken wie das 99. Perzentil und Anfragen pro Sekunde (rps) beobachten.
Bleiben wir jetzt also beim Terminal hängen? Ich möchte eine GUI! Keine Sorge, Yandex Tank hat auch dafür eine Lösung. Wir können eines der Überlastungs-Plugins verwenden. Hier ist ein Beispiel für das Hinzufügen:
overload: enabled: true package: yandextank.plugins.DataUploader job_name: "save docs" token_file: "env/token.txt"
Wir sollten unser Token hinzufügen; Gehen Sie einfach hierher und loggen Sie sich bei GitHub ein: https://overload.yandex.net
Okay, die Bearbeitung einer GET-Anfrage ist unkompliziert, aber was ist mit POST? Wie strukturieren wir die Anfrage? Die Sache ist, dass man die Anfrage nicht einfach in den Müll werfen kann; Sie müssen Muster dafür erstellen! Was sind diese Muster? Es ist ganz einfach: Sie müssen ein kleines Skript schreiben, das Sie wiederum aus der Dokumentation abrufen und ein wenig an unsere Bedürfnisse anpassen können.
Und wir sollten unseren eigenen Text und unsere eigenen Header hinzufügen:
#!/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()
Ergebnis:
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"}
Das ist es! Führen Sie einfach das Skript aus und wir erhalten ammo-json.txt. Legen Sie einfach neue Parameter für config fest und löschen Sie die URLs:
phantom: address: localhost:9001 ammo_type: phantom ammofile: ammo-json.txt
Und lassen Sie es noch einmal laufen!
Nachdem wir uns mit dem Laden von HTTP-Methoden vertraut gemacht haben, liegt es nahe, das Szenario für GRPC in Betracht zu ziehen. Haben wir das Glück, ein ebenso zugängliches Werkzeug für GRPC zu haben, das der Einfachheit eines Tanks ähnelt? Die Antwort ist bejahend. Erlauben Sie mir, Ihnen „ghz“ vorzustellen. Mal schauen:
Aber bevor wir das tun, sollten wir mit Go und GRPC einen kleinen Dienst als guten Testdienst erstellen.
Bereiten Sie eine kleine Prototypdatei vor:
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; }
Und generieren Sie es! (Außerdem sollten wir protoc installieren)
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative stocks.proto
Unsere Ergebnisse:
Nächste Schritte: Erstellen Sie Dienste so schnell wie möglich.
Erstellen Sie dto (Stock-Entität für DB-Layer)
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 implementieren
// 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 }
Vollständiger Code hier: Bitte klicken!
Jetzt sollten wir es ein wenig ändern:
Wechseln Sie in den Ordner mit den Protodateien.
Methode hinzufügen: stocks.StocksService.Save .
Einfachen Text hinzufügen: '{„stock“: { „name“:“APPL“, „price“: „1.3“, „description“: „apple stocks“} }‘.
10
Verbindungen werden von 20
Goroutine-Mitarbeitern gemeinsam genutzt. Jedes Paar aus 2
Goroutinen teilt sich eine einzelne Verbindung.
Legen Sie den Port des Dienstes fest
und das Ergebnis:
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
Starte es!
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
Und was, nochmal alles im Terminal anstarren? Nein, mit ghz können Sie auch einen Bericht erstellen, aber im Gegensatz zu Yandex wird dieser lokal generiert und kann im Browser geöffnet werden. Stellen Sie es einfach ein:
ghz --insecure -O html -o reports_find.html \ ...
-O + html → Ausgabeformat
-o Dateiname
Zusammenfassend lässt sich sagen: Wenn Sie eine schnelle Einschätzung der Fähigkeit Ihres Dienstes benötigen, eine Last von mehr als 100 Anfragen pro Sekunde zu verarbeiten oder potenzielle Schwachstellen zu identifizieren, besteht keine Notwendigkeit, komplizierte Prozesse einzuleiten, an denen Teams beteiligt sind, Unterstützung von AQA in Anspruch zu nehmen oder sich auf das Infrastrukturteam zu verlassen.
In den meisten Fällen verfügen Entwickler über leistungsfähige Laptops und Computer, die einen kleinen Lasttest durchführen können. Probieren Sie es einfach aus – sparen Sie Zeit!
Ich vertraue darauf, dass Sie diesen kurzen Artikel hilfreich fanden.
Yandex Tank: Link zu Dokumenten
Yandex Tank GitHub: GitHub-Link
Yandex-Tankeinstellung: Link
Offizielle ghz-Seite: Link
GHz-Einstellung: Link
GHz-Konfiguration: Link
Nochmals vielen Dank und viel Glück! 🍀🕵🏻