Всем привет!
Иногда возникает необходимость в быстром нагрузочном тестировании, будь то в локальной среде или на тестовой платформе. Обычно такие задачи решаются с использованием специализированных инструментов, требующих тщательного предварительного понимания. Однако на предприятиях и стартапах, где быстрый выход на рынок и быстрая проверка гипотез имеют первостепенное значение, чрезмерное ознакомление с инструментами становится роскошью.
Цель этой статьи — осветить решения, ориентированные на разработчиков, которые устраняют необходимость глубокого взаимодействия и позволяют проводить элементарное тестирование, не углубляясь в страницы документации.
местный бег
Вам следует установить::
Docker — для него необходимы все сервисы и инструменты.
Java 19+ — для сервиса Kotlin. Также вы можете попробовать использовать версию Java 8; это должно работать, но вам придется изменить настройки Gradle.
Golang — один из сервисов — сервис golang =)
Python 3+ — для танка Яндекс.
Прежде чем отправиться в наше путешествие, желательно сгенерировать пару сервисов, которые могут служить наглядными примерами для целей тестирования.
Стек: Котлин + webflux. r2dbc + постгрес.
В нашем сервисе есть:
— получить все акции (лимит 10) ПОЛУЧИТЬ
— получить сток по имени 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 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
Двигаясь вперед: как мы можем продолжить тестирование? В частности, как мы можем запустить скромный нагрузочный тест для нашего недавно разработанного сервиса? Крайне важно, чтобы решение для тестирования было простым в установке и удобным для пользователя.
Учитывая наши ограничения по времени, углубляться в обширную документацию и статьи — не лучший вариант. К счастью, есть реальная альтернатива — введите Яндекс Танк. Танк является мощным инструментом для испытаний и имеет важную интеграцию с
Документы:
Давайте начнем с создания папки для наших тестов. Как только мы разместим конфигурации и другие необходимые файлы (к счастью, их всего пара), все будет готово.
Для нашего сервиса нам нужно протестировать методы «получить все» и «сохранить». Первая конфигурация для метода 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
И беги!
Что мы увидим в результате? Яндекс Танк во время теста запишет все, что посчитает нужным. Мы можем наблюдать такие показатели, как 99-й процентиль и количество запросов в секунду (RPS).
Итак, мы теперь застряли в терминале? Я хочу графический интерфейс! Не волнуйтесь, у Яндекс Танка есть решение и для этой проблемы. Мы можем использовать один из плагинов перегрузки. Вот пример того, как его добавить:
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 (стандартный объект для слоя базы данных)
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 .
добавьте простое тело: '{"акции": { "имя":"APPL", "цена": "1.3", "описание": "яблочные акции"} }'.
10
подключений будут разделены между 20
работниками goroutine. Каждая пара из 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 тоже можно сформировать отчет, но в отличие от Яндекса он будет сформирован локально и его можно будет открыть в браузере. Просто установите его:
ghz --insecure -O html -o reports_find.html \ ...
-O + html → формат вывода
-о имя файла
Таким образом, когда вам нужна быстрая оценка способности вашего сервиса обрабатывать нагрузку более 100 запросов в секунду или выявлять потенциальные слабые места, нет необходимости инициировать сложные процессы с участием команд, обращаться за помощью к AQA или полагаться на команду по инфраструктуре.
Чаще всего у разработчиков есть мощные ноутбуки и компьютеры, которые могут выполнить небольшой нагрузочный тест. Итак, попробуйте — сэкономьте себе время!
Я надеюсь, что эта краткая статья оказалась для вас полезной.
Яндекс Танк: ссылка на документацию
Яндекс Танк GitHub: ссылка на GitHub
Настройка Яндекс Танка: ссылка
Официальная страница ghz: ссылка
Настройка ГГц: ссылка
Конфигурация ghz: ссылка
Еще раз спасибо и удачи! 🍀🕵🏻