안녕하세요!
때로는 로컬 환경이든 테스트 플랫폼이든 신속한 로드 테스트가 필요할 때가 있습니다. 일반적으로 이러한 작업은 철저한 사전 이해가 필요한 특수 도구를 사용하여 처리됩니다. 그러나 신속한 출시 시간과 신속한 가설 검증이 무엇보다 중요한 기업 및 스타트업에서는 도구에 과도하게 익숙해지는 것이 사치가 됩니다.
이 기사의 목표는 심도 깊은 참여의 필요성을 없애고 문서 페이지를 자세히 살펴보지 않고도 기초적인 테스트를 허용하는 개발자 중심 솔루션을 조명하는 것입니다.
지역 달리기
다음을 설치해야 합니다::
Docker — 모든 서비스와 도구가 필요합니다.
Java 19+ — Kotlin 서비스용. 또한 Java 8 버전을 사용해 볼 수도 있습니다. 작동해야 하지만 Gradle 설정을 변경해야 합니다.
Golang — 서비스 중 하나가 golang 서비스입니다 =)
Python 3+ — Yandex 탱크용.
여정을 시작하기 전에 테스트 목적으로 예시로 사용할 수 있는 몇 가지 서비스를 생성하는 것이 좋습니다.
스택: Kotlin + webflux. r2dbc + 포스트그레스.
우리의 서비스는 다음과 같습니다:
– 모든 주식을 얻으십시오 (한도 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() } }
전체 코드는 다음과 같습니다.
도커 파일을 생성합니다:
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를 선택하세요. 탱크는 테스트를 위한 강력한 도구이며 다음과 같은 중요한 통합 기능을 가지고 있습니다.
문서:
테스트용 폴더를 만들어 시작해 보겠습니다. 구성 및 기타 필수 파일(다행히도 몇 개만)을 배치하면 모든 준비가 완료됩니다.
우리 서비스를 위해서는 "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번째 백분위수 및 초당 요청 수(rps)와 같은 지표를 관찰할 수 있습니다.
그렇다면 이제 터미널에 갇혀 있는 걸까요? 나는 GUI를 원한다! 걱정하지 마세요. Yandex Tank에도 이에 대한 솔루션이 있습니다. 오버로드 플러그인 중 하나를 활용할 수 있습니다. 추가하는 방법의 예는 다음과 같습니다.
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가 생성됩니다. 새 매개변수를 구성으로 설정하고 URL을 삭제하세요.
phantom: address: localhost:9001 ammo_type: phantom ammofile: ammo-json.txt
그리고 한 번 더 실행해보세요!
HTTP 메소드 로딩에 익숙해지면 GRPC 시나리오를 고려하는 것이 당연합니다. 탱크의 단순성과 유사한 GRPC용 도구를 동등하게 사용할 수 있을 만큼 운이 좋았습니까? 대답은 긍정적이다. 'ghz'에 대해 소개해드리겠습니다. 한번 살펴보세요:
하지만 그 전에 좋은 테스트 서비스로 Go 및 GRPC를 사용하여 작은 서비스를 만들어야 합니다.
작은 프로토 파일을 준비합니다.
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 .
간단한 본문 추가: '{“stock”: { “name”:”APPL”, “price”: “1.3”, “description”: “apple stocks”} }'.
20
고루틴 작업자 간에 10
연결이 공유됩니다. 2
고루틴으로 구성된 각 쌍은 단일 연결을 공유합니다.
서비스 포트 설정
결과는 다음과 같습니다.
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 파일 이름
요약하자면, 초당 100개 이상의 요청 로드를 처리하거나 잠재적인 약점을 식별하는 서비스 능력에 대한 신속한 평가가 필요한 경우 팀이 관련된 복잡한 프로세스를 시작하거나 AQA에 지원을 구하거나 인프라 팀에 의존할 필요가 없습니다.
개발자는 소규모 부하 테스트를 실행할 수 있는 노트북과 컴퓨터를 보유하고 있는 경우가 많습니다. 그러니 한 번 시도해 보세요. 시간을 절약해 보세요!
이 간략한 기사가 도움이 되셨다고 믿습니다.
Yandex 탱크: 문서 링크
Yandex 탱크 GitHub: GitHub 링크
Yandex 탱크 설정: 링크
ghz 공식 페이지: 링크
다시 한 번 감사드리며, 행운을 빕니다! 🕵🏻