Pic 3 Performance Testing with Apache jMeter

Performance Testing with Apache jMeter

Designing and implementing distributed systems, both customer-faced or just datacrunching farms, one is soon required to determine performance impacts and possible bottlenecks.

In this specific case, I wanted to gain information regarding the limits and scalability aspects of a customer-facing web application which also manages a high number of connected devices.

Why I chose Apache jMeter

The performance testing case I was working on made me opt for jMeter in the end, for the following reasons:

  • Developed in Java, supporting plugins in Java or Beanshell. It is unlikely to have a metering case not which cannot be met with Java. In this case, Java became a killer feature, as most modules were implemented in Java, so it was possible to integrate jMeter into the given scenario without writing gluecode.
  • Distributed by design. It is unlikely for a single machine to stress the system under test (SUT) enough to gain any useable information. Testing and stressing a distributed system requires controlled distributed load generators.
  • Easy to get load generators on demand. jMeter is used by many engineers, there are a lot of services that accept jMeter recipies and play them on their machines for cash.
  • jMeter is able to take jUnit tests and their performance metrics into account, too. This makes it possible to share tests between jMeter and the main testbench.
  • jMeter brings a sophisticated GUI for testplan generation and debugging and also supports headless operation.
  • Very flexible to configure.
  • It is easy to package a jMeter node inside a docker image, so it can also run on a cloud computing provider which allows the execution of docker.
  • Many built-in graph and reporting components, data export to 3rd party analysis tools is available.
  • Open-Source, Apache Project
  • - Finally: A wise man once said to me: “Long live the standards!”. jMeter can be considered as a defacto-standard swiss army knife for performance testing.

Tools and Terminae

jMeter uses a set of functional units for performing tests. After learning the vocabulary, the system is quite straightforward. The table below gives an overview on the jMeter modules and their purpose.

Aspect jMeter Components
Control Structures Threads, Logic Controllers
Controlling iteration speeds and timing Timers -> Constant Timer, Random Timer, Constant Throughput timer, …
Storing Configuration and State Data Variables
Creating Load and performing actions on Systems under Test Samplers -> HTTP, Websocket, FTP, HTTPm Beanshell, Java Code, JDBC, WS, SMTP,..
Altering or extracting sampled Data before sampler execution Pre-Processors -> Regex, XPath, JSON Path
Altering or extracting sampled Data, after sampler execution Post-Processors -> Regex, XPath, JSON Path
Verifying and asserting sampled Data Assertions -> Regex, Compare, XPath,…
Obtaining Information and reporting Listeners -> Graph, File, Table, …
Grouping and behaviour Logic controllers

Designing a Test Plan

jMeter manages its test plan in a tree structure, which favours the XML data format used on the filesystem. So, the whole testplan meets a structure of ordered nodes with 0<n<inf children. For example, a parallel execution of a certain set of nodes would be represented by a parent node with the functionality of a thread controller, same applies on loop controllers or conditional controllers.

As an example, a login on a standard user/password form would be represented in jMeter as follows:

  • ConfigurationManager.CSV> get Mock users from CSV file, read into variables
  • CookieManager
    • Sampler.Http> Retrieve Login page, fail on HTTP error
    • Assert.XML> Check if site was delivered successfully
    • Postprocessor.XML> Extract CSRF token and save to variable
    • Sampler.Http> Post to login form with POSTdata composed from previous requests
    • Assert.Headers> Check that Session ID is present

Analysis and Reporting

After running the testplan and listening for the metrics delivered by the samplers, jMeter compiles a set of prebuilt reports which gather a lot of information, in most cases every information required to derive the next actions. For instance, it is possible to graph the respone times, the error ratio and the response times in relation to the quantity of parallel accesses. It is also possible to export the data into csv/xml files or use the generated reportfiles for further analysis. An interesting approach is to pass the data into R and use R’s graphing and reporting tools for further analysis.

Automation

Even though jMeter brings a really impressive GUI, it can be fully operated from the commandline. So, it is no problem to script it and, for example, integrate it into a CI/CD pipeline and let a build fail if it does not meet the performance expectations.

Distribution

In a distributed jMeter installation, workers are called “servers” and masters “clients”. Both are connected via old-fashioned Java RMI, so, after setting up an appropiate communication foundation between servers and client(s), triggering a load/performance testing job on the master suffices to start the slaves and collect their metrics.

Test plan creation

The jMeter files (.jmx) are pure XML, so it is theoretically possible to write them manually or, more probably, generate programatically. In most cases, one would use the GUI to click a test and customize it with config files, environment variables or own tiny scripting, depending on the system under test.

Plugin development

If jMeter does not deliver a functionality out of the box, it is possible to add the functionality by scripting or plugins. This means, it is possible to divide any implementation of a Sampler, Pre/Postprocessor, Assertion or Listener into three classes:

Class A: Available out of the box Class B: Possible by jMeter/Beanshell scripting Class C: Only possible by developing an own plugin

Developing a Java Sampler

A test-case classified as C needs to be implemented as a plugin. Basically, every aspect of jMeter can be delegated to a Java plugin, so it would also be possible to use a Java class to implement a custom assertion. Nevertheless, I think that the most common case is implementing a custom sampler to wrap a test around a functionality which either is not available through a public API or has asynchronity/concurrency requirements jMeter itself cannot meet.

An easy way of implementing a custom sampler is fetching the dependencies via maven and providing an implementation to the jMeter Sampler API.

A very minimal Maven POM just includes a dependency expression to the ApacheJMeter_java artifact, in a real use-case one might want to add maven-assembly to create a fat bundle including all further dependencies, so a downloadable package can be built on a buildserver.

 1<?xml version="1.0" encoding="UTF-8"?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 3  <modelVersion>4.0.0</modelVersion>
 4  <groupId>...</groupId>
 5  <artifactId>...</artifactId>
 6  <version>1.0-SNAPSHOT</version>
 7  <packaging>jar</packaging>
 8  <properties>
 9    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
10    <maven.compiler.source>1.8</maven.compiler.source>
11    <maven.compiler.target>1.8</maven.compiler.target>
12  </properties>
13
14  <dependencies>
15    <dependency>
16      <groupId>org.apache.jmeter</groupId>
17      <artifactId>ApacheJMeter_java</artifactId>
18      <version>2.7</version>
19      <type>jar</type>
20    </dependency>  
21  </dependencies>
22</project>

The JavaRequest sampler main class needs to extend the AbstractJavaSamplerClient class and provide a tree (1 <= n <= inf) of SampleResults:

 1import org.apache.jmeter.config.Argument;
 2import org.apache.jmeter.config.Arguments;
 3import org.apache.jmeter.protocol.java.sampler.AbstractJavaSamplerClient;
 4import org.apache.jmeter.protocol.java.sampler.JavaSamplerContext;
 5import org.apache.jmeter.samplers.SampleResult;
 6//...
 7
 8public class MyCustomRequestSampler extends AbstractJavaSamplerClient implements Serializable {
 9  private static final Logger LOG = Logger.getLogger(MyCustomRequestSampler.class.getName());
10
11  @Override
12  public Arguments getDefaultParameters() {
13    Arguments a = new Arguments();
14
15    a.setArguments(
16            Arrays.stream(JMeterCallParameters.values())
17                    .map(item -> new Argument(item.name(), ""))
18                    .collect(Collectors.toList())
19    );
20    return a;
21  }
22
23  /**
24   * Our actual entrypoint   
25   * (you want to do this to be able to unittest the sampler!)
26   */
27  public SampleResult runTest(SamplerConfiguration config) {
28    SampleResult result = new SampleResult();
29
30    result.sampleStart();
31    try (...) {          
32      result.addSubResult(anyAdditionalResult);    
33      result.setResponseCodeOK();
34      result.setResponseMessageOK();
35      result.sampleEnd();
36    } catch (Exception e) {
37      LOG.log(Level.SEVERE, "exception={0} stackTrace={1}", new Object[]{e, e.getStackTrace().toString()});      
38      result.setSuccessful(false);
39      result.setResponseData(e.getLocalizedMessage(), "UTF-8");
40    }
41
42    return result;
43  }
44
45  /**
46   * Entry point for jMeter Request
47   *
48   * Calls our internal function with the configuration gathered from jMeter
49   * Context and returns its SamplerResult
50   *
51   * @param jsc -  from jMeter
52   * @return Sample Result for jMeter
53   */
54  @Override
55  public SampleResult runTest(JavaSamplerContext jsc) {
56    SamplerConfiguration config = new MyCfgBuilder(jsc).build();
57    return runTest(config);
58  }
59}

