StatusType

Performance Optimierung: PreBid Server (2-Node Cluster)

Übersicht

Dieses Dokument fasst die vollständige Untersuchung und alle durchgeführten Maßnahmen zur Behebung der Latenz-Probleme im PreBid-Server-Cluster zusammen.

Scope

November - März


Ausgangssituation

Cluster-Setup

  • 2 Nodes: kube-master (5.199.128.26) + kube-worker (5.199.140.38)
  • k3s v1.30.6, flannel mit WireGuard-Backend (--flannel-backend wireguard-native)
  • 24 Pods (activeagent): 12 auf master, 12 auf worker
  • Externe IPs statisch vom Upstream-Router auf die Node-IPs geroutet (kein ARP/BGP)

Symptome

Benchmark-Ergebnisse zeigten eine starke Tail-Latenz unter Last (100k RPS, 100 Connections):

Percentile2-Node (problematisch)1-Node Referenz (200k RPS)
p50~3 ms< 1 ms
p90~17 ms< 3 ms
p95~245 ms< 3 ms
p99~531 ms< 3 ms
p999~676 ms< 3 ms
>30 ms
~8 %< 0.1 %

Schlechte Performance Active Agent Client

Active Agent betreibt einen internen Schwellwert: Liegt die PBS-Antwortzeit über 3 ms, drosselt der AA-Client die Query-Rate. Das ist by design und bleibt aktiv. Durch die hohe PBS-Latenz (NIC-Queue-Bottleneck, siehe Maßnahme 1) griff dieses Throttling dauerhaft — AA schickte deshalb nur ca. ~25 % der eigentlichen Anfragen an den PBS.

Zwei Client-seitige Konfigurationsprobleme verschärften die Situation zusätzlich:

ProblemAuswirkung
Nur 24 gleichzeitige VerbindungenZu wenig Parallelität; die geringe Verbindungsanzahl konzentrierte die Last auf zu wenige Sockets
Feste IP statt DNS-Round-RobinEffektiv wurde nur ein Node adressiert — der gesamte Traffic lief über eine VM, was den NIC-Queue-Bottleneck auf genau dieser VM noch weiter verschärfte

Durch die Kombination aus PBS-seitigem Bottleneck und ungünstiger Client-Konfiguration war die beobachtete 25%-Rate ein Symptom — nicht die Ursache. Mit Behebung der PBS-Latenz (Maßnahme 1) griff das Throttling deutlich seltener, und AA passte parallel den Client an.

Befund: Single-Ingress-Asymmetrie

Messungen in einem 60-Sekunden-Fenster auf beiden VMs zeigten, dass der gesamte externe Live-Traffic effektiv nur über einen Host lief:

HostVIP1 (5.199.128.26)VIP2 (5.199.140.38)
pve / kube-master3.275.478 Pakete0
MH-19003Y / kube-worker04

DNS prebid.sdk-cloud.de liefert zwei A-Records, wurde jedoch client-seitig auf eine feste IP reduziert. Damit konzentrierte sich der gesamte Ingress auf eine VM und verschärfte den NIC-Queue-Bottleneck massiv.


Maßnahme 1: NIC Multi-Queue (beide VMs)

Problem

Beide VMs nutzten standardmäßig nur eine NIC-Queue. Bei hohem Durchsatz (50k–100k RPS) führte das zu IRQ-Contention auf einem einzelnen CPU-Kern — alle Netzwerk-Interrupts wurden von einem Kern verarbeitet.

Lösung

Drei Teilschritte auf beiden VMs:

1. Hypervisor (Proxmox): NIC-Queues auf 8 erhöhen

# kube-master (VMID 100); für kube-worker entsprechend anpassen
VMID=100
CUR_NET0="$(qm config $VMID | awk -F': ' '/^net0:/{print $2}')"
if echo "$CUR_NET0" | grep -q 'queues='; then
  NEW_NET0="$(echo "$CUR_NET0" | sed -E 's/queues=[0-9]+/queues=8/')"
else
  NEW_NET0="${CUR_NET0},queues=8"
fi
qm set "$VMID" --net0 "$NEW_NET0"
qm reboot "$VMID"

2. In der VM: Multi-Queue aktivieren und verifizieren

IFACE=ens18
ethtool -L "$IFACE" combined 8 || true
# Verifizierung:
ethtool -l "$IFACE"
# Combined: 8

3. RPS (Receive Packet Steering) auf alle RX-Queues

Verteilt eingehende Pakete softwarebasiert auf alle CPU-Kerne — entscheidend, da irqbalance allein nicht ausreicht:

IFACE=ens18
NCPU="$(nproc)"
MASK="$(printf '%x' $(( (1<<NCPU)-1 )))"
RXQS="$(ls -d /sys/class/net/$IFACE/queues/rx-* | wc -l)"
FLOW_PER_Q=$((65536 / RXQS))
sysctl -w net.core.rps_sock_flow_entries=65536
 
for q in /sys/class/net/$IFACE/queues/rx-*; do
  echo "$MASK" > "$q/rps_cpus"
  [ -w "$q/rps_flow_cnt" ] && echo "$FLOW_PER_Q" > "$q/rps_flow_cnt"
done

4. irqbalance aktivieren

apt install -y irqbalance
systemctl enable --now irqbalance

Nachweis

Messung per mpstat / pidstat auf kube-master vor der Maßnahme:

  • CPU13 avg softirq: 97.47 % — praktisch vollständig durch Softirq gesättigt
  • ksoftirqd/13 avg CPU: 83–85 % — klarer Haupttreiber
  • Zusatzlast durch kworker/13:x-wg-crypt-flannel-wg: ~3–4 % (WireGuard-Krypto des Cross-Node-Traffics)
MetrikVorherNachher
CPU13 avg softirq~97 %3.57 %
ksoftirqd/13 avg CPU~85 %0.12 %
CPU13 avg idle~1 %83 %

Auswirkung

  • Netzwerk-Interrupts werden über 8 CPU-Kerne verteilt
  • RPS verteilt Paketverarbeitung zusätzlich softwarebasiert über alle Kerne
  • Reduziert IRQ-Bottleneck bei hohem RPS
  • Voraussetzung für skalierbare Durchsatz-Performance

Maßnahme 2: klipper-lb → NodePort (Kubernetes)

Root Cause: k3s klipper-lb Cross-Node Forwarding

Untersuchung

Ausgeschlossen

HypotheseBeweis
Cross-node Traffic auf ens18 (Port 6969/6970)tcpdump -i ens18 'port 6969 or 6970' → kein 10.10.10.x ↔ 10.10.10.x Traffic
Pod-Verteilung ungleichmäßigEndpoints: 12 Pods auf master, 12 auf worker — gleichmäßig
Ressourcen-Sättigung (CPU/RAM)Vom Betreiber ausgeschlossen
Azure LB / externes LBKein Azure — bare-metal k3s
TCP Buffer-Größen (512 Bytes)Wäre auch bei 200k RPS auf 1 Node aufgefallen

Gefunden

tcpdump -i any -n 'net 10.42.1.0/24'   # auf kube-master
tcpdump -i any -n 'net 10.42.0.0/24'   # auf kube-worker

Kube-worker’s svclb-Pod (10.42.1.249) leitet Verbindungen zu Master-Pods weiter:

10.42.1.249 → 10.42.0.56:6970   flannel-wg
10.42.1.249 → 10.42.0.50:6970   flannel-wg
10.42.1.249 → 10.42.0.57:6970   flannel-wg

Kube-master’s svclb-Pod (10.42.0.140) leitet Verbindungen zu Worker-Pods weiter:

10.42.0.140 → 10.42.1.33:6970   flannel-wg
10.42.0.140 → 10.42.1.34:6970   flannel-wg

Interface: flannel-wg — WireGuard-verschlüsselter Overlay-Tunnel zwischen den Nodes.


Root Cause

k3s ServiceLB (klipper-lb) ist ein Userspace-Proxy

k3s klipper-lb funktioniert nicht als reines iptables-DNAT. Es ist ein Userspace-TCP-Proxy:

  1. klipper-lb akzeptiert eingehende Verbindungen auf dem Host-Port
  2. Es erstellt eine neue TCP-Verbindung zum Backend
  3. Dabei verbindet es sich über die ClusterIP des internen Service
  4. kube-proxy wählt für ClusterIP-Traffic ein zufälliges Endpoint aus — auf beiden Nodes
Extern → Node-IP:6970
         └─► klipper-lb (Userspace, svclb-Pod)
              └─► ClusterIP:6970
                   └─► kube-proxy DNAT (Cluster-Policy, nicht Local!)
                        ├─► lokaler Pod (10.42.0.x)  ~50 %
                        └─► remote Pod (10.42.1.x)   ~50 %  ← flannel-wg (WireGuard)

Warum externalTrafficPolicy: Local nicht hilft

externalTrafficPolicy: Local steuert in k3s nur, ob klipper-lb auf einem Node startet (nur wenn lokale Pods vorhanden). Die Backend-Auswahl erfolgt über die ClusterIP, die immer Cluster-Policy verwendet — und damit zu jedem Pod auf beiden Nodes routen kann.

Warum es auf 1 Node funktioniert hat

Mit einem einzigen Node gibt es keine remote Pods. kube-proxy wählt zwangsläufig immer einen lokalen Pod. Kein WireGuard-Hop, keine zusätzliche Latenz.


Auswirkung

  • ~50 % aller Verbindungen, die klipper-lb aufbaut, gehen cross-node
  • Cross-node Traffic traversiert den flannel-wg WireGuard-Tunnel
  • WireGuard-Overhead: Verschlüsselung, Encapsulation, zusätzlicher RTT
  • Erklärt den harten Latenz-Cliff ab p90 und den langen Tail bis p999

Ergebnis nach beiden Maßnahmen

PercentileVorherNachher
p50~3 ms< 1 ms
p90~17 ms~1.2 ms
p95~245 ms~1.5 ms
p99~531 ms< 5 ms
>30 ms~8 %0.06 %

Ergebnisse (Rückmeldung Active Agent)

  • Peaks bis 350k RPS
  • stabile timeout rate von 1.2%
    => Problem gelöst.

Prävention / Runbook für künftige Vorfälle

Bei Latenz-Spikes (starke p95/p99-Degradation unter Last) in dieser Reihenfolge prüfen:

  1. Softirq/ksoftirqd-Hotspot prüfen:
    mpstat -P ALL 1 60
    pidstat -t -p ALL 1 30 | egrep 'ksoftirqd/[0-9]+'
  2. Hypervisor-Queues: Läuft die VM mit queues=8?
    qm config <VMID> | grep net0
  3. Guest-Queues: Hat das Interface mehrere RX-Queues?
    ethtool -l ens18
    ls /sys/class/net/ens18/queues/
  4. RPS aktiv?
    cat /sys/class/net/ens18/queues/rx-0/rps_cpus   # darf nicht '00' sein
  5. irqbalance aktiv? systemctl status irqbalance
  6. Traffic-Asymmetrie: Läuft Live-Traffic auf beide VMs? (60s tcpdump-Count auf beiden Hosts, siehe HOWTO Cheatsheet)
  7. Danach erneute Latenz-Messung mit prebid-bench (p95/p99)

Learnings

  • Ein Netzwerkpfad-Hotspot kann wie ein Applikationsproblem aussehen — die Ursache lag im Kernel-Paketpfad, nicht in der PBS-Anwendungslogik.
  • Mehr CPU-Kerne allein helfen nicht, wenn eingehende Pakete nicht über RX-Queues verteilt werden.
  • Multi-Queue + RPS war der entscheidende Hebel. irqbalance allein reicht für diesen Lasttyp nicht aus.
  • Client-seitige Verbindungsasymmetrie (feste IP statt DNS-Round-Robin) kann einen infrastrukturseitigen Bottleneck erheblich verstärken.
  • externalTrafficPolicy: Cluster in k3s klipper-lb führt zu ~50 % Cross-Node-Traffic über WireGuard — eine strukturelle Latenz-Falle in 2-Node-Setups.