Load testing met JMeter en AWS

Hoeveel kan jouw applicatie aan?

Over het thema “testen” wordt steeds vaker geschreven en gesproken. Frameworks zoals “Robot” die gebruikt kunnen worden voor end-to-end testing komen frequenter voor in projecten. Dit in combinatie met unit testing zorgt voor een goede bewaking van de codekwaliteit tegen regressie. Deze technieken testen verschillende flows van jouw applicatie als één gebruiker, maar hoe goed werkt jouw applicatie als tien gebruikers tegelijkertijd dezelfde flow doorlopen? En honderd? En duizend? Al verwacht je dat er maar twintig mensen jouw applicatie gaan gebruiken, ook dan is het de moeite waard om een load test uit te voeren om er zeker van te zijn dat alles goed gaat draaien onder die omstandigheden.

Door: Evertson Croes

Bij onze projecten hebben wij onverwachte problemen gevonden met onze netwerkinfrastructuur en configuratie, applicatielogica en meer, door het uitvoeren van load tests. Hierdoor konden we ze oplossen voordat ze grote impact hadden op onze projecten. Vaak wordt hier niet naar gekeken en als het aan bod komt, is het dan te laat. In dit artikel wil ik een eenvoudige en goedkope manier delen om load tests te gaan draaien.

JMeter

JMeter is een open source Java-applicatie die gebruikt kan worden voor load testing. Als je JMeter voor het eerst opstart, zie je alleen een leeg Test Plan component. Rechtsklikken op de Test Plan geeft je de mogelijkheid om je test plan uit te breiden. Hier gaan wij componenten aan toevoegen om onze load test te definiëren.

Voorbeeldplan

Een voorbeeldtestplan voor het load testen van een REST API ziet eruit zoals hier:

Test plan: De root component van de test plan

  • CSV data set config: Hiermee kan je een csv file gebruiken als input voor je test. Denk aan een lijst van auth tokens voor de headers.
  • User defined variables: Hier kan je een lijst van variabelen definiëren die overal in de test gebruikt kunnen worden. Variabelen kunnen gebruikt worden met deze notatie: $(variabeleNaam).
  • Thread group: Hier geef je aan hoeveel threads parallel moeten gaan draaien. In het geval van dit plan: hoeveel HTTP requests parallel moeten gaan draaien.
    • Loop controller: Hiermee kan je een “loop” instellen, waardoor alle stappen binnen de loop controller een aantal keer herhaald wordt.
      • Constant timer: Gebruik dit om een delay te zetten. Als je niet een delay hier zet zal JMeter de HTTP requests allemaal direct achter elkaar afvuren.
      • HTTP request: Hier staat alle informatie over een specifieke HTTP request zoals de hostname.
        • HTTP header manager: Deze gebruik je om headers toe te voegen aan de HTTP requests zoals auth tokens.
      • View results in table: Deze kan gebruikt worden om de resultaten te zien op een hoog niveau over de hele test. Je kan informatie zien zoals aantal succesvolle en gefaalde calls, en gemiddelde latency.
      • View results in tree: Deze kan gebruikt worden om de resultaten per HTTP request te zien. Hier kan je zien per request hoe lang het geduurd heeft of waarom het niet succesvol was.

 Met behulp van de thread group, loop controller en constant timer kunnen wij ervoor zorgen dat wij een aantal requests per seconde kunnen sturen naar een applicatie. Al deze configuraties kunnen wij geven aan de “user defined variables” zodat wij dat allemaal op één plek kunnen veranderen per acceptatie criteria.

Testplan draaien

Zorg dat je in de HTTP sampler naar een server verwijst die je wilt gaan testen. Je kan om te beginnen ook naar een server verwijzen die lokaal draait. De test plan draaien kan in de GUI via de “Play” knop. De test gaat op dat moment draaien met de ingestelde configuraties. Het resultaat kan je vinden in de “View results in tree/table” component. In afbeelding 2 zie je een voorbeeld met 1 thread en 10 messages (loops).

Dit was een test met een kleine load. Als je een echte load test wilt draaien, dan wordt er aangeraden om de test te draaien vanaf de command line, omdat dit minder resources kost. De test plan wordt opgeslagen in een .jmx file die aangeroepen zoals in Listing 1.

jmeter -n \
-t=test.jmx \
-j=test.log \
-l=test.xml \
-Jjmeter.save.saveservice.output_format=xml \
-Jjmeter.save.saveservice.response_data=true \
-Jjmeter.save.saveservice.samplerData=true \
-JnumberOfThreads=1 \
-JrampUpTime=1 \
-Jprotocol=https \
-Jhost=<<enter host server name here>> \
-Jpath=<<enter path here, example “/resource”>> \
-JnumberOfMessages=10 \
-JdelayBetweenMessages=1000 \

Listing 1.

De “-n” parameter geeft aan dat je JMeter wilt draaien in de “Non-GUI” mode. Daarna geef je de locatie aan van de test script en waar de log en xml file opgeslagen mag worden. De volgende drie parameters geven aan wat in de xml file komt te staan. Vanaf dit punt geven wij dezelfde variabelen aan die wij in het testplan bij de “User Defined Variables” hebben opgegeven. Dit kan alleen als je variabelen in de test definieert met deze notatie:

${__P(numberOfThreads,10)}

Waarbij “numberOfThreads” de parameter die gegeven kan worden gegeven vanaf de command line is en 10 de default waarde als dit niet doorgegeven is in de command line.

Als je de test via de command line draait, wordt er in de logging aangegeven welk percentage van de requests geslaagd is. Voor meer informatie zou je de xml file kunnen bekijken, die informatie per request bevat.

Acceptatiecriteria

Het is belangrijk om de acceptatiecriteria te bepalen voor je test. Hoeveel users per seconde wil je testen? Stel dat wij 100 users per seconde willen testen, kunnen wij de numberOfThreads (Thread group) op 100 zetten en de delayBetweenMessages (Constant timer) op 1000 (milliseconde). Om de test langer te laten draaien dan 1 seconde, kunnen wij de numberOfMessages (Loop Controller) zetten op 10. Met deze configuraties hebben wij een test die 100 requests per seconde gaat sturen naar onze server gedurende 10 seconden.

Wat is een acceptabele performance van je applicatie? In de “advanced” instellingen van de HTTP sampler kunnen wij een timeout aangeven. Stel dat wij de volgende acceptatiecriteria hebben: “Bij 100 gebruikers per seconde mogen de requests maximaal 500ms duren”. Dan kunnen wij de timeout in de HTTP sampler zetten op 500. Het succespercentage van de load test geeft in dit geval aan in hoeverre onze applicatie aan onze criteria voldoet.

Amazon Web Services (AWS)

Nu hebben wij een test die wij via de command line kunnen draaien met verschillende instellingen. De mogelijkheid bestaat dat de gewenste load op het systeem zo groot wordt dat je de test niet meer vanaf één machine/PC/laptop kan draaien. Je zou kunnen gaan schalen door meerdere laptops de test tegelijkertijd te laten draaien. Een alternatief is om de Cloud te gebruiken om je JMeter instanties te schalen. In dit onderdeel leg ik uit hoe je dit op AWS kan doen.

Wij gaan Amazon Elastic Container Service (ECS) hiervoor gebruiken. Voordat wij met ECS aan de gang kunnen, moeten wij eerst een Docker image bouwen en deze pushen naar de Elastic Container Registry (ECR).

Docker image

In listing 2 zie je een voorbeeld van een Dockerfile om mee te beginnen.

FROM openjdk:8-jre-alpine3.7
RUN apk update && apk add ca-certificates wget && update-ca-certificates
ENV JMETER_HOME=/usr/share/apache-jmeter \
JMETER_VERSION=5.2.1 \
TEST_SCRIPT_FILE=/var/jmeter/test.jmx \
TEST_LOG_FILE=/var/jmeter/test.log \
TEST_RESULTS_FILE=/var/jmeter/test-result.xml \
TOKEN_FILE=/var/jmeter/tokens.csv \
PATH="~/.local/bin:$PATH" \
NUMBER_OF_THREADS=1 \
RAMP_UP_TIME=1 \
PROTOCOL=https \
HOST=<<enter host server name here>> \
RESOURCE_PATH=test/test \
NUMBER_OF_MESSAGES=10 \
DELAY_BETWEEN_MESSAGES=1000
RUN wget http://archive.apache.org/dist/jmeter/binaries/apache-jmeter-${JMETER_VERSION}.tgz && \
tar zxvf apache-jmeter-${JMETER_VERSION}.tgz && rm -f apache-jmeter-${JMETER_VERSION}.tgz && mv apache-jmeter-${JMETER_VERSION} ${JMETER_HOME}

COPY test.jmx ${TEST_SCRIPT_FILE}
COPY tokens.csv ${TOKEN_FILE}
EXPOSE 443

CMD echo -n > $TEST_LOG_FILE && \
echo -n > $TEST_RESULTS_FILE && \
$JMETER_HOME/bin/jmeter -n \
-t $TEST_SCRIPT_FILE \
-j $TEST_LOG_FILE \
-l $TEST_RESULTS_FILE \
-Jjmeter.save.saveservice.output_format=xml \
-Jjmeter.save.saveservice.response_data=true \
-Jjmeter.save.saveservice.samplerData=true \
-JnumberOfThreads=$NUMBER_OF_THREADS \
-JrampUpTime=$RAMP_UP_TIME \
-Jprotocol=$PROTOCOL \
-Jhost=$HOST \
-Jpath=$RESOURCE_PATH \
-JnumberOfMessages=$NUMBER_OF_MESSAGES \
-JdelayBetweenMessages=DELAY_BETWEEN_MESSAGES \
-JtokenFile=$TOKEN_FILE && \
echo -e "\n\n===== TEST LOGS =====\n\n" && \
cat $TEST_LOG_FILE && \
echo -e "\n\n===== TEST RESULTS =====\n\n" && \
cat $TEST_RESULTS_FILE

Deze Dockerfile maakt een image die al onze JMeter properties als environment variables aanmaakt. Vervolgens gaat hij JMeter zelf downloaden en in de container zetten. Als laatste gaat hij de JMeter script draaien met alle parameters. De log en xml files worden ook gelogd.

Elastic Container Registry (ECR)

Nu dat wij een Dockerfile hebben die alle dependencies installeert en de test draait, kunnen wij een Docker image naar AWS sturen. Dit kan met de Elastic Container Registry (ECR) service. In de ECR console, klik op “create repository” en kies een naam. Vervolgens is de repository aangemaakt. Klik op de naam van de repository en klik op “view push commands”. Hier staat precies in wat je moet doen (copy paste van commando’s) om een Docker image te sturen naar ECR. Het resultaat is dat je een image krijgt te zien in jouw repository, zie onderstaande afbeelding.

 

Elastic Container Service (ECS)

De Elastic Container service is een AWS managed service om containers te draaien. Met behulp van deze service gaan wij meerdere containers naast elkaar draaien die dezelfde JMeter test gaan afvuren. Deze containers zijn gebaseerd op de image die wij in de vorige stap hebben gepusht naar ECR.

Task definition

Om deze containers te draaien, moeten wij een “task” definiëren in ECS. Klik op Task Definition in de ECS console om te beginnen. Maak een nieuwe aan en kies voor “Fargate”.

Het verschil tussen de Fargate en EC2 mode is het volgende. Bij de EC2 variant moet je zelf ervoor zorgen dat je eerst een aantal AWS EC2 instanties opstart waar je je test op wilt gaan draaien. Met Fargate geef je alleen aan hoeveel memory en CPU de taak nodig heeft en hoeveel van de taken je wilt draaien. Amazon regelt zelf dat er genoeg resources opgestart wordt om jouw taken te draaien.

