Пользователь
0,0
рейтинг
1 февраля 2015 в 00:30

Разработка → Пишем свой нагрузочный тестер на Node.js tutorial

В посте речь пойдет о написании утилиты для нагрузочного тестирования HTTP сервисов на Node.js, а также описание самого инструмента и области его использования.

Предыстория


За выходные нужно было срочно провести нагрузочное нашего сервиса. Первым делом я отправился ставить Яндекс Танк, но оказалось, что у парней по прежнему все заточено под Debian. Ok Google, рабочая машина у меня на маке, виртуалку ставить ради этого совсем не хотелось, поэтому я пошел на тестовый сервер, где оказалась беда с зависимостями и недостаточно памяти. Достукиваться до админа в выходные совсем не хотелось, а руки все больше чесались написать несложную и интересную утилиту самому. Так появился Stress.
Я не отговариваю вас от танка или jMeter, но если нужен быстрый и простой (поставил-запустил) инструмент, надеюсь он вам пригодится.

Почему Node.js?


Во-первых, асинхронность языка поможет нам максимально упростить написание кода для одновременного выполнения запросов на одном ядре.
Во-вторых, удобный встроенный cluster для воркеров и канал связи для них.
В-третьих, встроенный http-сервер и socket.io для отчетов в браузере.

Расширяемость


Нерасширяемый инструмент — мертвый инструмент. В нашем случае может понадобиться кастомизация для:

  • обработки ответа от удаленного сервера
  • стратегии отправки запросов
  • агрегирования полученных результатов
  • рисования грасивых графиков в браузере


Все это модули вашей конкретной стратегии, которую я решил назвать атакером. Атакеров может быть много и их можно писать самим. Каждый атакер состоит из следующих модулей:
  • Dispatcher коммуницирует воркеры между собой и репортером
  • Reporter анализирует данные и формирует отчеты
  • Receiver анализирует тело ответа и считает статистику
  • Frontend рисует графики в браузере


На данный момент создан только один атакер Step. Его поведение аналогично танковскому step, но этого достаточно для большинства задач. Также он пишет в логи все запросы, агрегированные результаты и рисует график.

Написание кода


С виду несложная архитектура омрачается необходимостью работать с параллельными запросами. Как известно Node.js имеет только один рабочий тред и при запуске одновременного большого числа http запросов они начнут вставать в очереди, увеличивая латенси. Поэтому мы сразу форкаем воркеры на количество ядер и общаемся через встроенный канал JSON сообщениями.

Stress.prototype.fork = function (cb) {
     var self = this;
     var pings = 0;
    var worker;
     if (cluster.isMaster) {
       for (var i = 0; i < numCPUs; i++) {
         worker = cluster.fork();
         worker.on("message", function (msg) {
              var data = JSON.parse(msg);
              if (data.type === "ping") {
                   pings++;
                   if (pings === self.workers.length) cb(); // Все воркеры подняты, можно начинать
              } else {
                   self.attack.masterHandler(data); // Рабочее сообщение от воркера
              }
         });
         self.workers.push(worker); // Тут они у нас все
       }
     } else {
          process.send(JSON.stringify({type: "ping"}));
          process.on("message", function (msg) {
               var data = JSON.parse(msg);
            if (data.taskIndex === undefined) {
                process.send("{}");
            } else {
                workerInstance.run(data); // логика воркера
            }
          });
     }
};



Dispatcher призван ровно распределить запросы между всеми ядрами.
В конструкторе вызываем этот метод параллельно со всякими подготовительными делами в init:

async.parallel([

	this.init.bind(this),
	this.fork.bind(this)

], function () {
	if (cluster.isMaster) {
		self.next();
    }
});


Метод next начинает итерировать таски, указанные в конфиге:

Stress.prototype.next = function () {
	var task = this.tasks[this.currentTask];
	if (!task) {
		console.log("\nDone");
		process.exit();
	} else {
        var attacker = this.attackers[task.attack.type];
		this.attack = new attacker.dispatcher(this.workers, this.currentTask, this.attackers);
		this.attack.on("done", this.next.bind(this));
		this.attack.run();
		this.currentTask++;
	}
};


Dispatcher вместе с Reporter'ом заправляет всем, что касается текущего таска. Сам по себе воркер совсем простой и представляет собой обертку вокруг request

