Lasttests mit k6 (PRX-36)
Drei k6-Szenarien gegen den laufenden docker-compose-Stack (siehe Repo-Root
docker-compose.yml) mit echten JWTs aus Keycloak — keine gemockte Auth, keine gemockte
Datenbank.
k6 einfach erklärt
k6 ist ein Lasttest-Werkzeug. Es simuliert viele gleichzeitige Benutzer, die HTTP-Aufrufe
gegen die Anwendung senden. Dadurch prüfen wir nicht nur, ob ein Endpunkt funktioniert, sondern
auch, wie schnell und stabil er unter paralleler Nutzung bleibt.
- VU (Virtual User): ein virtueller Benutzer. Mehr VUs bedeuten mehr parallele Nutzer.
- Iteration: ein vollständiger Durchlauf eines Tests durch einen virtuellen Benutzer.
- Request: ein einzelner HTTP-Aufruf an das Backend.
- Fehlerrate: Anteil der fehlgeschlagenen HTTP-Aufrufe; Ziel ist
0 %. - p95: 95 % aller Aufrufe waren höchstens so schnell wie dieser Wert. Die langsamsten 5 %
werden dabei nicht versteckt, sondern getrennt betrachtet.
- Threshold: ein automatisch geprüftes Ziel, zum Beispiel
p95 < 800 ms.
k6 verändert den Anwendungscode nicht. Die Tests erzeugen aber bewusst echte Testdaten in der
lokalen Datenbank. Deshalb niemals gegen eine produktive Umgebung ausführen.
Zusammenhang zwischen k6 und Keycloak
k6 ist nur der Benutzer-Simulator. Es ersetzt Keycloak nicht und umgeht die Security nicht.
Da die PROCUREX-REST-APIs geschützt sind, benötigt jeder API-Aufruf ein gültiges JWT von
Keycloak.
Der Ablauf eines Lasttests ist:
k6-Skript
│ 1. Benutzername + Passwort
▼
Keycloak (Realm: procurex, Client: procurex-loadtest)
│ 2. Access Token / JWT
▼
k6-Skript
│ 3. Authorization: Bearer <JWT>
▼
Traefik / PROCUREX-API
│ 4. JWT-Signatur und Rollen werden geprüft
▼
Geschützter REST-Endpunkt
Was passiert technisch?
setup()oder der Testablauf ruftloadtests/lib/auth.jsauf.- k6 sendet Benutzername, Passwort,
client_id=procurex-loadtestund
grant_type=password an den Keycloak-Token-Endpunkt.
- Keycloak prüft die Zugangsdaten und gibt ein kurzlebiges Access Token als JWT zurück.
- Das Skript übergibt dieses Token bei jedem API-Aufruf im Header
Authorization: Bearer <JWT>.
- Die PROCUREX-API beziehungsweise der Security-Filter prüft Signatur, Ablaufzeit und Rollen.
Erst danach wird der fachliche Endpunkt ausgeführt.
Der Lasttest misst damit den realistischen Zugriff eines authentifizierten Benutzers. Die
Token-Anforderung selbst ist normalerweise nicht Teil der Latenzmessung des fachlichen
Endpunkts: Das Token wird in setup() einmal pro k6-Testlauf oder pro benötigter Rolle geholt
und anschließend wiederverwendet.
Warum gibt es einen eigenen Keycloak-Client?
Für die Lasttests wird bewusst procurex-loadtest verwendet und nicht der Frontend-Client
procurex-frontend. Der Loadtest läuft ohne Browser und nutzt deshalb Direct Access Grants
(Password Grant). Der Frontend-Client bleibt beim vorgesehenen Authorization-Code-Flow mit PKCE.
So bleibt die echte SPA-Login-Konfiguration unverändert.
Die Rollen werden durch unterschiedliche Testbenutzer abgebildet:
| Testbenutzer | Rolle im Szenario | Verwendung |
|---|---|---|
buyer1 |
Buyer | Katalogsuche und Bestellung absenden |
approver1 |
Approver | Bestellung genehmigen |
supplieradmin1 |
Supplier Admin | Testlieferant und Katalogartikel vorbereiten |
Die Passwörter werden nicht im Skript dokumentiert oder fest einprogrammiert. Sie kommen über
Umgebungsvariablen wie BUYER_PASSWORD, APPROVER_PASSWORD und SUPPLIERADMIN_PASSWORD.
Für das Lernprojekt ist der Password Grant vertretbar, weil k6 ohne Browser arbeitet; in einer
produktiven Umgebung sollten dafür keine echten Benutzerpasswörter verwendet werden.
Typische Fehler bei Keycloak
| Symptom | Wahrscheinliche Ursache |
|---|---|
401 beim Token-Abruf |
Benutzer, Passwort, Realm oder Client fehlen/falsch |
401 bei der API |
JWT fehlt, ist abgelaufen oder wurde für den falschen Realm ausgestellt |
403 bei der API |
Token ist gültig, aber die erforderliche Rolle fehlt |
| Token-Endpunkt nicht erreichbar | Keycloak oder der Docker-Stack läuft nicht |
Bei Problemen zuerst prüfen:
docker compose ps
k6 version
Danach kontrollieren, ob der Client procurex-loadtest im Realm procurex vorhanden ist und
die Testbenutzer die erwarteten Rollen besitzen. Die vollständige Keycloak-Konfiguration steht
Installation auf diesem Rechner
k6 ist auf diesem Windows-Rechner direkt über Scoop installiert, nicht als Docker-Container:
k6 version
# k6.exe v2.1.0
Der lokale Aufruf wird über C:\Users\aydin\scoop\shims\k6.exe bereitgestellt. Docker ist
separat installiert und wird nur für den PROCUREX-Anwendungsstack verwendet.
Falls k6 auf einem anderen Rechner fehlt, kann es mit Scoop installiert werden:
scoop install k6
Ein Testlauf für Einsteiger
- PowerShell im Projektverzeichnis öffnen.
- Den benötigten Docker-Stack starten:
docker compose up -d
docker compose ps
- Prüfen, ob k6 erreichbar ist:
k6 version
- Einen einzelnen Test starten:
k6 run --dns "policy=onlyIPv4" loadtests/01-katalogsuche.js
- Am Ende die Zusammenfassung lesen. Wichtig sind
http_req_failed,http_req_durationund
die Ausgabe thresholds. Für die drei Szenarien existieren fertige Skripte; man muss keine
JavaScript-Datei schreiben, um den ersten Test auszuführen.
Beenden kann man einen laufenden Test mit Ctrl+C. Die bereits erzeugten Testdaten bleiben
dabei in der Datenbank und müssen anschließend bei Bedarf manuell bereinigt werden.
Voraussetzungen
- Stack läuft:
docker compose up -dim Repo-Root (Backend erreichbar unter
http://procurex-api.localhost, Keycloak unter http://keycloak.localhost).
- k6 installiert (
k6 version). - Keycloak-Client
procurex-loadtestexistiert im Realmprocurex— siehe
keycloak/README.md, Abschnitt „procurex-loadtest manuell anlegen“. Ohne diesen Client
schlägt der Token-Abruf mit 401 fehl.
Wichtig: --dns policy=onlyIPv4
*.localhost-Hostnamen lösen unter Windows sowohl zu ::1 (IPv6) als auch 127.0.0.1
(IPv4) auf. Traefik (der Reverse-Proxy vor procurex-app) nimmt aber nur IPv4-Verbindungen
an — k6 versucht ohne dieses Flag trotzdem zuerst IPv6, wartet dort auf einen TCP-Timeout
(~20–40 s) und fällt erst danach auf IPv4 zurück. Das macht jeden einzelnen Request absurd
langsam und verfälscht die Latenz-Messung komplett. Immer mit diesem Flag ausführen:
k6 run --dns "policy=onlyIPv4" loadtests/01-katalogsuche.js
k6 run --dns "policy=onlyIPv4" loadtests/02-bestellabsendung.js
k6 run --dns "policy=onlyIPv4" loadtests/03-genehmigungsentscheidung.js
Optional konfigurierbar per Umgebungsvariable (Defaults passen zum lokalen docker-compose-
Setup): API_BASE, KEYCLOAK_TOKEN_URL, LOADTEST_CLIENT_ID, BUYER_PASSWORD,
APPROVER_PASSWORD, SUPPLIERADMIN_PASSWORD.
Szenarien
| Skript | Endpunkt | Ablauf |
|---|---|---|
01-katalogsuche.js |
GET /api/v1/catalog-items (PRX-4) |
Reiner Lese-Endpunkt, daher höhere Last (bis 20 VUs). setup() holt einmal ein buyer1-Token, jede Iteration sucht mit zufälligem Suchbegriff. |
02-bestellabsendung.js |
POST /api/v1/cart/submit (PRX-2) |
setup() legt einmal einen aktiven Test-Lieferanten mit Katalogartikel an (als supplieradmin1). Jede Iteration sendet als buyer1 einen neuen Warenkorb ab — erzeugt bewusst viele PENDING_APPROVAL-Bestellungen (unkritisch für ein Lernprojekt). |
03-genehmigungsentscheidung.js |
POST /api/v1/orders/{id}/approval/approve (PRX-5/PRX-8) |
Eine Bestellung darf laut PRX-8 nur einmal entschieden werden — deshalb erzeugt jede Iteration zuerst eine frische Bestellung (als buyer1) und genehmigt genau diese (als approver1). Die Absende-Anfrage ist Vorbereitung, nicht Teil der eigentlichen Messung (separates name:approve-Tag im Threshold). |
Ergebnisse (Lauf vom 2026-08-10, lokaler Docker-Compose-Stack, PRX-36)
Aktueller Nachlauf mit Podman/enterprise-infrastructure
Erneuter Lauf am 2026-08-29 (Podman-Machine neu gestartet, podman-compose per pip
nachinstalliert, Rootless-Port-80-Fix für Traefik) bestand ebenfalls alle drei Szenarien mit 0 %
Fehlern: Katalogsuche 1643 Requests / p95 7,64 ms, Bestellabsendung 816 Requests / p95 18,85 ms
und Genehmigungsentscheidung 536 Requests / Approve-p95 16,06 ms — alle gesetzten Thresholds
deutlich unterschritten. Vorher lief der Lauf am 2026-08-11 gegen dieselbe Podman-Machine
ebenfalls fehlerfrei (Katalogsuche 1612 Requests / p95 46,57 ms, Bestellabsendung 783 Requests /
p95 107,67 ms, Genehmigungsentscheidung 524 Requests / Approve-p95 40,89 ms). Die darunter
dokumentierten Werte sind der historische Docker-Desktop-Lauf und bleiben als Vergleichsnachweis
erhalten.
Rohausgaben liegen in loadtests/results/*.txt.
| Szenario | Requests | Fehlerrate | p95 Latenz | Ziel-Threshold | Ergebnis |
|---|---|---|---|---|---|
| Katalogsuche (bis 20 VUs, 1m45s) | 1566 | 0,00 % | 130,9 ms | < 500 ms | ✅ erreicht |
| Bestellabsendung (bis 10 VUs, 1m45s) | 575 | 0,00 % | 869,1 ms | < 800 ms | ⚠️ knapp verfehlt (~9 %) |
Genehmigungsentscheidung (bis 5 VUs, 1m10s), nur approve-Aufruf |
204 | 0,00 % | 318,7 ms | < 800 ms | ✅ erreicht |
Fazit: Alle drei Endpunkte sind unter der getesteten Last funktional stabil — **0 %
Fehlerrate** über alle 2185 HTTP-Requests hinweg, kein einziger fehlgeschlagener Request.
Die Katalogsuche und die Genehmigungsentscheidung liegen deutlich innerhalb der gesetzten
Latenzbudgets. Die Bestellabsendung (schreibt in drei Tabellen: Order, Order-Lines,
Outbox-Event für PRX-17) verfehlt das mit 800 ms bewusst knapp gesetzte Ziel geringfügig
(869 ms p95 bei 10 parallelen Nutzern) — funktional unauffällig, aber ein Kandidat für
gezielte Optimierung (z. B. Indexe, Verbindungspool-Größe), falls striktere Latenzvorgaben
gefordert werden. Kein Blocker für den aktuellen Lernprojekt-Rahmen.
Aufräumen
Die Testläufe legen echte Datensätze in der geteilten Postgres-Instanz an (Test-Lieferanten,
Katalogartikel, Bestellungen mit k6-Lasttest-*-Namen bzw. loadtest@procurex.example
als Kontakt-E-Mail). Für ein Lernprojekt unkritisch und absichtlich nicht automatisiert
aufgeräumt — bei Bedarf manuell über die Postgres-Instanz oder per Datenbank-Reset
entfernen.
Jenkins-Ausführung (PRX-36)
Das Jenkinsfile enthält ein optionales Stage k6: Lasttests. Es wird nur mit dem Build-Parameter
RUN_K6=true aktiviert. Der Standard-Build führt k6 nicht automatisch aus, weil die Tests echte
Bestellungen und Katalogdaten erzeugen und deshalb eine bewusst gestartete Lernumgebung benötigen.
Vor der Aktivierung müssen auf dem Jenkins-Agent erfüllt sein:
k6ist installiert und überk6 versionerreichbar.- Docker-Compose-Stack, Keycloak, PostgreSQL, Kafka und Traefik sind erreichbar.
- Die Passwörter und optionalen URLs kommen aus Jenkins-Credentials oder geschützten
Environment-Variablen.
- Der Agent darf keine produktive Umgebung als
API_BASEverwenden.
Das Stage führt alle drei vorhandenen Skripte mit --dns "policy=onlyIPv4" aus. Ein Jenkins-
Nachweis gilt erst als vollständig, wenn die drei k6-Ausgaben archiviert und die erzeugten
Testdaten beziehungsweise deren Bereinigung dokumentiert sind.