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

от автора

Добрый день, Хабрахабр.
Вы, вероятно, знакомы с 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.

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

ссылка на оригинал статьи http://habrahabr.ru/post/187212/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *