Skip to content
  • ZipCode Api
  • Blog
  • About RedLine13
RedLine13
RedLine13
Primary Navigation Menu
Menu
  • Start Testing
  • Demo
  • Pricing
  • JMeter
  • Partners
  • Docs
    • Documentation Home
    • AWS Set Up for load testing
    • AWS Approval for Large Tests
    • PHP, NodeJS, Python Load Tests
    • Scalability
    • Jenkins Plugin Setup
    • Premium Features
    • Knowledge Base

Guest Post: Load Testing with Locust and JMeter on RedLine13

By: Eldad Uzman

Eldad Uzman, Automation Architect at Merck, is the guest author. You can read more from Eldad at Medium.

Abstract

In this article, I’ll present to you a case study of how you can integrate Locust into your Redline13 JMeter tests to get the maximum out of your testing environment, increase your load testing coverage while reducing the usage of infrastructure and thus reducing the costs.

JMeter in a nutshell

JMeter is a java-based load testing tool and one of the most long-standing tools on the market.

For the sake of this article, it’s main characteristic is it’s thread-based architecture, which means that for each virtual user (VU) JMeter will spawn a new thread in the operating system and each thread will perform the tasks related to the test flow of each virtual user.

For the most part, JMeter flows are scripted via its GUI, but it also allows some coding, namely by using groovy language.

Locust in a nutshell

Locust is a python-based load testing tool, and it is relatively new on the market.

Unlike JMeter, locust is powered by asynchronous io, which means that all virtual users run on a single thread, and concurrency is achieved not by spawning new threads in the operating system but rather by switching between IO contexts using a greenlet.

Hands on with locust

Since JMeter is already heavily covered in RedLine13 blog posts, in this section I’ll provide a short introduction to the various options available in locust.

Prerequisites

First, lets make sure we have python installed on our pc:

root@eldaduzman-lap:~# python  --version
Python 3.10.4
root@eldaduzman-lap:~#

Next, we can install locust using pip

root@eldaduzman-lap:~# pip install locust

Simple example

from itertools import count
from locust import HttpUser, between, task
USER_NUMBERS = count(1)

class WebsiteUser(HttpUser):
	wait_time = between(10, 12)
	host = "https://postman-echo.com"

	def __init__(self, *args, **kwargs):
		self._user_number = next(USER_NUMBERS)
		super().__init__(*args, **kwargs)

	@task
	def index(self):
		response = self.client.get(f"/get?var={self._user_number}", name="get_usr_num")
		var = response.json()['args']['var']
		response = self.client.get(f"/get?var={var}", name="get_var")

In this example, each virtual user is initiated with a unique sequence number (similar to the __threadNum variable in JMeter) and then it executes its task every 10-12 seconds

The task is to send a one get request with the sequence number, extract the number from the response, and then to send it as a different request.

To execute the locust test script, we can run the following command:

root@eldaduzman-lap:~# locust -f locustfile.py

This will create a web interface that you can open in your browser at `http://localhost:8089/’

We will spawn 20 virtual user and leave them to run for a while

Now with the test completed, we see that the average request per second was 3.7, which makes sense, as each user would send 2 requests every ~11 seconds.

Which means that 10.9 requests per minute will be sent from each user.

If we multiply that by 20, the total request per minute count should be 218.18, which would make ~3.63 request per second.

A more complex example

Let’s try a different scenario, instead of sending http requests, lets send a message to AWS SQS queue

Lets install first boto3 from pip:

root@eldaduzman-lap:~# pip install boto3 
import os
import time
import sys
import json
from itertools import count
from locust import User, between, task

import boto3
from boto3.exceptions import Boto3Error

REGION = os.environ.get("REGION")
ACCESS_KEY = os.environ.get("ACCESS_KEY")
SECRET_KEY = os.environ.get("SECRET_KEY")
QUEUE_NAME = os.environ.get("QUEUE_NAME")

assert (
    REGION and ACCESS_KEY and SECRET_KEY and QUEUE_NAME
), "One or more of the following environment variables is missing [REGION | ACCESS_KEY | SECRET_KEY | QUEUE_NAME]"

USER_NUMBERS = count(1)

def get_queue():
    """get boto3 sqs queue object"""
    return boto3.resource(
        "sqs",
        region_name=REGION,
        aws_access_key_id=ACCESS_KEY,
        aws_secret_access_key=SECRET_KEY,
    ).get_queue_by_name(QueueName=QUEUE_NAME)

class SQSUser(User):
    wait_time = between(120, 130)
    host = "https://postman-echo.com"

    def __init__(self, *args, **kwargs):
        self._user_number = next(USER_NUMBERS)
        self._queue = get_queue()
        super().__init__(*args, **kwargs)

    @task
    def send_sqs_message(self):
        """sends a message to SQS"""
        time_start = time.time() * 1000

        try:
            aws_response = self._queue.send_message(
                MessageBody=json.dumps({"user_id": self._user_number})
            )
            time_end = time.time() * 1000
            self.environment.events.request_success.fire(
                request_type="SQS",
                name="queue",
                response_time=(time_end - time_start),
                response_length=sys.getsizeof(aws_response),
            )
        except Boto3Error as ex:
            time_end = time.time() * 1000
            self.environment.events.request_failure.fire(
                request_type="SQS",
                name="queue",
                response_time=(time_end - time_start),
                exception=ex,
                response_length=0,
            )

In this example, the script assumes that the details for the SQS queue are stored in the environment variables.

Then it will use the details to generate connections to the sqs, and it will send messages every 120-130 seconds.

Now we can run this script with 50 users

And the results would be as follow:

Locust as library

In the examples above, we would be required to execute locust manually, now let’s run it as a python script

Let’s install another dependency, click, for parsing command line arguments.

root@eldaduzman-lap:~# pip install click
import os

import click
import gevent

from locustfile_sqs import SQSUser
from locust.env import Environment
from locust.html import get_html_report
from locust.env import Environment
from locust.stats import stats_printer, stats_history

@click.option(
    "--number-of-users",
    help="how many locust users to simulate",
    type=click.INT,
    required=True,
)
@click.option(
    "--duration-in-seconds",
    help="Duration of execution - if not provided, execution will run infinitely",
    type=click.INT,
    default=-1,
    required=False,
)
@click.option(
    "--output-path",
    help="path to output report",
    type=click.STRING,
    default="report.html",
    required=False,
)
def main(
    number_of_users,
    duration_in_seconds,
    output_path: str = "report.html",
):
    env = Environment(user_classes=[SQSUser])
    env.create_local_runner()
    assert env.runner, "must have a runner"
    try:

        gevent.spawn(stats_printer(env.stats))
        gevent.spawn(stats_history, env.runner)

        env.runner.start(number_of_users, spawn_rate=1)
        if duration_in_seconds > -1:
            gevent.spawn_later(duration_in_seconds, env.runner.quit)
        env.runner.greenlet.join()
    except KeyboardInterrupt:
        pass
    finally:

        dir_path = os.path.dirname(os.path.abspath(output_path))
        if not os.path.exists(dir_path):
            os.makedirs(dir_path)
        with open(output_path, "w", encoding="utf-8") as html_file:
            html_file.write(get_html_report(environment=env))

dispatch = click.command()(main)  # make pylint happy
if __name__ == "__main__":
    dispatch()

In this script, we use click to collect 3 command line argument:

  • number-of-users
  • duration-in-seconds
  • output-path

Then we take the SQSUser class we’ve created earlier and run it programmatically without the GUI.

At the end of the script, it will save the html report in the given path

To run this code, the command line can look something like this:

root@eldaduzman-lap:~# python main.py --number-of-users 50--duration-in-seconds 300 

So we spawned up 50 SQS users for 300 seconds.

The results is:

Locust as executable

So far, we’ve created a python script that runs locust programmatically.
However, to use the script, we need the target machine to have python installed and it must have all the prerequisites packages installed as well.

This might be a bit difficult to manage.
Ideally, we would like to have a self-contained executable which can be simply executed directly, and luckily this is possible by using pyinstaller!

Pyinstaller creates a self-contained file that wraps your python code with the python interpreter, libraries and other resources for you to execute in other machines of equal configuration.

This is made possible thanks to the new RedLine13 version which runs ubuntu version 22.04.

So lets first install pyinstaller using pip

root@eldaduzman-lap:~# pip install pyinstaller 

Now, we can wrap the main file as an executable with the following command:

root@eldaduzman-lap:~# pyinstaller --onefile main.py --add-data
venv/lib/python3.10/site-packages/locust/static:locust/static --add-data
venv/lib/python3.10/site-packages/locust/templates/:locust/templates

This creates the self-contained file with locust static files and templates.

The file is in the `dist` directory under the name `main`.

Now we can run this file directly:

root@eldaduzman-lap:~#./main --number-of-users 50 --duration-in-seconds 300 

Connecting all the dots

We’ve created a self-contined executable file, now we can take this file and run it from with in the context of JMeter 😊

Let’s take a look at the groovy code:

import org.apache.commons.io.IOUtils;
def do_command(command_args, uname=null){
    log.info(command_args);
    def proc = new ProcessBuilder( command_args.split(' ')
).redirectErrorStream(true);
    if (uname != null){
        proc.redirectOutput(new File('./output/'+uname));
        proc.redirectErrorStream(true);
    }
    Process process = proc.start();
    return process;
}
SampleResult.setIgnore()
def how_many_users = (vars.get("how_many_users").toInteger()*10).toString()
def duration_seconds = vars.get("how_many_seconds")
def exec_command = String.format("./main --number-of-users %s --duration-in-seconds
%s", how_many_users, duration_seconds);
log.info(exec_command)
log.info("chmod +x main ".execute().text)
do_command(exec_command ,'process.log')

In this script, we execute `main` with the inputs from the user defined variables:

For each JMeter user, locust will run 10 users and then the report will be restored in RedLine13’s output directory.

Load Testing with Locust and JMeter on RedLine13

Now lets run this from RedLine13:

Load Testing with Locust and JMeter on RedLine13

After the test is completed, lets look at the results:

Load Testing with Locust and JMeter on RedLine13

And we can also go into the output file and see the locust html report:

Why?

So, after the demo is completed, now we can go back to the question – why on earth would we go through all this trouble?

The different architectural style, thread-based vs asyncio-based, make these two tools excel in different domains.
For the most part, it seems that locust is better at large number of virtual users with low traffic, and JMeter is better at generating high traffic from a smaller number of users.

There could be some exceptions to the rule, but if you find yourself in a situation where you need to combine the two, it might be helpful for you to use this solution and reduce the costs of your load testing efforts.

Likewise, some test flows are easier to implement in locust, owing to the massively rich python eco-system.

On a final note

This case study illustrates RedLine13’s remarkable flexibility.

In a sense, RedLine13 provides not only with a test execution environment but you also get an experiment environment to explore different possibilities.

External links

  • locust
  • pyinstaller
  • click

Try RedLine13

You can sign up and try RedLine13 today for any load testing.

2022-12-19
Previous Post: Executing Gatling Tests with The SBT + Java RedLine13 Plugin
Next Post: Cost-Effective Cloud Load Testing

Recent Posts

  • JMeter XML Format Post Processor
  • Order of Elements in JMeter
  • The JMeter Synthesis Report
  • Using the JMeter Plugins Manager
  • JMeter Rotating JTL Listener

Related

  • JMeter XML Format Post Processor
  • Order of Elements in JMeter
  • The JMeter Synthesis Report
  • Using the JMeter Plugins Manager
  • JMeter Rotating JTL Listener
  • Using Test Fragments in JMeter Tests
  • Step-by-Step Guide to Testing with JMeter
  • Functional Testing vs Performance Testing
  • A Gentle Introduction to Load Testing
  • Using the JMeter Counter Element

© RedLine13, LLC | Privacy Policy | Contract
Contact Us: info@redline13.com

Designed using Responsive Brix. Powered by WordPress.