After deploying the build artifact to $JMETER/lib/ext and restarting jMeter, it is available and can be integrated into a testplan using the GUI.

Dockerizing jMeter

An easy way to deploy a distributed jMeter installation is providing a docker set which consists of a master and n slaves. In this setup, it is advisable to create a base image and derive both master and server. If the setup is supposed to run in distributed environments which do not neccessarily provide a registry, it is advisable to use an own lightweight mechanism, such as a list in Redis which is populated by the slaves as soon as they get invoked.

The base image:

 1FROM m9d/debian-withSsh:jessieLatest
 2MAINTAINER manfred dreese <>
 3
 4RUN apt-get update
 5
 6# Install basics
 7RUN apt-get install -y wget unzip tar software-properties-common
 8
 9# Install Java
10RUN add-apt-repository ppa:webupd8team/java && \
11    apt-get update &&\
12    echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | debconf-set-selections && \
13    apt-get install -y oracle-java8-installer
14
15# Install Download jMeter
16ENV JMETER_ROOT /opt/jmeter/
17ENV JMETER_BIN /opt/jmeter/bin/jmeter
18RUN wget -O /tmp/jmeter.tgz \
19  https://liam.aah.m9d.de/demo/binary-assets/third-party/apache-jmeter/3.1/apache-jmeter-3.1.tgz
20
21RUN mkdir $JMETER_ROOT && \
22  tar --directory /opt/jmeter --strip 1 -xzvf /tmp/jmeter.tgz
23
24# Pull jMeter Plugin
25RUN wget -O /tmp/InfraredPatternReactionPlugin.tgz \
26    https://jenkins.aah.m9d.de/InfraredPatternReactionPlugin-1.0-RELEASE-bundle.tar.gz && \
27  tar xzvf /tmp/InfraredPatternReactionPlugin.tgz -C /opt/jmeter/lib/ext

The master:

 1FROM m9d/jmeter-base
 2MAINTAINER manfred dreese <>
 3
 4# Install nginx (for retrieving reports)
 5RUN apt-get -y install nginx  && \
 6  rm /var/www/html/index.nginx-debian.html
 7COPY files/etc/nginx/sites-available/default /etc/nginx/sites-available/default
 8
 9# Install redis (for client registration)
10RUN apt-get -y install redis-server
11COPY files/etc/redis/redis.conf /etc/redis/redis.conf
12
13# Startscript
14COPY files/jmeter/run-testplan.sh /jmeter/run-testplan.sh
15COPY files/tmp/start.sh /tmp/start.sh
16COPY files/opt/jmeter/bin/user.properties /opt/jmeter/bin/user.properties
17
18ENTRYPOINT /tmp/start.sh
19
20EXPOSE 22
21EXPOSE 80

The server:

 1FROM m9d/jmeter-base
 2MAINTAINER manfred dreese <>
 3
 4# Install dependencies for client registration
 5RUN apt-get install -y redis-tools iproute2
 6
 7# Startscript
 8COPY files/tmp/start.sh /tmp/start.sh
 9COPY files/opt/jmeter/bin/user.properties /opt/jmeter/bin/user.properties
10
11ENTRYPOINT /tmp/start.sh

8 Minutes