PROCUREX · Statische HTML-Dokumentation

Lasttests mit k6 (PRX-36)

Aus loadtests\README.md konvertiert – vollständig lokal lesbar.

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.

werden dabei nicht versteckt, sondern getrennt betrachtet.

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?

  1. setup() oder der Testablauf ruft loadtests/lib/auth.js auf.
  2. k6 sendet Benutzername, Passwort, client_id=procurex-loadtest und

grant_type=password an den Keycloak-Token-Endpunkt.

  1. Keycloak prüft die Zugangsdaten und gibt ein kurzlebiges Access Token als JWT zurück.
  2. Das Skript übergibt dieses Token bei jedem API-Aufruf im Header

Authorization: Bearer <JWT>.

  1. 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

in keycloak/README.md.

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

  1. PowerShell im Projektverzeichnis öffnen.
  2. Den benötigten Docker-Stack starten:
docker compose up -d
docker compose ps
  1. Prüfen, ob k6 erreichbar ist:
k6 version
  1. Einen einzelnen Test starten:
k6 run --dns "policy=onlyIPv4" loadtests/01-katalogsuche.js
  1. Am Ende die Zusammenfassung lesen. Wichtig sind http_req_failed, http_req_duration und

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

http://procurex-api.localhost, Keycloak unter http://keycloak.localhost).

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:

Environment-Variablen.

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.

⌂ Cockpit