Salut!
Parfois, il devient nécessaire de procéder à des tests de charge rapides, que ce soit dans un environnement local ou sur une plateforme de test. En règle générale, ces tâches sont abordées à l’aide d’outils spécialisés qui nécessitent une compréhension préalable approfondie. Cependant, au sein des entreprises et des startups où la rapidité de mise sur le marché et la validation rapide des hypothèses sont primordiales, une familiarisation excessive avec les outils devient un luxe.
Cet article vise à mettre en lumière des solutions centrées sur les développeurs qui évitent la nécessité d'un engagement profond, permettant des tests rudimentaires sans se plonger dans des pages de documentation.
course locale
Vous devez installer : :
Docker — tous les services et outils sont nécessaires pour cela.
Java 19+ — pour le service Kotlin. Vous pouvez également essayer d'utiliser la version Java 8 ; cela devrait fonctionner, mais vous devez modifier les paramètres Gradle.
Golang — l'un des services est le service golang =)
Python 3+ — pour le tank Yandex.
Avant de nous lancer dans notre voyage, il est conseillé de générer quelques services qui peuvent servir d'exemples illustratifs à des fins de test.
Pile : Kotlin + webflux. r2dbc + postgres.
Notre service a :
– obtenir tous les stocks (limite 10) OBTENIR
– obtenir le stock par nom GET__ /api/v1/stock
– sauvegarder le stock POST /
Cela devrait être un service simple car nous devons nous concentrer sur les tests de charge =)
Commençons par créer un petit service avec une logique de base à l'intérieur. Nous allons préparer un modèle à cet effet :
@Table("stocks") data class Stock( @field:Id val id: Long?, val name: String, val price: BigDecimal, val description: String )
Routeur simple :
@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) } } } }
et gestionnaire :
@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() } }
Code complet ici :
Créez un fichier Docker :
FROM openjdk:20-jdk-slim VOLUME /tmp COPY build/libs/*.jar app.jar ENTRYPOINT ["java", "-Dspring.profiles.active=stg", "-jar", "/app.jar"]
Ensuite, créez une image Docker et ajustez-la 🤤
docker build -t ere/stock-service . docker run -p 8085:8085 ere/stock-service
Mais pour l'instant, il est préférable de s'en tenir à l'idée de tout exécuter via des conteneurs Docker et de migrer notre service vers une configuration 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
Aller de l’avant : comment procéder aux tests ? Plus précisément, comment pouvons-nous lancer un modeste test de charge pour notre service récemment développé ? Il est impératif que la solution de test soit à la fois simple à installer et conviviale.
Compte tenu de nos contraintes de temps, se plonger dans une documentation et des articles détaillés n'est pas une option viable. Heureusement, il existe une alternative viable : entrez dans Yandex Tank. Le réservoir est un instrument puissant pour les tests et possède d'importantes intégrations avec
Documents :
Commençons par créer un dossier pour nos tests. Une fois que nous aurons placé les configurations et autres fichiers essentiels (heureusement, juste quelques-uns), nous serons tous prêts.
Pour notre service, nous devons tester les méthodes « get-all » et « save ». La première configuration pour la méthode 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
Paramètres clés pour la configuration :
Copiez et collez le script bash (tank sh) :
docker run \ -v $(pwd):/var/loadtest \ --net="host" \ -it yandex/yandex-tank
Et courir!
Que verrons-nous comme résultat ? Yandex Tank enregistrera tout ce qu'il juge utile pendant le test. Nous pouvons observer des métriques telles que le 99e percentile et les requêtes par seconde (rps).
Alors, sommes-nous coincés avec le terminal maintenant ? Je veux une interface graphique ! Ne vous inquiétez pas, Yandex Tank a également une solution pour cela. Nous pouvons utiliser l'un des plugins de surcharge. Voici un exemple de la façon de l'ajouter :
overload: enabled: true package: yandextank.plugins.DataUploader job_name: "save docs" token_file: "env/token.txt"
Nous devrions ajouter notre jeton ; allez simplement ici et logique par GitHub : https://overload.yandex.net
D'accord, traiter une requête GET est simple, mais qu'en est-il du POST ? Comment structurer la demande ? Le fait est que vous ne pouvez pas simplement jeter la demande dans le réservoir ; vous devez créer des modèles pour cela ! Quels sont ces modèles ? C'est simple : vous devez écrire un petit script, que vous pouvez à nouveau récupérer dans la documentation et modifier légèrement pour répondre à nos besoins.
Et nous devrions ajouter notre propre corps et nos propres en-têtes :
#!/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()
Résultat:
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"}
C'est ça! Exécutez simplement le script et nous aurons ammo-json.txt. Définissez simplement de nouveaux paramètres à configurer et supprimez les URL :
phantom: address: localhost:9001 ammo_type: phantom ammofile: ammo-json.txt
Et exécutez-le encore une fois !
Après nous être familiarisés avec le chargement des méthodes HTTP, il est naturel d'envisager le scénario du GRPC. Sommes-nous assez chanceux pour disposer d'un outil tout aussi accessible pour le GRPC, semblable à la simplicité d'un tank ? La réponse est affirmative. Permettez-moi de vous présenter « ghz ». Jette un coup d'oeil:
Mais avant de faire cela, nous devrions créer un petit service avec Go et GRPC comme bon service de test.
Préparez un petit fichier 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; }
Et générez-le ! (aussi, nous devrions installer protoc )
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative stocks.proto
Nos résultats :
Prochaines étapes : créer des services aussi vite que possible.
Créer dto (entité stock pour la couche de base de données)
package models // Stock – base dto type Stock struct { ID *int64 `json:"Id"` Price float32 `json:"Price"` Name string `json:"Name"` Description string `json:"Description"` }
Implémenter le serveur
// 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 }
Code complet ici : cliquez, s'il vous plaît !
Maintenant, nous devrions le changer un peu :
déplacez-vous vers le dossier contenant les fichiers proto.
méthode d'ajout : stocks.StocksService.Save .
ajoutez un corps simple : '{"stock" : { "nom": "APPL", "prix": "1.3", "description": "actions Apple"} }'.
10
connexions seront partagées entre 20
travailleurs goroutines. Chaque paire de 2
goroutines partagera une seule connexion.
définir le port du service
et le résultat :
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
Exécuter!
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
Et quoi, regarder à nouveau tout dans le terminal ? Non, avec ghz, vous pouvez également générer un rapport, mais contrairement à Yandex, il sera généré localement et pourra être ouvert dans le navigateur. Définissez-le simplement :
ghz --insecure -O html -o reports_find.html \ ...
-O + html → format de sortie
-o nom de fichier
En résumé, lorsque vous avez besoin d'une évaluation rapide de la capacité de votre service à gérer plus de 100 requêtes par seconde ou à identifier des faiblesses potentielles, il n'est pas nécessaire de lancer des processus complexes impliquant des équipes, de demander l'aide d'AQA ou de compter sur l'équipe d'infrastructure.
Le plus souvent, les développeurs disposent d’ordinateurs portables et d’ordinateurs capables d’exécuter un petit test de charge. Alors, n’hésitez pas et essayez : gagnez du temps !
J’espère que vous avez trouvé ce bref article bénéfique.
Yandex Tank : lien vers la documentation
Yandex Tank GitHub : lien GitHub
Paramètres du réservoir Yandex : lien
page officielle ghz : lien
réglage GHz : lien
configuration GHz : lien
Merci encore et bonne chance ! 🍀🕵🏻