FIELDNOTES/ENGINEERING
KNOWLEDGE BASE · V1.0
THE PRACTICE OF SYSTEMS

Performance
Engineering

Systemleistung ist kein End-of-line-Test. Sie ist eine Entwurfsdisziplin: Hypothesen formulieren, realistische Last erzeugen, Signale korrelieren und Engpässe dort beseitigen, wo sie entstehen.

07MODULE
04TOOLS IM FOKUS
∞ITERATIONEN
FIELD GUIDE — 2026LESEZEIT 18 MIN
01 — FOUNDATIONS

Fundamentale & Metriken

Bevor ein Tool gestartet wird, muss klar sein, welche Systemfrage beantwortet werden soll. Diese Größen beschreiben Leistung aus unterschiedlichen Blickwinkeln.

τZEIT / ANFRAGE

Latenz

Latenz ist die verstrichene Zeit vom Absenden einer Anfrage (englisch: Request) bis zum Empfang ihrer Antwort. Auf dem Nutzerpfad umfasst sie Netzwerkübertragung und die Zeit in allen beteiligten Systemen. Ein niedriger Mittelwert kann eine langsame Minderheit verdecken.

FRAGE Wie lange wartet ein Nutzer?
λANFRAGEN / SEK.

Durchsatz

Durchsatz ist erfolgreich abgeschlossene Arbeit pro Zeit, zum Beispiel 120 Anfragen/s, 40 Transaktionen/min oder 8 MB/s. Immer mit Fehlerquote und Antwortzeit betrachten: mehr angebotene Last ist nicht mehr erledigte Arbeit.

FRAGE Wie viel Arbeit wird erledigt?
nGLEICHZEITIG

Gleichzeitigkeit (Concurrency)

Die Anzahl gleichzeitig aktiver Vorgänge. Sie ist nicht gleich Nutzerzahl oder Anfragerate: Ein Nutzer kann zwischen Aktionen pausieren, während ein asynchroner Vorgang noch nicht abgeschlossen und damit weiterhin „in flight“ ist.

FRAGE Wie viel Arbeit ist offen?
LWARTESCHLANGENMODELL

Little’s Law

Für ein stationäres System verbindet Little’s Law den mittleren Bestand L, den mittleren Durchsatz λ und die mittlere Verweildauer W eines Vorgangs innerhalb einer festgelegten Systemgrenze.

L = λ × W
FRAGE Wie viele Vorgänge sind im System?

Das Sättigungsbild lesen

Die angebotene Last ist die Arbeit, die Clients pro Sekunde anliefern; der Durchsatz ist die tatsächlich abgeschlossene Arbeit. Mit steigender Last wächst der Durchsatz zunächst annähernd linear. Erreicht eine Ressource ihre Kapazitätsgrenze, bilden sich Warteschlangen: Verweildauer und Zeitüberschreitungen (Timeouts) steigen, während der Durchsatz stagniert oder fällt. Beispiel: Steigt die Rate von 100 auf 150 Anfragen/s, der Durchsatz bleibt aber bei 120/s und die Warteschlange wächst, ist das System gesättigt. Dieser Knick ist aussagekräftiger als ein einzelner Maximalwert.

Service Level Indicator, Objective und Agreement

Ein Service Level Indicator (SLI, Dienstgütekennzahl) ist ein messbarer Wert für ein Nutzerergebnis, zum Beispiel der Anteil erfolgreicher Checkout-Anfragen mit weniger als 500 ms Antwortzeit. Ein Service Level Objective (SLO, Dienstgüteziel) legt den Zielwert und Zeitraum fest. Ein Service Level Agreement (SLA, Dienstgütevereinbarung) ist die externe Zusage samt möglichen Folgen bei Nichterfüllung. Das Error Budget (Fehlerbudget) ist der erlaubte Anteil an Nichterfüllung des SLO; bei 99,9 % Verfügbarkeit beträgt es 0,1 % des vereinbarten Zeitfensters. Das interne SLO wird üblicherweise strenger als das SLA gesetzt, damit vor einer Vertragsverletzung noch Reaktionszeit bleibt.

02 — LOAD TESTING

Lasttests, die etwas beweisen

Ein Lasttest ist ein reproduzierbares Experiment mit künstlich erzeugten Nutzeranfragen. Er beschreibt die Workload (Arbeitslast: Rate, Mischung und zeitlicher Verlauf der Anfragen), hält Testdaten unter Kontrolle und legt vor dem Lauf fest, woran Erfolg oder Abbruch gemessen wird.

KRITERIUMk6JMeter
ModellJavaScript-Skripte, zuerst über die Command-Line Interface (CLI, Kommandozeile); Virtual Users (VUs, virtuelle Nutzer) oder Arrival-Rate-Executors (vorgegebene Anfragerate) modellieren die Arbeit.Graphical User Interface (GUI, grafische Oberfläche) für Testpläne mit Thread Groups (parallelen Ausführungssträngen) und Samplern (einzelnen Aktionen); CLI für reproduzierbare Läufe in Continuous Integration (CI, automatisierten Build- und Testabläufen).
StärkenTests als Code, schnelle Starts, Prüfungen (Checks) und Grenzwerte (Thresholds) im Skript; gute CI-Integration.Breite Protokoll- und Plugin-Auswahl, GUI für Exploration, etablierter Unternehmenseinsatz.
BeachtenLastgenerator separat dimensionieren. Checks melden Ergebnisse, sind aber keine harten Abbruchkriterien: Dafür Thresholds setzen. VU-Kapazität prüfen.Ergebnis-Listener bei hoher Last minimieren; GUI nicht als Generator einsetzen. Heap (Java-Arbeitsspeicher) und Lastgenerator überwachen.
Passend, wenn …Versionierbare API-Tests und Pipeline-Gates im Vordergrund stehen.Bestehende Testpläne, spezielle Protokolle oder visuelle Testflow-Modellierung wichtig sind.
02.1

k6: Anlaufphase, Checks und belastbare Grenzwerte

Ein Arrival-Rate-Modell gibt eine festgelegte Zahl neuer Anfragen pro Zeiteinheit vor, statt die Rate indirekt aus der Geschwindigkeit virtueller Nutzer abzuleiten. Die Ramp-up-Phase erhöht diese Rate schrittweise.

JS checkout-load.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';

const errors = new Rate('checkout_errors');
const latency = new Trend('checkout_latency', true);
const baseUrl = __ENV.BASE_URL || 'https://staging.example.com';

export const options = {
  scenarios: { checkout_ramp: {
    executor: 'ramping-arrival-rate', startRate: 5, timeUnit: '1s',
    preAllocatedVUs: 30, maxVUs: 200,
    stages: [
      { target: 20, duration: '2m' },
      { target: 60, duration: '5m' },
      { target: 60, duration: '3m' },
      { target: 0, duration: '1m' },
    ],
  } },
  thresholds: {
    http_req_failed: ['rate<0.01'],
    http_req_duration: ['p(95)<400', 'p(99)<800'],
    checkout_errors: ['rate<0.01'], dropped_iterations: ['count==0'],
  },
};

export default function () {
  const payload = JSON.stringify({ sku: __ENV.SKU || 'sku-42', quantity: 1 });
  const response = http.post(`${baseUrl}/api/checkout`, payload, {
    headers: { 'Content-Type': 'application/json',
      'Authorization': `Bearer ${__ENV.TEST_TOKEN}` },
    tags: { name: 'checkout' }, timeout: '5s',
  });
  const valid = check(response, {
    'status is 201': (r) => r.status === 201,
    'has order id': (r) => Boolean(r.json('orderId')),
  });
  errors.add(!valid); latency.add(response.timings.duration); sleep(1);
}
RUNk6 run -e BASE_URL=https://staging.example.com -e SKU=sku-42 -e TEST_TOKEN="$TEST_TOKEN" checkout-load.js
● ● ●BEISPIEL: k6-ZUSAMMENFASSUNGPASS
checks.........................: 99.8% ✓ 1198  ✗ 2
      http_req_duration..............: avg=184ms p(95)=312ms
      http_req_failed................: 0.17%  2 out of 1200
      iterations.....................: 1200  20.0/s
      vus_max........................: 34

Der Lauf erfüllt p(95)<400 und die Rate unter 1 %. Zwei fehlgeschlagene Prüfungen zeigen trotzdem einen fachlichen Fehler, den die reine Latenzgrenze nicht abbildet.

02.2

JMeter: wiederholbarer CLI-Lauf

Im Testplan: Thread Group, CSV Data Set (eindeutige Nutzer), HTTP Request Defaults, Header Manager, Assertions und Backend Listener. GUI zum Entwurf, Non-GUI für reproduzierbare Läufe.

CLI Pipeline
# Thread Group: 60 Threads, 120 s Ramp-up, 8 min Laufzeit
# CSV Data Set: users.csv | Recycle: false
jmeter -n -t checkout.jmx \
  -Jusers=60 -Jramp=120 -Jduration=480 \
  -JbaseUrl=https://staging.example.com \
  -l results.jtl -e -o report/

Properties im Testplan mit ${__P(users,60)} auslesen. Daten pro VU eindeutig halten, Response-Assertions gezielt aktivieren und große Listener-Ergebnisse nicht im Injector-Speicher sammeln.

02.3

JMeter: Transaktionen und testdatengesteuerte Abläufe

Der Transaction Controller bündelt mehrere Sampler zu einer fachlichen Transaktion. Mit „Generate parent sample“ entsteht ein übergeordneter Messwert; ohne diese Option werden nur die Einzelschritte ausgewertet. CSV Data Set Config liefert pro Thread Testdaten und muss hinsichtlich Sharing, Dateiende und Wiederverwendung zur Thread Group passen.

JMX-Beispiel: Transaction Controller und CSV Data Set Config

Dieses gekürzte Beispiel liest Nutzerzeilen ein und führt Login, Warenkorb und Checkout als eine gemessene Transaktion aus. Bei nicht wiederverwendeten Daten müssen genügend eindeutige CSV-Zeilen für alle Threads vorhanden sein.

XML checkout-flow.jmx
<!-- Elemente liegen in der hashTree der ThreadGroup -->
<CSVDataSet guiclass="TestBeanGUI" testclass="CSVDataSet"
  testname="User data" enabled="true">
  <stringProp name="filename">${__P(csv,users.csv)}</stringProp>
  <stringProp name="variableNames">username,password,customer_id</stringProp>
  <stringProp name="delimiter">,</stringProp>
  <boolProp name="ignoreFirstLine">true</boolProp>
  <boolProp name="quotedData">true</boolProp>
  <boolProp name="recycle">false</boolProp>
  <boolProp name="stopThread">true</boolProp>
  <stringProp name="shareMode">shareMode.all</stringProp>
</CSVDataSet>
<hashTree/>

<TransactionController guiclass="TransactionControllerGui"
  testclass="TransactionController" testname="Checkout transaction"
  enabled="true">
  <boolProp name="TransactionController.parent">true</boolProp>
</TransactionController>
<hashTree>
  <HTTPSamplerProxy testname="Login"/>
  <hashTree/>
  <HTTPSamplerProxy testname="Add item to cart"/>
  <hashTree/>
  <HTTPSamplerProxy testname="Submit checkout"/>
  <hashTree/>
</hashTree>

Auswertung: Der Parent-Sample-Wert umfasst die Transaktion inklusive Zwischenwartezeiten. Einzelsampler zusätzlich behalten, wenn einzelne Schritte diagnostiziert werden sollen. Testdaten anonymisieren; Passwörter nicht in CSV-Dateien committen.

JTL-Logs automatisiert mit Python auswerten

Der Parser erwartet JMeter-JTL als CSV mit Header und den Spalten label, elapsed und success. Er fasst Dauer, P95/P99 und Fehlerquote je Transaktion zusammen.

PY summarize_jtl.py
import csv
import math
import sys
from collections import defaultdict


def percentile(values, quantile):
    ordered = sorted(values)
    index = max(0, math.ceil(quantile * len(ordered)) - 1)
    return ordered[index]


def summarize(path):
    durations = defaultdict(list)
    failures = defaultdict(int)
    with open(path, newline="", encoding="utf-8") as source:
        for row in csv.DictReader(source):
            label = row["label"]
            durations[label].append(int(row["elapsed"]))
            failures[label] += row["success"].lower() != "true"

    for label, samples in sorted(durations.items()):
        error_rate = failures[label] / len(samples)
        print(
            f"{label}: n={len(samples)}, "
            f"p95={percentile(samples, .95)}ms, "
            f"p99={percentile(samples, .99)}ms, "
            f"errors={error_rate:.2%}"
        )


if __name__ == "__main__":
    summarize(sys.argv[1] if len(sys.argv) > 1 else "results.jtl")

Die Sortierung im Speicher ist für moderate Testläufe geeignet; bei sehr großen Logs Streaming-Histogramme oder ein Metrics-Backend nutzen. JTL-Spalten hängen von der JMeter-Konfiguration ab. CI-Gates sollten Budgets über mehrere stabile Samples anwenden, nicht einen einzelnen Ausreißer.

03 — OBSERVABILITY

Signale zusammen lesen