task.request.jar = request.jar(new FileCookieStore(config.cookieStore));

async.each(arr, function (_, next) {

    request(task.request, receiver.handle.bind(receiver, function (result) {

        result.pid = process.pid;
        result.reqs = reqs;
        result.url = task.request.url;
        result.duration = duration;

        reporter.logAll(result);

        next();
    }));

}, function () {
    process.send(JSON.stringify(receiver.report));
});


Как видно, все что лежит в объекте request является options для одноименной библиотеки, позволяя использовать в конфиге все ее возможности. Также при запросах используется tough-cookie-filestore, что позволит нам строить из тасков цепочки реквестов, ведь для полноценного тестирования часто нужно бывает проверить на нагрузки закрытые части сервиса.

Помимо прочего, Dispatcher может без труда прокидывать данные, которые сагрегировал для него Reporter куда угодно, например на клиент, где их ждет Google Chart.

Step.prototype.masterHandler = function (data) {
	
	this.answers++;

    if (Object.keys(data).length) this.summary.push(data);

	if (this.answers === this.workers.length) {
	    var aggregated = this.attacker.reporter.logAggregate(this.summary);
        this.attacker.frontend.emit("data", {
            aggregated: aggregated,
            step : this.currentStep
        });
	    this.answers = 0;
	    
	    this.currentStep = this.currentStep + this.task.attack.step;
	    
	    this.run();
	}
};


Если не забыть выставить в конфиге webReport = true и перейти по ссылке в консоли, можно смотреть как растет латенси при увеличивающихся RPS:



Установка и запуск


git clone https://github.com/yarax/stress
cd stress
npm i
npm start


В папке configs лежит дефолтный файл с реквестами к гуглу, там же можно создать свой конфиг и запустить как

npm start myConfigName


Буду рад, если кому-то статья окажется полезной, а также pull requests welcome :)
Роман @yarax
карма
9,0
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (12)

  • +4
    Статья об утилите для нагрузочного тестирования, но при этом не сказано, сколько запросов в секунду она может выдать на определенном железе, пока не упрётся во что-нибудь (процессор, память, канал). Было бы круто провести исследование и добавить подобную информацию к статье.
    • 0
      Да, тема важная и интересная, но для нее нужно время на анализ. Как будут конечные результаты, обязательно поделюсь
  • 0
    Так есть же ab на macos :) Ну вообще реквестирую websocket тестирование с интеграцией с socket.io. Последний раз когда этого касался все было довольно печально.
    Вообще для локального тестирования такие штуки по-моему не сильно подходят если какой-то сверхлегкий сервис, потому как тогда сам по себе тест может больше ресурсов кушать.
    • 0
      Не только ab, есть масса бенчмарков, но хотелось с максимальной кастомизацией аналитики ответов и отчетов, чтобы всегда была возможность простыми средствами влезть с отверткой
      • +2
        jmeter?
        • +1
          jMeter это ужас, извините. Я в основном натыкаюсь на статьи, как по-быстрому начать использовать jMeter: перегруженный интерфейс, запутанная документация, долгий старт. И снова повторюсь, что я не ставил цель сделать инструмент лучше jMeter, ab или танка, тем более он еще достаточно сырой. Скорее предложить js сообществу поучаствовать в создании удобного и простого инструмента
          • 0
            Это правда, да…
  • +1
    запустил на ноуте свой вебсервер: данная тулза жрет все доступное цп и обрабатывает примерно 2000 запросов в секунду, однопоточный ab выдает 120к рпс
    • 0
      2000 это очень мало. У меня на 4 ядрах не ест даже 5% CPU. Вы уверены, что проц ела именно нода? Важный момент, при больших рпс разумно отключать fullLog в конфиге. 120к отработало на 100% CPU за несколько секунд
      • 0
        htop

        выключение лога помогло незначительно, теперь примерно 2.3к рпс
        • 0
          Спасибо за отзыв и скриншот. Чуть позже напишу о сравнениях в рпс
  • 0
    По умолчанию node имеет ограничение 5 паралельных запросов к хосту
    nodejs.org/api/http.html#http_agent_maxsockets

    request.get(url, pool: {maxSockets: 243}, callback);
    

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.