Olá!
Ocasionalmente, surge a necessidade de testes de carga rápidos, seja em um ambiente local ou em uma plataforma de testes. Normalmente, essas tarefas são realizadas usando ferramentas especializadas que exigem uma compreensão prévia completa. No entanto, em empresas e startups onde o rápido lançamento no mercado e a validação imediata de hipóteses são fundamentais, a familiarização excessiva com as ferramentas torna-se um luxo.
Este artigo tem como objetivo destacar soluções centradas no desenvolvedor que evitam a necessidade de envolvimento profundo, permitindo testes rudimentares sem se aprofundar em páginas de documentação.
corrida local
Você deve instalar::
Docker – todos os serviços e ferramentas são necessários para isso.
Java 19+ — para serviço kotlin. Além disso, você pode tentar usar a versão Java 8; deve funcionar, mas você precisa alterar as configurações do Gradle.
Golang — um dos serviços é o serviço golang =)
Python 3+ — para o tanque Yandex.
Antes de embarcarmos em nossa jornada, é aconselhável gerar alguns serviços que possam servir como exemplos ilustrativos para fins de teste.
Pilha: Kotlin + webflux. r2dbc + postgres.
Nosso serviço conta com:
– obter todas as ações (limite 10) GET
– obtenha estoque por nome GET__ /api/v1/stock
– salvar estoque POST /
Deve ser um serviço fácil porque temos que focar no teste de carga =)
Vamos começar criando um pequeno serviço com alguma lógica básica interna. Prepararemos um modelo para esse fim:
@Table("stocks") data class Stock( @field:Id val id: Long?, val name: String, val price: BigDecimal, val description: String )
Roteador simples:
@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) } } } }
e manipulador:
@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() } }
Código completo aqui:
Crie um arquivo docker:
FROM openjdk:20-jdk-slim VOLUME /tmp COPY build/libs/*.jar app.jar ENTRYPOINT ["java", "-Dspring.profiles.active=stg", "-jar", "/app.jar"]
Em seguida, crie uma imagem docker e ajuste-a 🤤
docker build -t ere/stock-service . docker run -p 8085:8085 ere/stock-service
Mas, por enquanto, é melhor manter a ideia de executar tudo por meio de contêineres Docker e migrar nosso serviço para uma configuração 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
Seguindo em frente: como podemos proceder com os testes? Especificamente, como podemos iniciar um teste de carga modesto para nosso serviço desenvolvido recentemente? É fundamental que a solução de teste seja simples de instalar e fácil de usar.
Dadas as nossas limitações de tempo, aprofundar-se em documentação e artigos extensos não é uma opção viável. Felizmente, existe uma alternativa viável – entre no Yandex Tank. O tanque é um instrumento poderoso para testes e possui integrações importantes com
Documentos:
Vamos começar criando uma pasta para nossos testes. Depois de colocarmos as configurações e outros arquivos essenciais – felizmente, apenas alguns deles – estaremos tudo pronto.
Para o nosso serviço, precisamos testar os métodos “get-all” e “save”. A primeira configuração para o método 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
Configurações principais para configuração:
Copie e cole o script bash (tank sh):
docker run \ -v $(pwd):/var/loadtest \ --net="host" \ -it yandex/yandex-tank
E corra!
O que veremos como resultado? Yandex Tank registrará tudo o que considerar digno durante o teste. Podemos observar métricas como o percentil 99 e solicitações por segundo (rps).
Então, estamos presos ao terminal agora? Eu quero uma interface gráfica! Não se preocupe, o Yandex Tank também tem uma solução para isso. Podemos utilizar um dos plugins de sobrecarga. Aqui está um exemplo de como adicioná-lo:
overload: enabled: true package: yandextank.plugins.DataUploader job_name: "save docs" token_file: "env/token.txt"
Deveríamos adicionar nosso token; basta acessar aqui e lógica pelo GitHub: https://overload.yandex.net
Ok, lidar com uma solicitação GET é simples, mas e quanto ao POST? Como estruturamos a solicitação? A questão é que você não pode simplesmente jogar a solicitação no tanque; você precisa criar padrões para isso! Quais são esses padrões? É simples — você precisa escrever um pequeno script, que pode ser obtido novamente na documentação e ajustado um pouco para atender às nossas necessidades.
E devemos adicionar nosso próprio corpo e cabeçalhos:
#!/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()
Resultado:
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"}
É isso! Basta executar o script e teremos ammo-json.txt. Basta definir novos parâmetros para configuração e excluir os URLs:
phantom: address: localhost:9001 ammo_type: phantom ammofile: ammo-json.txt
E execute mais uma vez!
Tendo nos familiarizado com o carregamento de métodos HTTP, é natural considerar o cenário do GRPC. Temos a sorte de ter uma ferramenta igualmente acessível para GRPC, semelhante à simplicidade de um tanque? A resposta é afirmativa. Permita-me apresentar-lhe 'ghz'. Só dê uma olhada:
Mas antes de fazermos isso, devemos criar um pequeno serviço com Go e GRPC como um bom serviço de teste.
Prepare um pequeno arquivo 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; }
E gere isso! (também, devemos instalar protoc )
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative stocks.proto
Nossos resultados:
Próximas etapas: Criar serviços o mais rápido possível.
Crie dto (entidade de estoque para camada 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"` }
Implementar servidor
// 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 }
Código completo aqui: clique, por favor!
Agora, devemos mudar um pouco:
vá para a pasta com os arquivos proto.
adicionar método: stocks.StocksService.Save .
adicione corpo simples: '{“estoque”: { “nome”:”APPL”, “preço”: “1.3”, “descrição”: “estoques de maçã”} }'.
10
conexões serão compartilhadas entre 20
trabalhadores goroutine. Cada par de 2
goroutines compartilhará uma única conexão.
definir porta do serviço
e o resultado:
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
Executá-lo!
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
E o quê, olhar tudo no terminal de novo? Não, com ghz você também pode gerar um relatório, mas ao contrário do Yandex, ele será gerado localmente e poderá ser aberto no navegador. Basta configurá-lo:
ghz --insecure -O html -o reports_find.html \ ...
-O + html → formato de saída
-o nome do arquivo
Em resumo, quando você precisa de uma avaliação rápida da capacidade do seu serviço de lidar com uma carga de mais de 100 solicitações por segundo ou identificar possíveis pontos fracos, não há necessidade de iniciar processos complexos envolvendo equipes, buscando assistência da AQA ou contando com a equipe de infraestrutura.
Na maioria das vezes, os desenvolvedores têm laptops e computadores capazes de executar um pequeno teste de carga. Então, vá em frente e experimente – economize algum tempo!
Espero que você tenha achado este breve artigo benéfico.
Tanque Yandex: link de documentos
Yandex Tank GitHub: link GitHub
Configuração do tanque Yandex: link
página oficial do ghz: link
configuração ghz: link
configuração ghz: link
Obrigado mais uma vez e boa sorte! 🍀🕵🏻