Wij gaan onze taak op de volgende manier instellen (als een instelling niet hier gemeld wordt, kan je het negeren):

  • Task Definition Name: loadtest
  • Task memory (GB): 2GB
  • Task CPU (vCPU): 1vCPU
  • Add container:
    • Container name: load testing
    • Image: Ga naar je image die je geupload hebt in ECR en plak hier de URI
    • Memory Limits (MiB): Soft limit, 2048
    • Port mappings: 443, tcp
    • CPU units: 1
    • Environment variables: Voeg alle environment variables die je in je Dockerfile hebt gedefinieerd. Voorbeeld: NUMBER_OF_THREADS met value 1.

De task memory en CPU kunnen iets hoger dan wat wij nu hebben ingesteld. Na het uitvoeren van de test kan je de metrics inzien van je test en beslissen of je meer of minder memory of CPU wilt gebruiken.

Cluster

Op dit punt hebben wij een JMeter test, een Docker image die de JMeter test bevat en een Task Definition die naar de Docker image refereert. Nu moeten wij een Fargate cluster gaan opzetten waar de containers op gaan draaien. Klik op clusters in ECS en klik op “Create cluster” en kies voor “Networking only”. Geef de cluster een naam en klik op “Create”. Nu zijn we klaar om te gaan testen!

Testen

Klik op de net aangemaakte cluster en selecteer de “Tasks” tab. Klik op “Run new Task”. Selecteer de aangemaakte Task definition en cluster. Selecteer de default VPC en Subnet. Klap vervolgens de “Advanced Options” uit om alle instelbare environment variables te zien en aan te passen.

Hiermee kan je verschillende configuraties van load uitproberen zonder dat je de JMeter test, Dockerfile en Task definition hoeft te wijzigen. Klik op “Run Task” om de test te starten.

In de cluster krijg je een nieuwe taak te zien, zie hieronder.

Of de load test gelukt is kunnen wij hier niet zien. Wij moeten naar de logging gaan kijken. Alle logging wordt automatisch naar Cloudwatch gestuurd.

Cloudwatch

De logging van de test wordt automatisch gestuurd naar Cloudwatch. Ga naar de CloudWatch console op AWS, klik op “log groups” en zoek op “/ecs/<naam van je container>/”. Daar wordt alle JMeter logging, de log file en de .xml file getoond. De JMeter logging geeft al initieel aan welk percentage voor die container geslaagd is.

Als je geïnteresseerd bent in het resultaat van meerdere containers, kan je gebruik maken van “Insights”. Met Cloudwatch Insights kan je queries schrijven die over meerdere logs gaan en statistieken berekenen. Stel dat je de gemiddeld latency wilt weten voor een test, zou je de volgende query kunnen draaien:

fields @latency
| filter (@message like 'lb="HTTP Request"')
| parse @message 't="*" ' as @latency
| stats avg(@latency) as @averageLatency by bin(1s)

Voor meer informatie zie de Cloudwatch Insights documentatie.

Conclusie

In dit artikel hebben wij een JMeter test aangemaakt. Vervolgens hebben wij een Docker image aangemaakt die alle dependencies installeert voor het draaien van de JMeter test en de JMeter test draait. Deze image hebben wij gepusht naar AWS ECR. Daarna hebben wij een Task Definition aangemaakt die gebruik maakt van de Docker image. Als laatste hebben wij de taak op een ECS cluster gedraaid en de resultaten ingezien op CloudWatch.

Deze setup is instelbaar op verschillende manieren en elke component kan vervangen worden door jouw favoriete tool en Cloud provider. Het belangrijkste is dat je gaat nadenken over load testen. Daar hebben jij en je klant veel aan. Ik hoop dat dit artikel jou heeft gemotiveerd om meer aandacht te gaan geven aan load testing.

 

Heb je behoefte aan meer informatie? Ik heb een meer uitgebreide blog op DZone en een video van Luminis DevCon 2019 op YouTube.

There is also a GitLab repository with the examples used for this blog.