Observability (Beobachtbarkeit) bedeutet, den inneren Zustand eines Systems aus seinen Ausgaben abzuleiten. Metriken sind über Zeit aggregierte Zahlen, Logs zeitgestempelte Ereignisse und Traces Ablaufspuren einer Anfrage über mehrere Services. Erst gemeinsam wird die Diagnose belastbar.

01METRIKENWas kippt wann? · RED / USE
→
02TRACESWo liegt die Zeit? · trace_id
→
03LOGSWas geschah genau? · span_id

Korrelieren, nicht nur sammeln

Verwende konsistente Attribute für Service, Umgebung und Version. Jeder Trace (Ablaufspur einer Anfrage) erhält eine Trace-ID (eindeutige Ablaufkennung); Logs übernehmen Trace-ID und Span-ID (Kennung eines einzelnen Arbeitsschritts) als strukturierte Felder. Metriken sollten Labels begrenzter Kardinalität (Anzahl möglicher Werte) nutzen, etwa ein Routenmuster statt einer vollständigen URL (Uniform Resource Locator, Webadresse). So führt ein Alarm vom Service-Dashboard zum Trace und zum passenden Log.

RED und USE als Startpunkt

RED steht für Rate (Anfragerate), Errors (Fehlerzahl) und Duration (Dauer) und prüft Services. USE steht für Utilization (Auslastung), Saturation (Rückstau) und Errors (Fehler) und prüft Ressourcen. Prüfe zusätzlich Warteschlangentiefe und Pool-Auslastung: Eine Central Processing Unit (CPU, Hauptprozessor) bei 40 % kann neben einem erschöpften Connection Pool (Verbindungspool) stehen, der Anfragen nacheinander statt parallel bearbeitet.

03.1

Grafana: Datenquellen, Panels und Zeitreihen

Grafana visualisiert und korreliert Messdaten; es ist typischerweise nicht selbst deren Quelle. Ein Dashboard fragt Data Sources wie Prometheus, Loki, Elasticsearch oder SQL-Datenbanken ab und rendert Resultate in Panels.

Vom Sample zum Panel

Eine Time Series (Zeitreihe) besteht aus Zeitstempeln und Werten, identifiziert durch Metrikname und Labels. Prometheus speichert Samples als numerische Messwerte: Counter steigen monoton und werden mit rate() oder increase() ausgewertet; Gauges wie Queue-Tiefe steigen und fallen. Histogramme bestehen aus Bucket-Zählern sowie _sum und _count. Ein Panel definiert Abfrage, Visualisierung, Einheit, Legende, Schwellenwerte und optionale Transformationen. Achsen und Einheiten müssen zur Messgröße passen.

Variables und Dashboard-Kontext

Variables parametrisieren Abfragen, zum Beispiel $environment, $service oder $instance. Grafana kann Optionen aus Label-Werten einer Data Source befüllen und als Dropdown oder Multi-Select anbieten. Mehrfachwerte müssen zur PromQL-Regex passen. Vermeide hochkardinale Labels wie Nutzer-ID, vollständige URL oder Request-ID. Halte Zeitbereich, Zeitzone und Refresh-Intervall konsistent; Deployment-Annotationen machen Vorher/Nachher-Vergleiche nachvollziehbar.

PromQL-Beispiele: P99-Latenz und CPU-Auslastung

Die P99-Abfrage setzt ein Prometheus-Histogramm http_request_duration_seconds_bucket voraus. Die erste CPU-Abfrage berechnet Prozess-CPU-Kerne aus einem Counter; die Node Exporter-Abfrage schätzt die Host-Auslastung.

Q PromQL
# P99 je Service im rollierenden 5-Minuten-Fenster
histogram_quantile(
  0.99,
  sum by (le, service) (
    rate(http_request_duration_seconds_bucket{
      environment=~"$environment", service=~"$service"
    }[5m])
  )
)

# Prozess-CPU-Kerne je Instanz (1.0 = ein voll genutzter Kern)
sum by (instance) (
  rate(process_cpu_seconds_total{
    job="$job", environment=~"$environment"
  }[5m])
)

# Host-CPU-Auslastung in Prozent, aus idle-Zeit abgeleitet
100 * (1 - avg by (instance) (
  rate(node_cpu_seconds_total{mode="idle", instance=~"$instance"}[5m])
))

Histogramm-Quantile sind abhängig von Bucket-Grenzen. Bei wenigen Samples schwankt P99 stark; Request-Anzahl, Fehler und mehrere Läufe gemeinsam betrachten. Metrik- und Labelnamen an die eigene Instrumentierung anpassen.

TAIL LATENCY

Der lange Schwanz
gehört zum Produkt.

P95 ist die Grenze, unter der 95 % der Beobachtungen liegen; 5 % sind langsamer. P99 betrachtet das langsamste Prozent. Mittelwerte verschleiern genau diese Nutzererfahrung.

P50 82 msP95 410 msP99 1,2 s
LATENZ
1.2 s800 ms400 ms0P50P99FASTSLOW
ANTEIL DER REQUESTS →
CPU-FREQUENZ

P-States & Throttling

CPU-P-States bezeichnen Performance-/Frequenzzustände. Moderne CPUs ändern Takt und Spannung dynamisch nach Last, Temperatur und Power-Budget. Niedriger Takt im Leerlauf ist normal; thermisches oder Power-Throttling kann unter Last Kapazität begrenzen. Takt, Temperatur, CPU-Zeit und Durchsatz gemeinsam vergleichen.

SYSTEMAUSLASTUNG

Load Average ist keine CPU-%

Linux Load Average zählt laufbereite Tasks und Tasks im ununterbrechbaren Sleep, meist I/O-Wait. Bei 8 Kernen bedeutet dauerhafter Load 8 etwa eine volle Run Queue; Load 16 weist auf Rückstau hin. vmstat, CPU-Steal, Run Queue und I/O-Latenz ergänzen, bevor du die Ursache festlegst.

04 — BOTTLENECK ANALYSIS

Vom Symptom zur Engstelle

Formuliere eine prüfbare Hypothese, ändere jeweils eine Variable und vergleiche denselben Workload. Korrelation lenkt die Suche; gezieltes Profiling bestätigt die Ursache.

A

Thread-Dumps lesen

Mehrere Dumps im Abstand von 5–10 Sekunden aufnehmen. BLOCKED deutet auf Monitor-Konkurrenz; viele WAITING können normal sein. Wiederkehrende Stackframes in RUNNABLE lokalisieren CPU-Arbeit. Deadlocks sind explizit markiert. Thread-Zustand mit CPU-Profil und Request-Trace abgleichen.

"http-worker-17" #84 RUNNABLE
    at com.acme.Cart.calculate(Cart.java:142)
"pool-2-thread-1" #91 BLOCKED
  - waiting to lock <0x00000007>
JVM
B

Heap-Dumps & Speicherwachstum

Ein Dump ist eine Momentaufnahme, kein Zeitverlauf. In MAT/YourKit zuerst dominator tree und retained size prüfen, dann GC roots bis zur unerwarteten Referenz verfolgen. Wiederholte Dumps nach vergleichbarer Last zeigen, ob Objekte dauerhaft gehalten werden. Dumps können pausieren und sensible Daten enthalten.

jcmd <pid> GC.heap_dump /secure/path/heap.hprof
jcmd <pid> GC.class_histogram
MEMORY
C

GC-Logs interpretieren

Bei G1: -Xlog:gc*,safepoint:file=gc.log:time,uptime,level,tags. Pausendauer und -frequenz, Heap vor/nach GC, Humongous Allocation, Concurrent-Cycle-Dauer und Full GCs prüfen. Häufige lange Pausen weisen auf Allocation Pressure oder unpassende Heap-Größe hin; den Heap blind zu vergrößern kann Pausen verlängern. Allocation Rate und Live Set dazunehmen.

GC
04.1

Linux: von Übersicht zu Ursache

Erst den Engpass-Typ bestimmen, dann mit dem passenden Werkzeug enger heranzoomen.

● ● ●CPU / PROZESSE01
$ top -H -p 1842
%Cpu(s): 78.1 us, 8.2 sy, 9.4 id,
         3.1 wa, 0.0 st
 PID USER  S %CPU
1943 app   R 76.4
$ pidstat -u -t 1

Ein einzelner Thread bei 76 %: thread-spezifisch profilieren. Hohes wa: I/O prüfen. st bedeutet gestohlene VM-Zeit.

● ● ●STORAGE / I/O02
$ iostat -xz 1
Device   await aqu-sz %util
nvme0n1  18.4  12.8   96.2
$ vmstat 1

Hohe await, wachsende Queue und hohe Nutzung sprechen für I/O-Sättigung. %util allein ist bei parallelen SSDs kein vollständiges Kapazitätsmaß.

● ● ●CPU-PROFIL03
$ sudo perf top -p 1842
24.3% acme [.] hash_lookup
18.7% acme [.] Cart.calculate
 9.2% libc [.] malloc
$ perf record -g -p 1842 -- sleep 30
$ perf report

Hotspots nach kumulierter Zeit prüfen, Stacktraces aktivieren, Symbole verfügbar machen. Sampling ist statistisch; Rechte und Profiling-Overhead beachten.

● ● ●eBPF / TRACING04
$ sudo biolatency -m 10
usecs     : count distribution
0 - 1     : 8 |****
128 - 256 : 91 |****************
$ sudo runqlat 1

BCC/bpftrace zeigen Kernel-Latenzen, Run-Queue-Wartezeit oder Block-I/O. Kernel-Version, BTF, Capabilities und Produktions-Overhead beachten.

Kommentierte Momentaufnahmen: top, vmstat und iostat

Beispielwerte sind illustrativ. Für eine Diagnose den Verlauf während derselben reproduzierbaren Testphase erfassen. Die erste vmstat-Zeile nach Programmstart enthält häufig Mittelwerte seit dem Systemstart.

● ● ●top: CPU UND PROZESSE
top - 14:32:10 up 12 days,  3 users
%Cpu(s): 71.0 us, 12.0 sy,  9.0 id,
          7.0 wa,  1.0 st
MiB Mem :  16000 total,  1200 free
 PID USER  %CPU %MEM S COMMAND
1842 app    165  8.2 R java

us + sy ist hoch: User- und Kernel-Arbeit beanspruchen CPU. wa=7% weist zusätzlich auf I/O-Wartezeit hin. 165 % entspricht ungefähr 1,65 logischen CPUs; Thread-Ansicht und cgroup-Limits prüfen.

● ● ●vmstat: RUN QUEUE UND SWAP
vmstat 1
 r  b   swpd   free  si  so  us sy id wa
 9  2  524288 180000 128 256 71 12  9  7
11  3  524288 176000  64 128 74 13  6  7

r sind ausführbare oder laufende Tasks, b ununterbrechbar wartende Tasks. Anhaltendes si/so bedeutet Swap-Aktivität. Bei 8 vCPUs deutet r=9–11 auf CPU-Rückstau, beweist ihn ohne Verlauf und Kontext aber nicht.

● ● ●iostat: DEVICE QUEUE
iostat -xz 1
Device   r/s  w/s  await aqu-sz %util
nvme0n1  820  410   18.4   12.8  96.2
nvme1n1  110   24    1.2    0.3  14.1

nvme0n1 zeigt höhere mittlere Wartezeit und Queue als das zweite Gerät. Mapping, konkurrierende Workloads und Tail-Latenz prüfen. Bei parallelen SSDs ist %util allein keine sichere Kapazitätsgrenze.

DATENBANKEN

Query-Plan statt Bauchgefühl

POSTGRESQL

Slow Queries nach Gesamtzeit, Aufrufzahl und mittlerer Laufzeit sortieren. pg_stat_statements aggregiert Query-Fingerprints. Mit repräsentativen Parametern EXPLAIN (ANALYZE, BUFFERS) ausführen und geschätzte gegen tatsächliche Zeilen vergleichen.

Seq Scan ist nicht automatisch falsch. Verdächtig sind große Schätzabweichungen, viele gelesene Blöcke, Sorts auf Disk oder Nested Loops mit sehr vielen Wiederholungen.

BEISPIEL: FILTER + SORT
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, created_at FROM orders
WHERE customer_id = 42
ORDER BY created_at DESC LIMIT 20;

CREATE INDEX CONCURRENTLY
  idx_orders_customer_created
ON orders (customer_id, created_at DESC);
● ● ●BEISPIEL: POSTGRESQL-PLANVORHER → NACHHER
-- ohne passenden Index
Seq Scan on orders (actual rows=18432)
  Rows Removed by Filter: 181568
Execution Time: 18.432 ms

-- mit (customer_id, created_at DESC)
Index Scan using idx_orders_customer_created
  on orders (actual rows=20)
Execution Time: 0.084 ms

Illustrative Werte derselben Beispielabfrage: 18,432 ms auf 0,084 ms. Vergleiche denselben Datensatz und dieselben Parameter; ein schnellerer Einzelplan beweist nicht automatisch bessere Gesamtleistung.

EXPLAIN visuell lesen: Scan, Sortierung und tatsächliche Zeilen

Der erste Plan filtert 200.000 Tabellenzeilen und sortiert die Treffer. Der optimierte Index beginnt mit der Gleichheitsbedingung customer_id, hält danach created_at absteigend sortiert und kann nach 20 Treffern stoppen.

