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