Zum Inhalt springen
Kibo – zur StartseiteDokumentation
Alle Seiten
Auf dieser Seite

Lasttests mit k6

Die Lasttests prüfen die öffentlichen Kita- und Brettseiten und das Nachladen älterer Zettel. Sie ändern weder Spam-Budgets noch Produktverhalten. Sie messen HTTP-Antworten des SSR-Servers, keine Browserausführung, Bilder oder Web-Push. Nur künstliche Daten verwenden; echte Kita-Links gehören weder in Tickets noch in Logs oder Artefakte. Die ersten Staging-Messungen stehen unten; eine Produktions-Baseline wurde noch nicht ermittelt. Die unten genannten Grenzwerte sind Startwerte, keine zugesicherte Kapazität.

Vorbereitung und Freigabe

Der eigenständige Lastgenerator ghcr.io/florivdg/kibo-load-runner enthält Bun 1.4.2, k6 2.3.0, den Wrapper und das gebündelte Testskript. Damit braucht der separate Generator nur Docker, keinen Checkout und keine lokale Bun-/k6-Installation. Die beiden Helfer haben unterschiedliche Aufgaben: load-fixtures legt Daten im Stack an, load-runner erzeugt HTTP-Last von außerhalb. Der Runner gehört nicht in den Anwendungsstack.

Wer aus dem Repository arbeitet, muss Bun 1.4.2 und k6 2.3.0 verwenden. bun install --frozen-lockfile installiert auch den Devalue-Decoder. Der Wrapper baut das k6-Skript mit Bun. Eine andere k6-Version wird vor dem ersten HTTP-Aufruf abgelehnt.

Staging und Produktion laufen ausschließlich durch den öffentlichen HTTPS-Reverse-Proxy, auf einem separaten Lastgenerator. Nicht auf dem Anwendungs- oder Datenbankhost laufen lassen. Zieladresse, DNS, Zertifikat, Deploy-Commit und Image-Digest vorab prüfen. Lokal ist HTTP auf localhost oder 127.0.0.1 erlaubt. URLs dürfen keine Pfade, Zugangsdaten oder Query enthalten.

Künstliche Daten werden separat angelegt, nicht vom Lastgenerator oder CI. Im bestehenden Compose-Stack reicht der optionale Dienst load-fixtures: Er verwendet dessen DATABASE_URL, BETTER_AUTH_URL und HOSTED. Auf dem Server sind weder ein Checkout noch Bun, ein manuelles Bundle, eine weitere Env-Datei oder eigene Deployment-Aktionen nötig. Die Befehle im Verzeichnis mit der bestehenden compose.yml und .env ausführen, etwa über die Servershell des Deployment-Werkzeugs. Der Dienst ist im Profil load und startet bei einem gewöhnlichen docker compose up -d nicht. Auch ohne Argumente zeigt sein Image nur Hilfe und schreibt nichts.

docker compose pull load-fixtures
docker compose run --rm load-fixtures provision /fixtures/staging.json

Das Image ghcr.io/florivdg/kibo-load-fixtures trägt denselben KIBO_IMAGE_TAG wie App und Migration. Der gewählte Tag muss bereits ein Helper-Image enthalten; die CI veröffentlicht es zusammen mit den anderen Images. Der Helfer hängt nur im internen Datenbanknetz, öffnet keine Ports und führt keine HTTP-Last aus. Das private Journal liegt dauerhaft im benannten Volume load_fixtures, auch wenn der Container mit --rm entfernt wird. Sein Docker-Logtreiber ist ausgeschaltet, damit auch die gezielte Ausgabe eines Journals nicht in Containerlogs gespeichert wird.

Die Vorgabe ist KIBO_LOAD_ENVIRONMENT=staging. Für Produktion zunächst ein Betriebsfenster vereinbaren und Zielhost/Datenbank prüfen, dann bewusst KIBO_LOAD_ENVIRONMENT=production in der bestehenden .env setzen und /fixtures/production.json statt /fixtures/staging.json verwenden. Diese Einstellung wirkt nur auf den ausdrücklich gestarteten Helfer, nicht auf die App. Das manuelle Provision/Cleanup ist die Freigabe für synthetische Daten; die bisherigen Umgebungs- und Identitätsprüfungen bleiben wirksam.