VORHER · Seq Scan + Sort
Limit  (actual rows=20 loops=1)
  -> Sort  (actual rows=20 loops=1)
       Sort Key: created_at DESC
       Sort Method: top-N heapsort
       -> Seq Scan on orders
            (actual rows=18432 loops=1)
            Rows Removed by Filter: 181568
            Filter: (customer_id = 42)
Buffers: shared hit=2200 read=310
Execution Time: 18.432 ms

Viele Zeilen geprüft und anschließend sortiert. Bei selektivem Filter entsteht vermeidbare Arbeit.

NACHHER · geordneter Indexzugriff
Limit  (actual rows=20 loops=1)
  -> Index Scan using idx_orders_customer_created
       on orders (actual rows=20 loops=1)
       Index Cond: (customer_id = 42)
Buffers: shared hit=24 read=2
Execution Time: 0.084 ms

Filter und Sortierreihenfolge passen zum Index; das LIMIT beendet den Scan früh.

Zeiten hängen von Cache, Datenverteilung, Hardware und Parallelität ab. EXPLAIN ANALYZE führt die Abfrage tatsächlich aus. Vergleiche erwartete und tatsächliche Zeilen sowie Buffer, nicht nur Gesamtdauer.

05 — DISTRIBUTED SYSTEMS

Engpässe über Systemgrenzen

Verteilte Systeme vervielfachen neben dem Durchsatz auch Netzwerkhops, Queues, Timeouts und Fehlerpfade. Jede Grenze braucht ein Budget und beobachtbares Verhalten unter Sättigung.

EDGEClientDNS · CDN
INGRESSGatewayAuth · Limits
COMPUTEServicePool · Queue
STATECache / DBHit Rate · I/O
SCHICHTTYPISCHES SYMPTOMERSTE PRÜFUNGGEGENMASSNAHME
API GatewayQueueing vor Services, 429/5xx, TLS-CPU, Limits erreichtRate-Limits, Upstream-Pool, Keep-alive, Body-Größe, Gateway-P95Limits an echte Kapazität koppeln, Connection-Reuse, Payload begrenzen; keine unbeschränkten Retries.
Load BalancerUngleiche Instanzlast, Connection-Draining, Health-FlapsAlgorithmus, Sticky Sessions, Health-Checks, Ziel-PoolsReadiness korrekt prüfen, graceful draining, Verbindungsreserve vorhalten.
MicroservicesKaskadierende Latenz, Thread-/Connection-Pool erschöpftTrace-Wasserfall, Queue-Tiefe, Pool-Wait, Timeout-/Retry-MetrikenEnd-to-end Deadline propagieren; Timeout, Retry-Budget, Backoff/Jitter, Bulkhead und Circuit Breaker.
Redis / MemcachedHit Rate fällt, Hot Key, Evictions, Tail-LatenzHit/Miss, Evictions, Fragmentierung, Slow Log, NetzwerkTTL-Jitter, Stampede mit Single-Flight, Hot Keys verteilen, Werte begrenzen; Cache nicht als DB-Ersatz behandeln.
Queue / WorkerBacklog wächst, Alter ältester Nachricht steigtEnqueue/Dequeue, Consumer-Lag, Redelivery, Partition-SkewKapazität/Parallelität erhöhen, Poison Messages isolieren, Backpressure und maximale Queue-Länge.
KubernetesCPU throttling, Neustarts oder träges AutoscalingThrottled time, Requests/Limits, HPA-Metrik, Readiness, Scale-up-VerzögerungLimits anhand Messungen setzen, HPA passend dimensionieren; Cold Start und Warm-up in Kapazität einrechnen.
06 — FIELD GUIDE

Ein belastbarer Arbeitsablauf

Die beste Analyse endet nicht mit einem Dashboard. Sie führt zu einer Entscheidung, einem kontrollierten Eingriff und einem wiederholbaren Nachweis.

01
Ziel und Grenzen festlegen

Nutzerpfad, SLI/SLO, Lastprofil, Testfenster, Abbruchkriterien und Verantwortliche dokumentieren.

02
Workload modellieren

Transaktionsmix, Think Time, Datenverteilung, Cache-Warm-up und Spitzenlast aus Produktionsdaten ableiten und anonymisieren.

03
Baseline und Generator prüfen

Testdaten zurücksetzen, Generator beobachten, Rate verifizieren sowie Metriken, Traces und Logs zeitlich synchronisieren.

04
Eine Dimension verändern

