We write our load tester on Node.js

  • Tutorial
The post will focus on writing a utility for stress testing HTTP services on Node.js, as well as a description of the tool itself and the area of ​​its use.

Background


Over the weekend, it was urgent to carry out a load of our service. The first thing I went to install Yandex Tank, but it turned out that the guys still everything is imprisoned under Debian. Ok Google, I had a working machine on my Mac, I didn’t want to install a virtual machine for this, so I went to a test server, where there was a problem with dependencies and not enough memory. I didn’t feel like knocking at the administrator on the weekend, and my hands were scratching my face more and more to write a simple and interesting utility myself. So Stress appeared .
I do not dissuade you from the tank or jMeter, but if you need a quick and simple (set-up) tool, I hope it comes in handy.

Why Node.js?


Firstly, the asynchrony of the language will help us simplify the writing of the code for simultaneous execution of queries on one core.
Secondly, a convenient built-in cluster for workers and a communication channel for them.
Thirdly, the built-in http-server and socket.io for reports in the browser.

Extensibility


A non-expandable tool is a dead tool. In our case, you may need customization for:

  • processing a response from a remote server
  • request strategies
  • aggregation of results
  • drawing graphs in the browser


All these are modules of your specific strategy, which I decided to call an attacker. There can be many attackers and you can write them yourself. Each attacker consists of the following modules:
  • Dispatcher communicates between workers and the reporter
  • Reporter analyzes data and generates reports
  • Receiver analyzes the response body and reads statistics
  • Frontend draws graphics in a browser


At the moment, only one Step attacker has been created. Its behavior is similar to the tank step , but this is enough for most tasks. He also writes all queries, aggregated results to the logs and draws a graph.

Code writing


In appearance, the simple architecture is overshadowed by the need to work with parallel queries. As you know, Node.js has only one working thread, and when you start a large number of http requests at the same time, they will start to queue up, increasing latency. Therefore, we immediately fork workers for the number of cores and communicate via the built-in JSON channel with messages.

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 is designed to evenly distribute requests between all cores.
In the constructor, we call this method in parallel with all sorts of preparatory tasks in init:

async.parallel([
	this.init.bind(this),
	this.fork.bind(this)
], function () {
	if (cluster.isMaster) {
		self.next();
    }
});


The next method starts iterating the tasks specified in the config:

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, along with Reporter'om runs everything related to the current task. The worker itself is quite simple and is a wrapper around 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));
});


As you can see, all that lies in the request object is options for the library of the same name, allowing you to use all its features in the config. Also, tough-cookie-filestore is used for requests, which will allow us to build request chains from task, because for full testing it is often necessary to check the closed parts of the service for loads.

Among other things, Dispatcher can easily forward data that Reporter has aggregated for it anywhere, for example, to a client where Google Chart is waiting for them.

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();
	}
};


If you do not forget to set webReport = true in the config and follow the link in the console, you can watch how the latency grows with increasing RPS:



Installation and launch


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


In the configs folder there is a default file with Google requests, you can also create your own config file there and run as

npm start myConfigName


I will be glad if someone finds the article useful, as well as pull requests welcome :)

Also popular now: