Разработка плагина k6 для работы с EGTS

Введение

Часто возникает задача провести нагрузочное тестирование какого-либо сервиса или системы. Из известных и распространенных инструментов для этой задачи есть 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