Das Journal privat auf den separaten Lastgenerator übertragen. Es enthält die Kita-Tokens: nicht in die Zwischenablage einer öffentlichen Konsole, in Logs, Tickets oder Artefakte kopieren. Folgendes leitet es direkt in eine private Datei im Home-Verzeichnis auf dem ausführenden Rechner um; diese Datei anschließend sicher auf den Lastgenerator übertragen oder als GitHub-Environment-Secret hinterlegen:

umask 077
docker compose run --rm -T --no-deps --entrypoint cat load-fixtures /fixtures/staging.json > "$HOME/staging.json"
chmod 600 "$HOME/staging.json"

Falls Docker nur mit sudo zugänglich ist, sudo vor docker compose setzen. Die Umleitung > läuft weiterhin als angemeldeter Benutzer; deshalb in dessen Home-Verzeichnis schreiben, nicht in ein schreibgeschütztes Stack-Verzeichnis.

Wer lokal aus dem Repository entwickelt, kann weiterhin direkt provisionieren: LOAD_ENVIRONMENT=local, LOAD_ORIGIN=http://localhost:4321, LOAD_DATABASE_URL, LOAD_HOSTED=false und LOAD_FIXTURE_AUTHORIZATION=provision-synthetic-local setzen, dann bun run load:fixtures provision .load-private/fixtures.json ausführen.

Bei HOSTED erhält nur der künstliche Eigentümer ein kostenfreies Abo, damit Laufzeitaufgaben die Daten nicht entfernen. Es entstehen drei Kitas mit je einem Brett: leer (0), typisch (12) und groß (240 aktuelle Zettel, 40 pro Seite). Alle Texte sind künstlich, Kontaktfelder leer, Push aus, keine anmeldbaren Konten oder Sessions. Die Zettel laufen nach 24 Stunden ab; danach neu provisionieren. Dieselben Daten können innerhalb dieses Tages für Vergleichsläufe wiederverwendet werden.

Der private Wiederherstellungsnachweis enthält die konkreten IDs und Kita-Tokens. Er wird vor dem Transaktionscommit mit Dateimodus 600 geschrieben. Nicht veröffentlichen. Bei Unterbrechung kann er bereits existieren, obwohl die Transaktion zurückgerollt wurde. Cleanup funktioniert auch dann.

Profile und harte Grenzen

Profil Umgebung Seitentraffic Dauer Max. Seiten-VUs
smoke lokal oder Staging 2 Nutzer, je 2 Sekunden Pause 120 s 2
baseline nur Staging konstante 5, 10, 25 Ankünfte/s, je 100 s 300 s 50 je Stufe, ohne Überlappung
production nur Produktion konstant 1 Ankunft/s 300 s 4

Eine Seiteniteration sendet genau einen GET: 30 % Kitaübersicht, 70 % Brett, gleichmäßig verteilt auf die drei Datenmengen. Ankunftsraten sind angebotene Last; dropped_iterations zeigt, wenn der Generator das Ziel nicht erreicht. Zusätzlich prüft ein VU alle 10 Sekunden /api/health. Smoke und Baseline senden über einen weiteren VU alle 10 Sekunden einen POST an /_actions/notes.list mit dem Cursor nach den ersten 40 Zetteln des großen Bretts. Das sind höchstens 30 Nachladeaufrufe in fünf Minuten, unabhängig von den Seiten-VUs. Produktion sendet keine Actions und bleibt bei GETs.

Alle VUs teilen die Netzwerkidentität des Generators. Kibo erlaubt 120 Leseaufrufe pro Netzwerk in 15 Minuten. Keine IP-Header fälschen, keine Netzwerkrotation, keine Budgetausnahmen. Zwischen Läufen mindestens 15 Minuten warten; anderen Traffic des Generators berücksichtigen. 429 beim Nachladen wird als erwartete Ablehnung separat gezählt, nicht als erfolgreiche Latenz. Ein solcher Lauf belegt keine erfolgreiche Nachladeleistung.

Vom eigenständigen Docker-Runner starten

Alle folgenden Schritte auf dem separaten Lastgenerator ausführen. Ein Image-Tag wie main ist erst nach einem erfolgreichen Build dieser Version verfügbar; für wiederholbare Vergleiche das Runner-Image mit sha-<Commit> oder Digest festlegen. Das gewählte Image muss den Runner enthalten. Bei privaten GHCR-Paketen zuerst mit einem berechtigten Konto docker login ghcr.io ausführen; alternativ muss das Paket öffentlich lesbar sein.

