Введение
Часто возникает задача провести нагрузочное тестирование какого-либо сервиса или системы. Из известных и распространенных инструментов для этой задачи есть Yandex.Tank и Jmeter.
Основная проблема Танка в том, что из коробки он может тестировать только web приложения. Jmeter же написан на java и чтобы его можно было расширить какой-то функциональностью необходимо знать этот язык.
После поисков других вариантов я натнуля на k6, которым пользуется Mail.ru для проведения нагрузочного тестирования Tarantool. Меня очень заинтересовал данный инструмент по следующим причинам:
- имеет систему плагинов (пишутся на Go)
- сценарии тестированя пишутся как js код
- написан на Go
После этого, для эксперимента, я решил написать плагин для тестирования сервиса для приема ЕГТС пакетов.
Введение в k6
Установить k6 можно одним из следующих способов.
Далее потребуется js сценарий, в котором будет описаны действия для проведения тестирования. В общем виде он выглядит так:
// код для инициализации
export function setup() {
// функционла который должен выполниться перед стартом тестирования
}
export default function (data) {
// сценарий пользователя
}
export function teardown(data) {
// функционла который должен выполниться по завершению теста
}
Когда скрипт тестирования создан, необходимо его запустить с нужными параметрами тестирования. Это можно сделать либо при старте из консоли:
k6 run --vus 10 --duration 30s script.js
либо задав в файле тестирования переменную options
:
export let options = {
vus: 10,
duration: '30s',
};
Оснвными настройками являются:
vus
- кол-во одновременных пользователей (VU)duration
- время проведения тестаiterations
- кол-во итераций которые выполнит один VU
Настройки можно упорядочивать в различные сценарии, например:
export let options = {
stages: [
{ duration: '30s', vus: 2},
{ duration: '1m30s', vus: 10},
],
};
Кроме того возможно использовать готовые сценарии. В этом случае необходимо выбрать тип исполнителя и задать ему нужные настройки.
Исполнители могут быть следующих тестирования:
shared-iterations
- выполняет общее кол-во заданных итераций между всеми исполнителями (vu).per-vu-iterations
- каждый исполнитель выполняет точно заданное число итераций.constant-vus
- фиксированное число исполнителей выполняет как можно больше запросов в зананный период времени.ramping-vus
- произвольное число исполнителей выполняет как можно больше запросов в зананный период времениconstant-arrival-rate
- фиксированное кол-во итераций исполняемое в заданный период времени.ramping-arrival-rate
- произвольное кол-во итераций исполняемое в заданный период времени..externally-controlled
- контролируемое и мастабирование тестирование с возможность изменять параметры через api или cli во время проведения
Из коробки k6 умеет работать со следующими протоколами:
- HTTP/1.1
- HTTP/2
- WebSockets
- gRPC
Кроме того с помощью механизма плагинов можно реализовать поддержку любых других протоколов.
Разработка плагина на Go
Как я писал выше, для кастомного плагина я выбрал протокол EGTS, библиотека для работы с которым доступна на GitHub.
Для того чтобы наш плагин можно было вызвать из js скриптов необходимо создать следующий файл:
package egts
import (
"context"
"log"
"go.k6.io/k6/js/modules"
)
// регистрируем структуру через которую будет работать плагин
func init() {
modules.Register("k6/x/egts", new(Egts))
}
type Egts struct{}
// создание нового клиента
func (*Egts) NewClient(addr string, clientID uint32) *EgtsClient {
return NewClient(addr, clientID)
}
// функция отправки пакета на сервер
func (*Egts) SendPacket(ctx context.Context, client *EgtsClient, lat, lon float64, sensVal uint32, fuelLvl uint32) {
if err := client.SendPacket(ctx, lat, lon, sensVal, fuelLvl); err != nil {
log.Println(err)
}
}
Структура EgtsClient
выглядит следующим образом:
type EgtsClient struct {
Conn net.Conn
Client uint32
actualPID uint32
recordNumber uint32
}
func (c *EgtsClient) SendPacket(ctx context.Context, lat, lon float64, sensVal uint32, fuelLvl uint32) error {
state := lib.GetState(ctx)
if state == nil {
return errors.New("state is empty")
}
if c.Conn == nil {
return errors.New("empty connection")
}
p := c.createPacket(time.Now().UTC(), lat, lon, sensVal, fuelLvl)
receivedTime := time.Now().UTC()
n, err := c.Conn.Write(p)
if err != nil {
return err
}
stats.PushIfNotDone(ctx, state.Samples, stats.Sample{
Time: receivedTime,
Metric: metrics.DataSent,
Value: float64(n),
})
if n != len(p) {
return errors.New("sending not full packet")
}
response := make([]byte, 1024)
if n, err = c.Conn.Read(response); err != nil {
return err
}
now := time.Now().UTC()
stats.PushIfNotDone(ctx, state.Samples, stats.Sample{
Time: now,
Metric: metrics.DataReceived,
Value: float64(n),
})
ackPacket := egts.Package{}
if _, err = ackPacket.Decode(response[:n]); err != nil {
return err
}
ack, ok := ackPacket.ServicesFrameData.(*egts.PtResponse)
if !ok {
stats.PushIfNotDone(ctx, state.Samples, stats.Sample{
Time: now,
Metric: EgtsPacketFailed,
Value: 1.0,
})
return errors.New("incorrect ack packet")
}
// проверка корректности ответа от сервера
stats.PushIfNotDone(ctx, state.Samples, stats.Sample{
Time: now,
Metric: EgtsProcessTime,
Value: now.Sub(receivedTime).Seconds(),
})
stats.PushIfNotDone(ctx, state.Samples, stats.Sample{
Time: now,
Metric: EgtsPackets,
Value: 1.0,
})
return nil
}
func (c *EgtsClient) createPacket(ts time.Time, lat, lon float64, sensVal uint32, fuelLvl uint32) []byte {
// создание тестового пакета егтс
}
В листенге выше стоит обратить внимание на 2 функции:
lib.GetState
stats.PushIfNotDone
эти функции нужны чтобы работать с метриками в процессе тестирования. GetState берет текущее состояние, а PushIfNotDone добавляет в это состояние актуальное значение заданной метрики.
Метрики задаются следующим образом:
var (
EgtsPackets = stats.New("egts_packets", stats.Counter)
EgtsPacketFailed = stats.New("egts_packets_failed", stats.Rate)
EgtsProcessTime = stats.New("egts_packets_process_time", stats.Trend, stats.Time)
)
Метод New принимает имя метрики и ее тип. Типы могут быть следующие:
Counter
это тип для хранения счетчика;Gauge
это тип хранятся минимальное, максимальное и последнее добавленные к ней значения;Rate
это тип для расчета процентов от всех добаленных значений;Trend
этот тип для хранения статичстической информации (min, max и персентели).
Теперь чтобы скомпилировать k6 вместе с расширением необходимо выполнить следующие команды:
go install github.com/k6io/xk6/cmd/xk6@latest
xk6 build --with xk6-plugin-dtm="$(pwd)"
После чего получится бинарный файл k6 с установленным внутри плагином и именно его нужно будет запускать для проведения теста.
Полный код расширения xk6-plugin-egts.
Пример использования
Для примера я подготовил простой сценарий example.js
:
import egts from "k6/x/egts";
export let options = {
scenarios: {
scenario_1: {
executor: 'shared-iterations',
vus: 2,
iterations: 4,
},
scenario_2: {
executor: 'per-vu-iterations',
vus: 2,
iterations: 2,
},
}
};
// client testing tracks, where key is VU number
// point of track is array [lat, lon, sens_value, fuel_level]
// if sens_value or fuel_level equals 0 then sending simple packet whith coordinate section only
const data = {
0: [[55.55389399769574, 37.43236696287812, 1000, 1000], [55.55389399769574, 37.43236696287812, 1000, 1000]],
1: [[55.55389399769574, 37.43236696287812, 1000, 1000], [55.55389399769574, 37.43236696287812, 200, 200]]
}
//for each VU open connection for emulating device
export default () => {
let client = egts.newClient("127.0.0.1:6000", __VU);
data[__VU%2].forEach((rec) => {
egts.sendPacket(client, ...rec)
})
client.close()
};
В массиве data находится треки по конкретному 2-х типов для четных и не четных клиентов. Запись трека состоит из:
- широта
- долгота
- значение датчика
- уровень топлива
Также можно увидеть что скрипт тестирования включает в себя 2 сценария нагрузки shared-iterations и per-vu-iterations.
После запуска команды (важно чтобы k6 был через команду из предыдущего пункта):
k6 run example.js
На экране будет выдан результат:

Заключение
Впечатления от K6 остались сугобо положительные из-за простоты работы с ним и, в частности, от написания сценариев тестирования. Также понравилось легкость с которой под k6 создаются плагины, что делает его очень мощным инструментом.
comments powered by Disqus