Нагрузочный тест c помощью Go

в 19:17, , рубрики: golang, jmeter, Программирование, метки: ,

Добрый день, Хабрахабр.
Вы, вероятно, знакомы с JMeter. Если в кратце — очень удобный инструмент для проведения нагрузочного тестирования, имеет огромный функционал и много-много полезных фишек. Но статья не о нем.

С чего началось

В нашем проекте есть довольно нагруженный узел, JMeter помогал долгое время. Проффилирование и оптимизации дали свой профит, но все уперлось в маленькую проблему. JMeter не мог создать очень большой трафик, а если более точно, то после 10 секунд нужного нам режима, происходил OutOfMemory и тестирование прекращалось, в некоторых случаях проблемы не было, но скорость отправки запросов заметно уменьшалась, при этом загрузка CPU — 400%, решалось перезапуском программы. Пользоваться было крайне не удобно.
Итак, мы имеем проблему, и ее нужно решить, первое, что пришло в голову — сделать свой мини-тест, отвечающий минимальным требованиям. Давно было интересно попробовать Go на вкус. Так родилось приложение go-meter. При написании возникало очень много вопросов, ответов на которые либо не было, либо они не объясняли проблему, поэтому я решил поделиться опытом и примером рабочего кода, если Вам интересно, прошу подкат.

Предисловие

Думаю писать о том, что это за язык не имеет смысла, вы всегда можете посмотреть тур по языку, который раскрывает основные элементы. Как устанавливать и настраивать окружение тоже не стоит, в документации все написано на вполне понятном языке.
Почему выбрал именно Go? Тут есть несколько критериев, очень важных для меня: он быстро работает, кроссплатформенный, есть потоки, которыми просто управлять, необычный. Конечно, Вы скажите, что написать это можно и на любом другом языке. Я с Вами согласен, но задачей было не только написать, но и узнать что-то новое.

Приступим

Не долго думая было решено хранить профиль теста в JSON формате, после запуска приложения читается профиль и запускается тестирование. Во время тестирования в консоль выводится сводная таблица(время ответа, количество запросов в секунду и процентное отношение ошибок, предупреждений и удачных запросов). С JSON все просто, для этого нужно сделать структуры для каждого элемента, открыть и прочитать файл:

func (this *Settings) Load(fileName string) error {
	file, e := ioutil.ReadFile(fileName); if e != nil {
		return e
	}
	e = json.Unmarshal(file, this); if e != nil {
		return e
	}
	return nil
}

Пойдем дальше. После запуска нам нужно запустить N-потоков, и после отработки каждого из них агрегировать данные, далее выводить красиво в консоль. Для этого в этом интересном языке есть Channels. Своего рода «трубы» между разными потоками. Не нужно никаких синхронизаций, блокировок, все сделано за нас. Идея такая: поток отправляет запрос, определяет результат и об этом сообщает в основной поток, который в свою очередь ждет пока все потоки не отработают и выводить все полученные данные. Потоки у нас будут общаться по средствам передачи структуры:

type Status struct {
	IsError bool
	IsWarning bool
	IsSuccess bool
	Duration *time.Duration
	Size int64
	IsFinished bool
	Error *error
	FinishedAt *time.Time
	StartedAt *time.Time
}

Каждый поток у нас будет выполнять M-раз HTTP запрос к указанному ресурсу. Если у нас POST запрос, то еще отправляя определенные данные, которые хочет пользователь:

func StartThread(setts *settings.Settings, source *Source, c chan *Status){
	iteration := setts.Threads.Iteration
	//Формируем объект key, value для заголовков запроса
	header := map[string]string{}
	for _, s := range setts.Request.Headers {
		keyValue := regexp.MustCompile("=").Split(s, -1)
		header[keyValue[0]] = keyValue[1]
	}

	sourceLen := len(*source)

	//необходимый URL
	url := setts.Remote.Protocol + "://" + setts.Remote.Host + ":" + strconv.Itoa(setts.Remote.Port) + setts.Request.Uri
	if iteration < 0 {
		iteration = sourceLen
	}
	index := -1
	for ;iteration > 0; iteration-- {
		status := &Status{false, false, false, nil, 0, false, nil, nil, nil}
		index++
		if index >= sourceLen {
			if setts.Request.Source.RestartOnEOF {
				index = 0
			} else {
				index--
			}
		}
		//Получаем данные для отправки запроса
		var s *bytes.Buffer
		if strings.ToLower(setts.Request.Method) != "get" {
			s = bytes.NewBuffer((*source)[index])
		}
		//Создаем HTTP запрос
		req, err := http.NewRequest(setts.Request.Method, url, s); if err != nil {
			status.Error = &err
			status.IsError = true
			c <- status
			break
		}
		//Выставляем заголовки
		for k,v := range header {
			req.Header.Set(k,v)
		}
		//Засекаем время
		startTime := time.Now()
		//Отправляем запрос
		res, err := http.DefaultClient.Do(req); if err != nil {
			status.Error = &err
			status.IsError = true
			c <- status
			break
		}
		endTime := time.Now()
		//Записываем служебную информацию
		status.FinishedAt = &endTime
		status.StartedAt = &startTime
		diff := endTime.Sub(startTime)
		//Проверяем статус ответа и причисляем в одной из 3 групп (Error, Warning, Success)
		checkStatus(setts.Levels, res, diff, status)
		//Закрываем соединение
		ioutil.ReadAll(res.Body)
		res.Body.Close()
		//Оповещаем главный поток
		c <- status
		//Если установлена в настройках задержка, выполняем ее
		if setts.Threads.Delay > 0 {
			sleep := time.Duration(setts.Threads.Delay)
			time.Sleep(time.Millisecond * sleep)
		}
	}
	//Оповещаем главный поток о завершении работы
	status := &Status{false, false, false, nil, 0, true, nil, nil, nil}
	c <- status
}

Осталось только запустить наши потоки при старте программы и слушать от них данные

c := make(chan *Status, iteration * setts.Threads.Count)
for i := 0; i < setts.Threads.Count; i++{
	go StartThread(&setts, source, c)
}
for i := iteration * setts.Threads.Count; i>0 ; i-- {
	counter(<-c)
}
fmt.Println("Completed")
Вместо заключения

Это самые интересные моменты, на мой взгляд. Все исходики доступны на GitHub, там можно посмотреть весь цикл работы с примером использования. По факту, с данной задачей этот чудо язык справился с лихвой, при генерации трафика объемом в 3 раза больше чем было в случае с JMeter загрузка процессора редко превышает 15%.
Если будет интересно, расскажу о процессе написание HTTP Restfull Web сервиса с хранилищем в MongoDB и Redis.

Спасибо за внимание!

Автор: a696385

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js