mkdir -p .load-private/run
chmod 700 .load-private .load-private/run
# Das sicher übertragene Journal hierhin kopieren:
cp "$HOME/staging.json" .load-private/staging.json
chmod 600 .load-private/staging.json

Die private Datei .load-private/load.env anlegen. Zieladresse, Zielcommit, Zielimage und Infrastruktur auf die tatsächliche Installation setzen; Platzhalter ersetzen. Die URL bleibt eine Laufzeitvariable. Die Datei enthält keine Datenbankzugangsdaten, SMTP-Zugangsdaten oder Sitzungscookies:

LOAD_ENVIRONMENT=staging
LOAD_PROFILE=smoke
LOAD_ORIGIN=https://staging.example.org
LOAD_MANIFEST=/input/fixtures.json
LOAD_RUN_AUTHORIZATION=run-readonly-staging
LOAD_COMMIT=<40-stellige Commit-ID des Zieldeployments>
LOAD_IMAGE=ghcr.io/florivdg/kibo@sha256:<Digest des Zieldeployments>
LOAD_INFRASTRUCTURE=4logical-2physical-4GiB-Postgres18-Traefik-separate-generator
chmod 600 .load-private/load.env
LOAD_RUNNER_IMAGE=ghcr.io/florivdg/kibo-load-runner:main
docker pull "$LOAD_RUNNER_IMAGE"
docker run --rm --init \
  --user "$(id -u):$(id -g)" \
  --security-opt no-new-privileges:true --cap-drop=ALL \
  --read-only --tmpfs /tmp:rw,noexec,nosuid,size=64m \
  --log-driver=none \
  --env-file "$PWD/.load-private/load.env" \
  --mount "type=bind,src=$PWD/.load-private/staging.json,dst=/input/fixtures.json,readonly" \
  --mount "type=bind,src=$PWD/.load-private/run,dst=/work" \
  "$LOAD_RUNNER_IMAGE" run

Der Container läuft mit der UID/GID des ausführenden Benutzers. Das Journal ist nur lesbar eingehängt; Bundle und Ergebnisse entstehen im beschreibbaren Arbeitsverzeichnis. Das gesamte Arbeitsverzeichnis /work einhängen, nicht nur /work/load-results: Vor jedem Lauf entfernt der Wrapper alte Ergebnisse, einschließlich dieses Unterverzeichnisses. Der Standardbenutzer des Images ist ebenfalls unprivilegiert. Ohne Argumente oder mit --help zeigt das Image nur Hilfe und startet keine Last; run startet das gewählte Profil. Containerlogs sind abgeschaltet; die bereinigten Ergebnisse sind auf dem Generator unter .load-private/run/load-results/summary.json und provenance.json verfügbar. Ein Exit-Code ungleich null bedeutet einen abgelehnten Lauf, Abbruch oder ein verfehltes Testziel; Ergebnisse mitprüfen.

Nach einem Smoke-Test mindestens 15 Minuten Pause nach dessen Ende einhalten, dann für die Staging-Baseline LOAD_PROFILE=baseline in der Env-Datei setzen und denselben Docker-Befehl erneut ausführen. Vorher Ergebnisse sichern, wenn sie für Vergleiche gebraucht werden. Für Produktion gelten ausschließlich LOAD_ENVIRONMENT=production, LOAD_PROFILE=production, LOAD_RUN_AUTHORIZATION=run-readonly-production und ein passend provisioniertes Produktionsjournal. Das Ziel muss zur Herkunft des Journals passen.

Aus dem Repository starten

Bei dieser Alternative Bun/k6 wie oben installieren und das übertragene Journal unter .load-private/fixtures.json ablegen. Die gleichen Einstellungen vollständig als Umgebungsvariablen setzen:

export LOAD_ENVIRONMENT=staging
export LOAD_ORIGIN=https://staging.example.org
export LOAD_PROFILE=baseline
export LOAD_MANIFEST=.load-private/fixtures.json
export LOAD_RUN_AUTHORIZATION=run-readonly-staging
export LOAD_COMMIT='<40-stellige Commit-ID des Zieldeployments>'
export LOAD_IMAGE='ghcr.io/florivdg/kibo@sha256:<Digest>'
export LOAD_INFRASTRUCTURE='2CPU 4GB RAM Postgres18 Proxy separate-generator'
bun run load:run

Ungültige Umgebung, Profil, Ziel, abgelaufene Fixtures oder fehlende Freigabe werden vor der Last abgelehnt. Keine zusätzlichen k6-Flags oder Umgebungs- Overrides: Der Wrapper gibt k6 eine eigene Umgebung und verwirft dessen rohe Ausgaben, weil Transportfehler vollständige URLs enthalten können.

