Wie man Lasttests fuer APIs durchfuehrt


Wenn Sie jemals eine Webanwendung entwickelt haben, die fuer ein erhebliches Benutzervolumen ausgelegt ist, standen Sie vor der Frage, welche Ressourcen Ihre Anwendung benoetigt, um den erwarteten Datenverkehr mit den gewuenschten Service-Levels zu bewaeltigen. Performance- und Lasttests ermoeglichen es Ihnen zu verstehen, wie sich Ihre Anwendung in verschiedenen Nutzungsszenarien verhaelt, indem sie die folgenden Fragen beantworten:
Wie viele Ressourcen (CPU, RAM, Festplatte, Bandbreite, GPU usw.) verbraucht meine Anwendung zur Verarbeitung des Datenverkehrs?
Welche Infrastruktur benotige ich, um auf den erwarteten Datenverkehr zu reagieren?
Wie schnell ist meine Anwendung?
Wie schnell skaliert meine Infrastruktur? Welche Auswirkungen hat die Skalierung auf die Benutzer?
Ist die Leistung stabil oder verschlechtert sie sich im Laufe der Zeit?
Wie widerstandsfaehig ist meine Anwendung gegenueber Verkehrsspitzen?
Wie veraendert sich die Leistung meiner Anwendung mit der Einfuehrung neuer Funktionen?
Das Ziel dieses Beitrags ist es, Werkzeuge bereitzustellen, um diese Fragen fuer Ihre Anwendung zu beantworten. Wir beginnen mit einer theoretischen Einfuehrung in Performance- und Lasttests und gehen dann zu einem praktischen zweiten Teil ueber, in dem wir sehen werden, wie man Lasttests an einer API anhand eines Beispielprojekts durchfuehrt.
Teststrategie (Theoretischer Teil)
Bevor Sie mit der Erstellung des Testplans beginnen, muessen Sie analysieren, wie die API definiert ist und wie sie in der Produktion verwendet wird, um zu bestimmen, wie die Tests durchgefuehrt werden sollen.
In diesem speziellen Fall werden wir die API einer Webanwendung fuer einen Blog testen. Diese API besteht aus mehreren Endpoints fuer die Benutzer- und Artikelverwaltung. Ueberlegungen:
Da jeder Endpoint eine unterschiedliche Geschaeftslogik hat, werden deren Leistung und unterstuetzter Durchsatz voraussichtlich unterschiedlich sein.
Tests sollten auf der erwarteten Anzahl von Benutzern fuer unsere Anwendung basieren. Es macht keinen Sinn, mit 100.000 gleichzeitigen Benutzern zu testen, wenn unsere Anwendung wahrscheinlich 100 nicht ueberschreiten wird.
Zunaechst wird empfohlen, jeden Endpoint separat zu testen, um die Grenzen und Faehigkeiten jedes einzelnen isoliert zu verstehen. Als naechsten Schritt sollte man mit Aufrufmustern (an verschiedene Endpoints) testen, die reale Anwendungsfaelle reproduzieren (zum Beispiel ein Benutzer meldet sich an, listet dann Artikel auf, ruft 2 oder 3 bestimmte Artikel ab usw.).
Wenn die Anwendung noch nicht in Produktion ist, muessen wir Tests auf Basis von Hypothesen erstellen, von denen wir glauben, dass sie die Realitaet darstellen koennen. Sobald die Anwendung in Produktion bereitgestellt ist, koennen wir reale Nutzungsdaten verwenden, um die Tests zu aktualisieren.
Wenn das System Auto-Scaling hat, ermoeglicht der Test das Testen des Auto-Scalings und das Verstaendnis seiner Geschwindigkeit, Fehler zwischen Auto-Scaling-Ereignissen usw. In diesem speziellen Fall verfuegt die Anwendung, die wir testen werden, nicht ueber Auto-Scaling.
Was gemessen werden soll
Ressourcenverbrauch: CPU, RAM, Netzwerkverkehr, …
Aufruflatenz: die Latenz des Aufrufs (einschliesslich Antwort) an jeden API-Endpoint. Es gibt verschiedene Moeglichkeiten, die Latenz zu messen; normalerweise werden Perzentile verwendet. Wenn zum Beispiel p95 (oder 95. Perzentil) 85 ms betraegt, bedeutet dies, dass 95 % der Aufrufe in hoechstens dieser Zeit abgeschlossen werden.
Durchsatz, gemessen als die Anzahl der Aufrufe an einen Endpoint pro Sekunde (RPS oder Requests-per-Second), die das Backend unter verschiedenen Bedingungen akzeptiert.
Fehlerrate oder Anzahl der Aufrufe, die mit einem Fehler antworten, im Verhaeltnis zur Gesamtzahl der Aufrufe.
Arten von Performance-Tests
Innerhalb der Performance-Tests werden verschiedene Typen anhand der Last oder des Datenvolumens unterschieden. Ziel ist es, verschiedene Nutzungssituationen darzustellen, die unser System in der Produktion erleben kann:
Baseline- oder Durchschnittstests. Sie helfen zu verstehen, wie sich die API mit einer Anzahl von Aufrufen verhaelt, die wir als normal betrachten. In unserem Beispiel gehen wir davon aus, dass wir unter normalen Bedingungen 20 gleichzeitige Benutzer haben, die Anfragen an die API stellen.
Lasttests. Sie ermoeglichen es uns zu verstehen, wie die API reagiert, wenn erwartete Nutzungsspitzen auftreten. Wenn wir beispielsweise berechnet haben, dass wir zu Spitzenzeiten 50 Benutzer haben werden, die Anfragen an die API stellen, wird der Test ein Verkehrsmuster reproduzieren, das diesen Spitzenwert erreicht.
Stresstests. Sie helfen, die maximale Kapazitaet unserer API (gemessen in RPS, Anfragen pro Sekunde) mit der aktuellen Infrastruktur zu verstehen, wie sich das Auto-Scaling verhaelt (falls vorhanden), und wie sich die Leistung verschlechtert (Latenz) und die Fehlerrate steigt (bei fester Kapazitaet), wenn der Datenverkehr zunimmt. Einige Beispiele fuer Situationen, die ein System belasten koennten, waeren der Black Friday im E-Commerce oder ein Sportereignis bei einem Essenslieferdienst.
Spike-Tests. Sie ermoeglichen es zu sehen, wie sich das System bei sporadischen Verkehrsspitzen verhaelt. Und zu sehen, wie viele Aufrufe wir korrekt beantworten koennen und wie viele mit Fehlern. Sie ermoeglichen auch die Ueberpruefung, ob der Dienst widerstandsfaehig ist und aktiv bleibt oder umgekehrt abstuerzt. In der realen Welt koennen diese Spitzen aus verschiedenen Gruenden auftreten, zum Beispiel wenn jemand Bekanntes einen Tweet ueber unsere Website schreibt und viele Besuche gleichzeitig auftreten (auch bekannt als “HackerNews hug of death”).
Dauertest (Soak Testing). Sie ermoeglichen das Verstaendnis des API-Verhaltens ueber laengere Zeitraeume, die Tage oder Wochen dauern koennen. Sie zielen darauf ab zu pruefen, ob es Systemdegradationen gibt, die den Dienst mittel- bis langfristig beeintraechtigen koennten. Wenn wir zum Beispiel ein kleines Speicherleck in der Anwendung haben, koennte es Tage dauern, bis der gesamte zugewiesene RAM verbraucht ist, was wahrscheinlich einen Neustart des Dienstes und das Fehlschlagen aller Aufrufe verursachen wuerde, die der Dienst zu diesem Zeitpunkt bearbeitete (und Aufrufe, die waehrend der Zeit auftraten, bis der Dienst wieder verfuegbar ist, wenn keine anderen Dienstinstanzen vorhanden sind, um den Datenverkehr aufzufangen).
Ziele und Plan (Praktischer Teil)
Der Kuerze halber konzentrieren wir uns auf Baseline-Tests und Stresstests. Es ist jedoch nicht kompliziert, Last- und Dauertests aus den Informationen zu entwerfen, die wir teilen werden.
Um den Beitrag nicht zu sehr zu verlaengern, bestehen die Tests darin, einen einzelnen Endpoint zu testen (Artikelliste). Die Aufgabe, die Tests zu erweitern und an die eigene Anwendung und spezifische Anwendungsfaelle anzupassen, bleibt dem Leser ueberlassen.
Der Plan ist wie folgt:
- Beschreibung des Stacks und des Testprojekts
- Beschreibung der Umgebung
- Bereitstellung der Umgebung
- Baseline-Tests
- Stresstests
- Abschliessende Anmerkungen
1 — Beschreibung des Stacks und des Testprojekts
Fuer die Testausfuehrung haben wir uns entschieden, ein Backend in GO zu erstellen, das eine einfache REST-API implementiert und eine MariaDB-Datenbank fuer die Datenpersistenz verwendet. Den Code sowie die Bereitstellungsanweisungen finden Sie hier:
https://github.com/gerodp/blog-sample-backend-go-grafana
Hinweis: Dies ist ein Test-Backend nur fuer illustrative Zwecke und ist nicht fuer Produktionsumgebungen gedacht.
Andererseits haben wir ein weiteres Repository mit verschiedenen Tests erstellt, die wir im Folgenden erklaeren:
https://github.com/gerodp/blog-sample-perf-test-k6-grafana
Stack
- REST-API-Backend in GO mit GIN (HTTP-Server) und GORM (ORM) Bibliotheken
- Lasttest-Tool: Grafana K6.
- Systemueberwachung mit Prometheus und Grafana
- MariaDB-Datenbank
- Ausfuehrung und Orchestrierung mit Docker und Docker Compose
Ueber Grafana K6 — Lasttest-Tool
K6 ist eine Open-Source-Loesung, die von Grafana Labs entwickelt wurde. Wir haben sie ausgewaehlt, weil sie einfach zu bedienen ist, alle Arten von Performance-Tests unterstuetzt, eine gute Integration mit den Ueberwachungstools Grafana und Prometheus bietet und ueber eine ausgezeichnete Dokumentation in mehreren Sprachen verfuegt, darunter auch Spanisch.
Es gibt jedoch eine grosse Anzahl von Tools dieser Art, die ebenfalls verwendet werden koennten, wie zum Beispiel: JMeter, Locust, Taurus, Artillery oder andere.
2 — Beschreibung der Umgebung
Der Einfachheit halber werden wir das Backend, das die API implementiert, auf einer AWS EC2 bereitstellen. Diese Art der Bereitstellung ist sehr einfach und nicht fuer Produktionsumgebungen gedacht; in diesen Faellen wird empfohlen, andere Loesungen wie Cluster-Bereitstellung oder andere Alternativen zu erkunden.
In der README des Repositories koennen wir die Schritte lesen, um es auf einer EC2 oder einer kompatiblen Linux-Maschine zu installieren.
3 — Bereitstellung der Umgebung
Als Teil des Tests haben wir 2 differenzierte Komponenten: 1) das Backend, das die API implementiert, und 2) die K6-Lasttests
Bereitstellung der Backend-API-Implementierung
1 — Wir starten eine AWS EC2 mit Linux. In unserem Fall haben wir es auf einem t3.large und m5.large mit Ubuntu getestet, aber andere kleinere Instanzen funktionieren perfekt (es dauert nur etwas laenger, das Backend aufgrund der Go-Kompilierung zu starten). Und wir muessen ihr eine oeffentliche IP-Adresse zuweisen, um von aussen darauf zugreifen zu koennen.
2 — In der Security Group fuegen wir 2 eingehende Verkehrsregeln fuer die Ports 22 (SSH) und 9494 (Port, auf dem die API exponiert ist) von unserer Heim-IP oder der Maschine hinzu, von der aus wir die K6-Tests starten werden.

3 — Wir verbinden uns per SSH mit der Maschine und fuehren die folgenden Befehle aus (in der README):
https://github.com/gerodp/blog-sample-backend-go-grafana#deployment-in-aws-ec2
4 — Wir koennen einen SSH-Tunnel oeffnen, um auf Grafana zuzugreifen, mit diesem Befehl:
ssh -i "/path/to/pemfile" -N ubuntu@<PUBLIC_IP> -L 8800:localhost:3000
5 — Und wir gehen zu http://localhost:8800 mit einem Browser, um Grafana zu oeffnen.

Test-Bereitstellung
Wir koennen die Tests direkt auf unserem Rechner ausfuehren, wenn das zu testende System klein ist, oder eine dedizierte Infrastruktur starten, wenn das System groesser ist. K6 verfuegt ueber einen Kubernetes Operator, der dies ermoeglicht. Fuer diesen Beitrag werden wir sie der Einfachheit halber von unserem Rechner aus starten (in diesem Fall ein Mac mit M1-Chip).
1- Repository klonen:
https://github.com/gerodp/blog-sample-perf-test-k6-grafana
2- Einen Test starten, indem Sie diesen Befehl mit make ausfuehren:
API_URL=http://EC2_PUBLIC_IP:9494 TEST=testfile.js make start
Der Befehl startet mehrere Container:
- Einen Container mit K6, der den in testfile.js angegebenen Test ausfuehrt und auf Aenderungen in dieser Datei lauscht, um ihn bei jeder Speicherung neu zu starten
- Einen Container mit Prometheus und einen weiteren mit Grafana fuer die Ueberwachung
3- Grafana im Browser oeffnen unter http://localhost:3000
4- Wir koennen die Ausfuehrung des Test-Containers beenden, da wir sie im naechsten Abschnitt mit einem spezifischen Test erneut starten werden.
Hinweis: Wie wir sehen koennen, haben wir 2 Grafana-Instanzen, 1 fuer die API (mit Metriken zur Ressourcennutzung des API-Backends) und eine weitere fuer Tests (mit Metriken, die K6 ueber die Testausfuehrung generiert: Durchsatz, Latenzen, Fehlerraten usw.). Ab jetzt werden wir auf jede Grafana-Instanz verweisen, indem wir angeben, ob es sich um die fuer Tests oder die fuer die API handelt.
4 — Baseline-Tests
Nehmen wir an, dass unter normalen Bedingungen unser Blog etwa 20 gleichzeitige Benutzer haben wird, die einen Aufruf pro Sekunde an die Artikellisten-API machen (dies ist ein fiktives Szenario, da das reale Verkehrsmuster wahrscheinlich anders sein wird, aber es dient zur Veranschaulichung).
Wenn wir zum Repository mit den K6-Tests gehen und die Datei oeffnen:

Sehen wir den folgenden Inhalt:
import http from "k6/http";
import { check, sleep } from "k6";
export const options = {
thresholds: {
http_req_failed: ['rate<0.01'], // http errors should be less than 1%
http_req_duration: ['p(95)<200'], // 95% of requests should be below 200ms
},
scenarios: {
read_posts_constant: {
executor: 'constant-arrival-rate',
// Our test should last 10 minutes in total
duration: '600s',
// It should start 20 iterations per `timeUnit`. Note that iterations starting points
// will be evenly spread across the `timeUnit` period.
rate: 20,
// It should start `rate` iterations per second
timeUnit: '1s',
// It should preallocate 55 VUs before starting the test
preAllocatedVUs: 55,
// It is allowed to spin up to 80 maximum VUs to sustain the defined
// constant arrival rate.
maxVUs: 80,
}
}
};
//This function runs only once per Test
//and performs a login
export function setup() {
let loginParams = { username: 'testint1', password: 'testint1'};
let loginRes = http.post(__ENV.SERVICE_URL+'/login', JSON.stringify(loginParams), {
headers: { 'Content-Type': 'application/json' },
});
check(loginRes, {
"status is 200": (r) => r.status == 200,
});
return { token: loginRes.json().token };
}
export default function(data) {
const token = data.token;
let resp = http.get(__ENV.SERVICE_URL+"/auth/post?page_size=5",{
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token,
},
});
check(resp, {
"status is 200": (r) => r.status == 200,
});
sleep(1);
}
Wir werden nicht alle Code-Details erklaeren, da diese in der ausgezeichneten K6-Dokumentation nachgeschlagen werden koennen. Aber kommentieren wir die wichtigsten Teile:
thresholds: {
http_req_failed: ['rate<0.01'], // http errors should be less than 1%
http_req_duration: ['p(95)<200'], // 95% of requests should be below 200ms
},
K6 ermoeglicht es, unsere SLOs (Service Level Objectives) direkt im Code zu definieren. Fuer unseren Test haben wir festgelegt, dass die Latenz im 95. Perzentil unter 200 ms und die Fehlerrate unter 1 % liegen soll. Wenn eines der SLOs nicht erfuellt wird, wird K6 dies im Ergebnisbericht anzeigen, der am Ende der Testausfuehrung erscheint.
executor: 'constant-arrival-rate',
// Our test should last 10 minutes in total
duration: '600s',
// It should start 20 iterations per `timeUnit`. Note that iterations starting points
// will be evenly spread across the `timeUnit` period.
rate: 20,
// It should start `rate` iterations per second
timeUnit: '1s',
Da wir eine konstante Aufrufrate pro Sekunde wuenschen, waehlen wir diesen Executor, der garantiert, dass ‘rate’ Iterationen pro ’timeUnit’ gestartet werden. Um mehr ueber die verfuegbaren Executors zu erfahren, koennen wir die K6-Dokumentation besuchen.
Zum Starten fuehren wir folgenden Befehl aus:
API_URL=http://EC2_PUBLIC_IP:9494 TEST=baseline_test.js make start
Nach 10 Minuten erhalten wir das Testergebnis in den Logs:

Die Zeile ‘checks’ im Bericht sagt uns, dass 100 % der Pruefungen bestanden haben.

‘http_req_duration’ zeigt uns die Aufruflatenz (gemessen vom K6-Client; beachten Sie, dass die Latenz stark proportional zur Entfernung zwischen der Maschine, auf der der K6-Client laeuft, und der Maschine, auf der das getestete System laeuft, sein wird). Das p(095) oder 95. Perzentil betraegt 135,53 ms, was bedeutet, dass 95 % der Aufrufe in hoechstens dieser Zeit abgeschlossen wurden. Dies liegt unter dem von uns festgelegten SLO von p95<200 ms. Andererseits betraegt die durchschnittliche Latenz 105,28 ms.

Wenn wir zum Test-Grafana (http://localhost:3000) gehen und das Test Result Dashboard oeffnen, koennen wir Kurven mit RPS, aktiven VUs (K6 Virtual Users) und durchschnittlicher Antwortzeit (durchschnittliche Latenz) sehen.

Wie zu beobachten ist, wurden die RPS (orangefarbene gestrichelte Kurve) bei 20 RPS gehalten, wie wir im Test angegeben haben. Die durchschnittliche Antwortzeit (gruene Kurve) zeigt zwar auf den ersten Blick am Anfang groessere Veraenderungen und wird dann mehr oder weniger konstant, aber der Unterschied zwischen dem maximalen und minimalen Durchschnittswert betraegt weniger als 10 %, was den Erwartungen entspricht. Im Allgemeinen sind kleine Variationen in den Messungen zu erwarten, da viele Faktoren die Zeiten beeinflussen koennen, von der Systemlast zum Zeitpunkt des Tests bis zum Netzwerkzustand.
Andererseits koennen wir sehen, dass keine Fehler aufgetreten sind, oder mit anderen Worten, die Fehlerrate betrug 0 %, was bedeutet, dass wir das andere SLO erfuellt haben, das festlegte, dass die Fehlerrate unter 1 % der Aufrufe liegen sollte.

Bisher scheint alles darauf hinzudeuten, dass sich die API wie erwartet verhaelt, ohne Fehler und mit konstanter Antwortlatenz. Dies ist nicht abschliessend, dass es keine anderen Probleme geben kann, die die Zukunft beeinflussen koennten. Dafuer sind wir daran interessiert zu ueberpruefen, ob die CPU- und RAM-Ressourcennutzung der Dienste angemessen ist und keine Aufwaertstrends zeigt, die auf ein zukuenftiges Problem hindeuten koennten.
Wenn wir zum Cadvisor exporter Grafana-Dashboard gehen, finden wir Metriken zur Ressourcennutzung. Im ersten Diagramm koennen wir den CPU-Verbrauch fuer verschiedene Dienste beobachten. Wie zu sehen ist, ist die CPU-Nutzung fuer beide Dienste sehr gering, mit einem leichten Anstieg zu Beginn des Tests, dann stabilisiert sie sich.

Die Speichernutzung betraegt 18,4 MB fuer das Backend und 96,9 fuer MariaDB, und die Kurven bleiben flach.

Mit diesen Ueberpruefungen scheint sich das System in Bezug auf die Leistung stabil zu verhalten, und ohne Indikatoren, dass ein Problem mit 20 gleichzeitigen Benutzern und dem im Test definierten Nutzungsmuster auftreten koennte. Um jedoch eine groessere Sicherheit zu haben, dass sich das System ueber laengere Zeitraeume stabil verhaelt, wird empfohlen, Dauertests durchzufuehren.
Je nach den von uns verwendeten Technologien koennen wir die Ueberwachung auf weitere Metriken ausweiten. Wenn unsere Datenbank beispielsweise MySQL oder MariaDB ist, koennen wir einen Prometheus-Exporter verwenden, der eine grosse Anzahl von Metriken bereitstellt, von der Anzahl aktiver Verbindungen bis zur Abfragelatenz, und sie sind nuetzlich, um die Systemlast detaillierter zu kennen. Auf dieser Seite finden wir eine detaillierte Liste von Prometheus-Exportern fuer die Metrikerfassung basierend auf der Technologie.
Als Naechstes werden wir Stresstests durchfuehren, um zu sehen, wie sich die API verhaelt, wenn die Last viel hoeher ist.
5 — Stresstests
Wie wir im vorherigen Test beobachtet haben, verhaelt sich das System mit 20 Benutzern wie erwartet. Wir koennen beispielsweise mit 25 RPS beginnen und die Last weiter auf 50, 100, 150, … erhoehen. K6 unterstuetzt mehrere Moeglichkeiten, dies zu tun, aber um zu verstehen, wie es geht, muessen wir zunaechst einige Konzepte ueber K6 einfuehren.
K6 Virtual Users (VUs) und Iterationen
In K6 besteht jeder Virtual User aus einer While-Schleife, die die Testfunktion, die wir in der K6-Datei definiert haben, in einer Schleife ausfuehrt. Jede Ausfuehrung der Schleife wird als Iteration bezeichnet. Wenn wir 10 VUs definieren, haben wir 10 While-Schleifen, die Iterationen parallel ausfuehren. Ein VU fuehrt nur eine Iteration gleichzeitig aus, und die naechste Iteration beginnt nicht, bis die vorherige beendet ist. Daher wird die Anzahl der Iterationen, die ein VU ausfuehren kann, dadurch bestimmt, wie schnell oder langsam das getestete System antwortet. Das bedeutet, dass wir, wenn wir in unserem Test N Benutzer reproduzieren wollen, einen VU-Typ-Executor verwenden muessen, und wenn wir N RPS (Anfragen pro Sekunde) reproduzieren wollen, ist es ideal, einen Iterations-Typ-Executor zu verwenden, der die Anzahl der VUs dynamisch anpasst, um die im Test angegebenen Iterationen zu erreichen. K6 bietet verschiedene Arten von Executors, die bestimmen, wie Tests ausgefuehrt werden. Um mehr ueber die verfuegbaren Executors zu erfahren, koennen wir die K6-Dokumentation besuchen.
Fuer diesen speziellen Fall werden wir den Ramping Arrival Rate Executor verwenden, der es ermoeglicht, die Anzahl der Iterationen pro Zeiteinheit anzugeben, da wir daran interessiert sind, die Kapazitaet unseres Systems in Bezug auf RPS zu kennen. Da wir in unserem Test nur einen Aufruf machen, wird die Anzahl der Iterationen den RPS entsprechen.
Jede Stufe besteht aus 60 Sekunden Hochlauf plus 60 Sekunden mit stabiler Rate. Es ist wichtig zu beruecksichtigen, wie oft Prometheus Daten abtastet; wenn der Test zu schnell ist, kann Prometheus Metriken und Aenderungen nicht gut erfassen.
Stufe 1: 0 -> 25 RPS.
Um 25 RPS in 60 Sekunden zu erreichen, muss ich 25*60 Iterationen -> 1500 Iterationen ausfuehren
Stufe 2: 25 -> 50 RPS -> 50*60 = 3000 Iterationen
usw..
Wenn wir zum Repository mit den K6-Tests gehen und die Datei oeffnen:

import http from "k6/http";
import { check, sleep } from "k6";
export const options = {
thresholds: {
http_req_failed: ['rate<0.01'], // http errors should be less than 1%
http_req_duration: ['p(95)<200'], // 95% of requests should be below 200ms
},
scenarios: {
read_posts_stress_test: {
executor: 'ramping-arrival-rate',
// Our test with at a rate of 50 iterations started per `timeUnit` (e.g minute).
startRate: 25,
// It should start `startRate` iterations per second
timeUnit: '1m',
// It should preallocate 300 VUs before starting the test.
preAllocatedVUs: 300,
// It is allowed to spin up to 1500 maximum VUs in order to sustain the defined
// constant arrival rate.
maxVUs: 5000,
stages: [
{ target: 25*60, duration: '1m' },
{ target: 25*60, duration: '2m' },
{ target: 50*60, duration: '1m' },
{ target: 50*60, duration: '2m' },
{ target: 100*60, duration: '1m' },
{ target: 100*60, duration: '2m' },
{ target: 150*60, duration: '1m' },
{ target: 150*60, duration: '2m' },
{ target: 25*60, duration: '3m' },
],
}
}
};
//This function runs only once per Test
export function setup() {
let loginParams = { username: 'testint1', password: 'testint1'};
let loginRes = http.post(__ENV.SERVICE_URL+'/login', JSON.stringify(loginParams), {
headers: { 'Content-Type': 'application/json' },
});
check(loginRes, {
"status is 200": (r) => r.status == 200,
});
return { token: loginRes.json().token };
}
export default function(data) {
const token = data.token;
let resp = http.get(__ENV.SERVICE_URL+"/auth/post?page_size=5",{
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token,
},
});
check(resp, {
"status is 200": (r) => r.status == 200,
});
sleep(1);
}
Zum Starten fuehren wir folgenden Befehl aus:
API_URL=http://EC2_PUBLIC_IP:9494 TEST=stress_test.js make start
Nach 15 Minuten erhalten wir das Testergebnis in den Logs:

Wie in den Ergebnissen zu sehen ist, zeigt die letzte Zeile an, dass einige der definierten Schwellenwerte (mit SLOs) fehlgeschlagen sind. Die p95-Latenz betraegt 28,4 s, was weit ueber den als Ziel gesetzten 200 ms liegt. Allerdings ist die Fehlerrate von 0,27 % unter dem als Ziel gesetzten 1 % geblieben.
Wenn wir das Test Result Dashboard in Grafana oeffnen, koennen wir im Diagramm die Entwicklung der Latenz sehen, wenn die RPS steigen.


Wenn wir in das API Call Response Time Diagramm hineinzoomen, sehen wir den Moment, in dem die p95-Latenz den Schwellenwert von 200 ms (rote Linie) ueberschreitet. Wenn wir den RPS-Wert im anderen Diagramm zu diesem Zeitpunkt beobachten, sehen wir, dass er bei etwa 22 RPS liegt. Das bedeutet, dass wir mit der aktuellen Infrastruktur diese Grenze nicht ueberschreiten sollten, wenn wir das Latenz-SLO einhalten wollen.
Jenseits dieses Punktes funktioniert das System jedoch weiterhin ohne Fehler (obwohl mit hoher Latenz) bis etwa 115 RPS erreicht werden, wenn einige Fehler aufzutreten beginnen.

Eine weitere Beobachtung ist, dass die maximal erreichten RPS bei etwa 138 liegen, obwohl unser Ziel war, 150 RPS zu erreichen. Dies geschieht, weil die Latenz so stark ansteigt, dass die maximale Anzahl von VUs nicht ausreicht, um die angegebenen RPS zu erreichen. Hier koennten wir die maximale Anzahl von VUs im Test erhoehen, aber wir sollten beruecksichtigen, dass die Maschine, von der der Test gestartet wird, ausreichend Kapazitaet haben muss, um dies zu unterstuetzen. In einem groesseren System koennten Tests von einem Cluster aus gestartet und die Anzahl der Container skaliert werden. In jedem Fall reichen die erreichten RPS aus, um zu sehen, dass das System beginnt sich zu verschlechtern und Aufrufe mit Fehlern zurueckgibt, obwohl es nicht abstuerzt. Und wenn der Datenverkehr auf normale Werte zurueckkehrt, antwortet das System wieder innerhalb der festgelegten Schwellenwerte.
Andererseits koennen wir, wenn wir das API Grafana Dashboard oeffnen, ueberpruefen, dass Speicher und CPU im Vergleich zum Baseline-Test gestiegen sind, aber selbst bei hohen Lasten bleiben die Kurven flach, was darauf hinweist, dass wir keine Speicherleck-Probleme oder aehnliches fuer die getestete Funktionalitaet haben.
Als moegliche naechste Schritte koennte der Leser untersuchen, wo die Engpaesse liegen, die dazu fuehren, dass die Latenz ab bestimmten RPS ansteigt, und warum Fehler auftreten, die dazu fuehren, dass die API nicht mit Code 200 antwortet.
6 — Abschliessende Anmerkungen
In diesem Beitrag haben wir theoretische Konzepte zur Definition einer Last- und Performance-Teststrategie eingefuehrt und dann einige Beispiele gezeigt, wie einige dieser Tests an einer Anwendung durchgefuehrt werden koennen, die eine API implementiert. Obwohl wir uns auf APIs konzentriert haben, sind diese Tests auf andere Systeme uebertragbar, deren Last variabel ist.
Wir hoffen, dass der Leser das Gefuehl hat, mehr Werkzeuge zu haben, um die Fragen zu beantworten, die wir am Anfang gestellt haben:
Wie viele Ressourcen (CPU, RAM, Festplatte, Bandbreite, GPU usw.) verbraucht meine Anwendung zur Verarbeitung des Datenverkehrs?
Welche Infrastruktur benotige ich, um auf den erwarteten Datenverkehr zu reagieren?
Wie schnell ist meine Anwendung?
Wie schnell skaliert meine Infrastruktur? Welche Auswirkungen hat die Skalierung auf die Benutzer?
Ist die Leistung stabil oder verschlechtert sie sich im Laufe der Zeit?
Wie widerstandsfaehig ist meine Anwendung gegenueber Verkehrsspitzen?
Wie veraendert sich die Leistung meiner Anwendung mit der Einfuehrung neuer Funktionen?
Die Art der Tests, die wir fuer unser endgueltiges System implementieren, haengt stark von der Art des Systems, der erwarteten Nutzung und der Infrastruktur ab.
Ueber mich
In den letzten 4 Jahren habe ich als CTPO eines Startups gearbeitet, das KI- und Computer-Vision-basierte Produkte fuer verschiedene Sektoren wie Einzelhandel, Bauwesen, Medien und Transport entwickelt.
Ich habe an der Konzeption verschiedener Produkte, deren Entwicklung und Produktionsbereitstellung fuer Kunden in 8 Laendern in Europa, dem Nahen Osten und den Vereinigten Staaten teilgenommen.
Ich habe die Skalierbarkeit und Transformation des Teams geleitet, von einer anfaenglichen Gruppe von 4 Ingenieuren zu einer Abteilung von 25 Fachleuten, darunter Entwickler, Data Scientists, DevOps und Customer Success Manager.
Zuvor habe ich als leitender Architekt und Software-Ingenieur fuer verschiedene Kunden (Vodafone, LaLiga, Orange - Optiva Media) und grosse Unternehmen wie Amadeus gearbeitet.
Derzeit arbeite ich als Fractional CTO und helfe Startups und Unternehmen in jeder Phase, die Beduerfnisse und Probleme mit Technologie- und Produktstrategie, Teamproduktivitaet und der Befoerderung von Ingenieuren in Fuehrungspositionen haben.
Wenn Sie daran interessiert sind zu erkunden, wie ein Fractional CTO Ihnen bei bestimmten Aspekten Ihres Unternehmens/Startups helfen kann, koennen Sie einen Anruf vereinbaren.