Baseline, Ziel-Last, Stress- und Soak-Test unterscheiden. Build, Konfiguration, Daten und Skript pro Lauf versionieren.

05
Ursache triangulieren

Mit RED/USE eingrenzen, Trace öffnen, Ressource oder Thread profilieren und gegen die konkrete Hypothese wiederholen.

06
Kapazität kommunizieren

Durchsatz am SLO, P95/P99, Fehlerquote, Sättigungsgrenze und Sicherheitsmarge berichten, nicht nur den Bestwert.

↗
DER LERNPFAD IN KURZFORM

Fundamentale → Tool → Signale → System → Architektur

Baue entlang echter Fragen: Checkout mit k6 belasten, ein P95-Dashboard erstellen, einen langsamen SQL-Plan erklären und ein CPU-Profil vor/nach einer Änderung vergleichen. Jedes Projekt hält Hypothese, Methode, Ergebnis und Trade-off fest.

07 — DELIVERY GATES

Performance-Tests in CI/CD

Performance-Prüfungen werden dann verlässlich, wenn sie zur passenden Pipeline-Stufe passen: schnell und deterministisch bei jeder Änderung, umfassender auf planbaren Testumgebungen und langlaufend als regelmäßiger Kapazitätsnachweis.

Teststufen nach Feedbackzeit

Pull Request: kurze Smoke- oder Mikrobenchmarks mit kleiner, stabiler Last und klaren Korrektheitschecks. Merge oder Release-Kandidat: repräsentativer Workload gegen feste Umgebung und Baseline. Nightly: Ramp-up, Stress- und Soak-Tests sowie größere Datenmengen. Große Lasttests nicht auf gemeinsam genutzten Runnern ausführen, wenn deren CPU, Netzwerk oder Nachbarn nicht kontrolliert sind.

Performance Budget definieren

Budgetiert werden Nutzer-SLIs und nicht bloß Ressourcen: etwa Fehlerquote unter 0,5 %, P95 unter 350 ms, P99 unter 900 ms bei mindestens 100 Requests/s. Zusätzlich Generator-Drops auf null und Mindestzahl erfolgreicher Samples fordern. Absolute Grenzwerte schützen das Ziel; relative Regression gegen eine versionierte Baseline erkennt Änderungen. Vergleichswerte nur bei gleicher Hardware, Datenmenge, Konfiguration und Testdauer gegenüberstellen.

STUFEUMFANGGATEERGEBNIS
Pull Request1–3 Minuten, kleiner Smoke-Workload, funktionale AssertionsKeine fachlichen Fehler; grobe Latenz- und DurchsatzgrenzenKommentar mit Trend, keine Kapazitätszusage
Release-KandidatRepräsentativer Transaktionsmix, mehrere Steady-State-FensterSLI-Budgets erfüllt; keine relevante Regression zur BaselineVersionierter Bericht, Logs und Dashboard-Snapshot
Nightly / geplantStress, Soak, Skalierung, Produktionsnahe DatenmengeKapazitätsziele, Stabilität, Fehlerbudget und Generator-GesundheitTrendbericht und Ticket/Alarm bei bestätigter Regression
Beispiel: GitHub-Actions-Quality-Gate

Das Beispiel startet einen kurzen k6-Lauf, macht Schwellenwerte zum Prozess-Exitcode und archiviert JSON-Ergebnisse. Secrets kommen aus dem CI-Secret-Store. Für belastbare Vergleiche Runner-Größe und Zielumgebung fixieren; das PR-Beispiel ist ein Regression-Signal, keine abschließende Kapazitätsmessung.

YAML .github/workflows/performance.yml
name: performance-smoke
on:
  pull_request:
  workflow_dispatch:

jobs:
  smoke:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: grafana/setup-k6-action@v1
      - name: Run performance smoke test
        env:
          BASE_URL: ${{ secrets.STAGING_BASE_URL }}
          TEST_TOKEN: ${{ secrets.PERF_TEST_TOKEN }}
        run: |
          k6 run --summary-export=summary.json \
            -e BASE_URL="$BASE_URL" \
            -e TEST_TOKEN="$TEST_TOKEN" \
            tests/checkout-smoke.js
      - name: Archive result
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: performance-${{ github.sha }}
          path: summary.json
          if-no-files-found: ignore

In k6 Thresholds zum Beispiel http_req_failed: ['rate<0.005'], http_req_duration: ['p(95)<350', 'p(99)<900'] und dropped_iterations: ['count==0'] definieren. Ein fehlgeschlagener Threshold liefert einen von null verschiedenen Exitcode.