Prüfen und stoppen

Seiten müssen Status 200 und HTML enthalten. Die Kitaübersicht muss den passenden Zettelstand zeigen; Brettseiten alle erwarteten ersten 0/12/40 Zettel beziehungsweise den Leerzustand. Namen allein reichen nicht aus, pausierte Bretter und andere Hinweisantworten werden als Fehler gezählt. Das Nachladen verwendet Astros normalen JSON-Request und den gebündelten Devalue-Decoder für application/json+devalue; erwartet werden 40 ältere, streng absteigend sortierte Zettel und hasMore: true.

Die vorläufigen Leistungsziele sind Seiten p95 < 500 ms, p99 < 1.000 ms, Nachladen p95 < 300 ms und unerwartete Fehler < 0,5 %. Überschreitungen machen den abgeschlossenen Lauf rot.

Automatisch wird bei unerwarteten Fehlern ab 1 % nach 20 Sekunden, bei Erfolgs-Latenz p95 ab 2 Sekunden nach 30 Sekunden oder bei einem fehlgeschlagenen Healthcheck abgebrochen. Jeder Request hat 5 Sekunden Timeout, Redirects sind aus. Der Wrapper beendet k6 spätestens nach sechs Minuten. Fehlende Generatorleistung macht den Lauf ebenfalls rot. Parallel CPU, RAM, DB-Verbindungen, DB-Latenz und Proxy-5xx beobachten. Bei auffälligen Werten oder Beschwerden sofort Ctrl-C; im GitHub-Workflow „Cancel workflow“. Die Tests fahren keine Laststeigerung außerhalb der festgelegten Profile.

Artefakte und Baseline

Nur load-results/summary.json und provenance.json weitergeben. Die Summary enthält ausschließlich freigegebene numerische Aggregate: erfolgreiche Seiten- und Nachlade-Latenzen einschließlich p95/p99, unerwartete Fehler, erwartete 429, Healthfehler, HTTP-Anzahl, Iterationen und verworfene Iterationen. URL- und Error-Systemtags fehlen. Response-Inhalte, Tokens, Session-Cookies, Gruppen, Setup-Daten und rohe k6-Ausgaben werden nicht gespeichert. Das private Manifest und Bundle bleiben in .load-private/, beide Verzeichnisse sind Git-ignoriert. Keine anderen Outputs, HTTP-Debugging oder Konsolenlogs ergänzen.

Für jede Ankunftsstufe stehen zusätzlich freigegebene Stufennamen, angeforderte Rate, konfigurierte und tatsächlich gemessene Dauer, Seitenanzahl, erreichte Rate, erfolgreiche Latenzen, Fehler und verworfene Iterationen in der Summary. Die erreichte Rate teilt die Seitenanzahl durch die Dauer dieser Stufe; sie verwendet nicht die Gesamtdauer aller Stufen.

Die Provenienz hält Zieldeploy-Commit, Image, Infrastruktur, Profil, Umgebung, Startzeit, k6-Version und Fixture-Größen fest. Bei Vergleichen zusätzlich Deploymentkonfiguration, Generatorstandort, Reverse-Proxy, Datenbankversion, Cachezustand und konkurrierende Nutzung dokumentieren, ohne Links oder Secrets. Für weitere Baselines diese Tabelle nach einem freigegebenen Lauf ausfüllen:

Angabe Wert
Zeitpunkt, Commit, Image noch nicht gemessen
CPU/RAM/DB/Proxy/Generator noch nicht erfasst
Profil und angebotene/erreichte Rate noch nicht gemessen
Seiten p95/p99, Nachladen p95/p99 noch nicht gemessen
429, unerwartete Fehler, verworfene Iterationen noch nicht gemessen
Betriebsmesswerte und Abbruchgrund noch nicht gemessen

Erste Staging-Messungen vom 7. Oktober 2026

Mit synthetischen Fixtures wurden Smoke und Baseline erfolgreich ausgeführt. Die Baseline erreichte 5,00 / 9,98 / 24,98 Seitenaufrufe pro Sekunde über je 100 Sekunden; Seiten-p95 war 174 / 179 / 170 ms. Nachladen-p95 war 61 ms; unerwartete Fehler, Healthfehler und verworfene Iterationen waren null.

Zusätzlich wurden separat freigegebene, ausschließlich lesende Kapazitätsversuche durchgeführt. Diese sind keine zusätzlichen eingebauten Profile und keine Freigabe, die festen Profile selbstständig zu erhöhen:

Angebotene Last Dauer Erreichte Rate Seiten p95 Verworfene Iterationen
100/s 100 s 99,91/s 166 ms 0
200/s 300 s 199,95/s 154 ms 0
300/s 600 s 299,81/s 162 ms 78
400/s 20 s, dann Abbruch 348,09/s 1.607 ms 529

Der 300er-Lauf lief zehn Minuten vollständig, verfehlte aber das strenge Ziel von null verworfenen Iterationen. Der 400er-Lauf brach am zusätzlich vereinbarten Stufenlimit p95 < 1.500 ms ab. Beide hatten keine unerwarteten HTTP- oder Healthfehler. 348,09/s aus der kurzen abgebrochenen Stufe ist keine nachgewiesene Dauerkapazität.

Der gemeinsam genutzte Host hatte 4 logische CPUs, 2 physische Kerne und 3,75 GiB RAM; ein Node-App-Prozess, PostgreSQL und Traefik wurden beobachtet. Ein Wiederholungslauf mit SSH-Beobachtung zeigte bei 400/s den Node-Hauptthread bei ungefähr 96 % eines Kerns, während die Host-CPU im Mittel etwa 61 % erreichte. App- und Datenbankcontainer hatten keine CPU-Quote oder feste Speichergrenze, keine CPU-Drosselung und keine OOM-Ereignisse. Mindestens 1,5 GiB RAM blieb verfügbar. Das spricht für CPU-Sättigung im App-Prozess. Der separate Generator hatte 10 logische CPUs und 32 GiB RAM.

Der Testcode basierte auf ca842f7cc2a40e17695923ed2783f6e09d6f8932. Commit und Image-Digest des Zieldeployments wurden nicht verifiziert. Generatorstandort und Cachezustand wurden nicht vollständig erfasst. Diese Messungen sind deshalb eine Orientierung für diese Installation, keine versionsgebundene Kapazitätszusage. Für belastbare Vergleiche die vollständige Provenienz vor dem nächsten Lauf erfassen. Keine privaten Zieladressen oder Fixture-Tokens veröffentlichen.

Der manuell gestartete Workflow Read-only load test läuft pro Umgebung nur einmal gleichzeitig und lädt ausschließlich die beiden bereinigten Dateien hoch (14 Tage Aufbewahrung). In den GitHub-Environments staging und production jeweils LOAD_ORIGIN, LOAD_MANIFEST_JSON (Inhalt des privaten Manifests), LOAD_RUN_AUTHORIZATION als Secrets und LOAD_COMMIT, LOAD_IMAGE, LOAD_INFRASTRUCTURE als Variablen setzen. Produktions-Environment mit Freigaberegeln schützen. Die Variablen beschreiben das Zieldeployment, nicht nur den ausgecheckten Testcode. Der Runner liegt außerhalb des Zielhosts.

Aufräumen

Im bestehenden Stack mit derselben geprüften Datenbank und KIBO_LOAD_ENVIRONMENT wie bei der Provisionierung:

docker compose run --rm load-fixtures cleanup /fixtures/staging.json

Nach erfolgreichem Cleanup kann das Journal entfernt werden, damit derselbe Dateiname später für einen neuen Datensatz verfügbar ist:

docker compose run --rm -T --no-deps --entrypoint rm load-fixtures /fixtures/staging.json

Für lokale Repository-Tests bleibt bun run load:fixtures cleanup .load-private/fixtures.json mit den bisherigen LOAD_*-Einstellungen verfügbar.

Cleanup löscht nur die drei dokumentierten Kita-IDs, deren Name, Slug, Marker und alleinige Eigentümermitgliedschaft exakt passen. Andere Daten und Spam-Zähler bleiben erhalten. Der künstliche Eigentümer wird nur bei passender Identität und ohne weitere Mitgliedschaften gelöscht. Alles geschieht in einer Transaktion; jede Abweichung stoppt die Löschung. Wiederholter Cleanup ist unschädlich. Private Journale erst nach bestätigtem Cleanup entfernen. Bei fehlendem Journal nicht anhand eines Namenspräfixes großflächig löschen; dokumentierte IDs und Marker manuell prüfen.

Weitere Betriebsprüfungen: Betrieb, Spam-Schutz, Sicherheit.