你好呀!
有时,需要进行快速负载测试,无论是在本地环境中还是在测试平台上。通常,此类任务是使用需要事先彻底理解的专用工具来解决的。然而,在企业和初创公司中,快速上市和及时的假设验证至关重要,过度熟悉工具成为一种奢侈。
本文旨在重点关注以开发人员为中心的解决方案,这些解决方案无需深入参与,无需深入研究文档即可进行基本测试。
本地运行
你应该安装::
Docker——所有服务和工具都是它所必需的。
Java 19+ — 用于 kotlin 服务。另外,你可以尝试使用Java 8版本;它应该可以工作,但你必须更改 Gradle 设置。
Golang — 其中一项服务是 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。该罐是一种强大的测试仪器,并且与以下系统有重要的集成:
文件:
让我们首先为我们的测试创建一个文件夹。一旦我们放置了配置和其他重要文件(幸运的是,只有其中几个),我们就准备好了。
对于我们的服务,我们需要测试“获取全部”和“保存”方法。 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) 等指标。
那么,我们现在被终端困住了吗?我想要一个图形用户界面!别担心,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 创建一个小型服务作为良好的测试服务。
准备一个小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 }
完整代码在这里:请点击!
现在,我们应该稍微改变一下:
移动到包含原始文件的文件夹。
添加方法: stocks.StocksService.Save 。
添加简单的正文:'{“stock”:{“name”:“APPL”,“price”:“1.3”,“description”:“applestocks”}}'。
10
连接将在20
Goroutine Worker 之间共享。每对2
goroutine 将共享一个连接。
设置服务的端口
结果:
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 Tank: 文档链接
Yandex Tank GitHub: GitHub 链接
Yandex 水箱设置: 链接
ghz 官方页面:链接
再次感谢,祝你好运! 🍀🕵🏻