Ich zeige praxisnah, wie irq affinity auf Mehrprozessorsystemen Netzwerk‑Interrupts gezielt an CPU‑Cores bindet, Latenz drückt und Durchsatz hebt. Mit klaren Schritten, Beispielen und einer Tabelle lege ich fest, wie ich Hex‑Masken wähle, NUMA beachte und Prozesse passend pinne.
Zentrale Punkte
- IRQ Affinity lenkt Hardware‑Interrupts gezielt auf CPUs und senkt Overhead.
- CPU Affinity für Dienste an dieselben Cores pinnen hält Caches lokal.
- NUMA beachten, Adapter und Cores derselben Node nutzen.
- irqbalance abwägen: automatisch verteilen oder manuell feinsteuern.
- Monitoring und iterative Anpassung sichern stabile Latenzen.
IRQ Affinity verstehen: Grundlagen und Wirkung
Auf Linux‑Hosts lösen eingehende Pakete, Disk‑I/O und Timer IRQs aus, die der Kernel an CPU‑Cores verteilt. Ich bestimme über die Dateien /proc/irq/<IRQ>/smp_affinity und .../smp_affinity_list, welche CPUs eine Quelle bedienen dürfen. Eine breite Standardmaske wirkt zunächst flexibel, doch sie erzeugt bei Last Cache‑Misses, teure Kontextwechsel und zerstreute SoftIRQs über viele CPUs. Ich binde kritische Queues einzelner NICs an definierte Cores, entlaste Hotspots und halte Datenwege kurz. Diese Lenkung verbessert die Stabilität spürbar, sobald viele Flows gleichzeitig aktiv sind.
IRQ‑ und CPU‑Affinity abstimmen: Daten lokal halten
Ich koppelte erfolgreich die IRQ-Zuständigkeit der NIC‑Queues mit der CPU‑Zugehörigkeit relevanter Worker. Dazu pinne ich Webserver‑ oder Proxy‑Threads mittels taskset oder CPUAffinity= in systemd auf jene Cores, die auch die RX/TX‑Interrupts verarbeiten. Dadurch bleiben Cache‑Lines lokal und ich reduziere Inter‑CPU‑Kommunikation auf ein Minimum, was die Latenz glättet. Gerade API‑Backends, Echtzeit‑Dienste und virtualisierte Stacks profitieren von dieser Kohärenz. Ich teste die Bindung unter Produktionslast, bis Flows, SoftIRQs und User‑Space sauber aufeinanderpassen.
Automatik gegen Feintuning: irqbalance richtig einordnen
Der Dienst irqbalance verteilt Interrupts automatisch über verfügbare Cores, was auf Allzweck‑Servern gut funktioniert. In hoch belasteten Netzwerk‑Setups bremst die Streuung jedoch Cache‑Lokalität und erschwert gezieltes Pinning. Ich schränke irqbalance ein oder deaktiviere ihn selektiv, wenn bestimmte Queues feste Cores benötigen. Für das Grundverständnis und passende Profile hilft mir dieser Leitfaden zu irqbalance konfigurieren. Im Ergebnis steuert die Automatik Unkritisches, während ich sensible IRQs manuell zuweise.
Schritt: Relevante IRQs sichtbar machen
Ich beginne mit einem Blick in /proc/interrupts und filtere nach dem Gerätenamen wie ens192, eno1 oder eth0. Moderne Adapter legen mehrere RX‑ und TX‑Queues an, daher finde ich eine Gruppe von IRQ‑Nummern, die derselben NIC zugeordnet sind. Ich achte auf Zählerstände, um Hotspots schnell zu erkennen und stark beanspruchte Queues zuerst zu binden. Diese Sicht prüfe ich regelmäßig während Lasttests, damit die Zuordnung dauerhaft trägt. Zusätzlich kontrolliere ich Treiberbezeichnungen, weil sie Hinweise auf RSS‑Fähigkeiten und Offloading liefern.
# Alle Interrupts einsehen
cat /proc/interrupts
# Nur NIC-relevante Zeilen anzeigen (Beispiel ens192)
grep -i ens192 /proc/interrupts
NUMA‑Topologie klug nutzen
Auf Hosts mit mehreren Nodes verlagere ich IRQs vorzugsweise auf Cores jener NUMA-Node, an der die NIC physisch hängt. Ich prüfe das mit lscpu sowie numactl --hardware und markiere passende CPU‑Sätze für die spätere Maskenbildung. Prozesse, die diese Netzpfade bedienen, binde ich ebenfalls an dieselbe Node und sorge über Memory‑Policies für lokale Speicher-Zuweisungen. So vermeide ich teure Remote‑Zugriffe quer über die QPI/UPI‑Links. Diese Disziplin bringt in Latenztests rasch messbare Vorteile.
Bitmasken sicher wählen: Hex‑Logik auf einen Blick
Die Datei smp_affinity akzeptiert Hex‑Bitmasken, die direkt auf Core‑IDs abbilden und auch große Systeme abdecken. Ich starte oft mit einfachen Mustern: CPU0 ist 0x1, CPU1 ist 0x2, CPU2 ist 0x4, CPU3 ist 0x8 usw., während 0xF die Cores 0–3 umfasst. Auf Maschinen mit vielen Cores schreibe ich mehrere 32‑Bit‑Blöcke, getrennt durch Kommas, damit die Maske alle IDs sauber abbildet. Diese Übersicht hilft mir, fehlerfreie Zuweisungen vorzunehmen und unbeabsichtigte Verlagerungen zu vermeiden. Die folgende Tabelle nutze ich häufig als Gedächtnisstütze.
| CPU‑Nummer | Bit (binär) | Hex‑Maske | Hinweis |
|---|---|---|---|
| 0 | …0001 | 0x1 | CPU0 oft entlasten und nur sparsam nutzen. |
| 1 | …0010 | 0x2 | IRQ auf CPU1 pinnen. |
| 2 | …0100 | 0x4 | IRQ auf CPU2 pinnen. |
| 3 | …1000 | 0x8 | IRQ auf CPU3 pinnen. |
| 0–3 | …1111 | 0xF | Alle vier Cores zulassen, Latenz steigt oft leicht. |
| 0–7 | 11111111 | 0xFF | Breite Verteilung, Cache‑Lokalität leidet. |
IRQ‑Affinity setzen: so binde ich Queues an Cores
Nachdem ich die Queue‑IRQs der NIC identifiziert habe, verteile ich sie auf dedizierte Cores wie 1, 2 und 3, um parallele Verarbeitung sauber zu skalieren. Damit bleibt jede RX/TX‑Queue bei ihrem Core, was Cross‑Talk vermeidet und SoftIRQ‑Arbeit konsistent hält. Während des Feinschliffs vergleiche ich Durchsatz und Latenz, bis die Verteilung zuverlässig trägt. Für einen tieferen Einstieg hilft mir ein kurzer Praxisleitfaden mit Varianten je nach Adapter. Die Kommandos setze ich bewusst in Wartungsfenstern und sichere sie per Boot‑Skript ab.
# Beispiel: drei Queue-IRQs an CPU1..3 binden
echo 2 > /proc/irq/181/smp_affinity # CPU1 (0x2)
echo 4 > /proc/irq/182/smp_affinity # CPU2 (0x4)
echo 8 > /proc/irq/183/smp_affinity # CPU3 (0x8)
Prozess‑Pinning: Dienste auf dieselben Cores legen
Ich pinne Anwendungs‑Worker auf jene CPUs, die die zugehörigen IRQs verarbeiten, damit Datenpfade kurz bleiben. Unter systemd nutze ich CPUAffinity=1 2 3 oder starte einmalig mit taskset -c 1-3. Für Multi‑Worker‑Server vergebe ich feste Core‑Sätze pro Worker‑Gruppe, damit keine Konkurrenz entsteht. Diese Kopplung bringt konstant niedrigere Latenzen, weil CPU‑Caches treffend gefüllt bleiben. Nach Änderungen kontrolliere ich Threads, Sockets und SoftIRQs mit htop, ss und perf.
Network Tuning: RSS, RPS/RFS und Kernel‑Parameter verbinden
Viele NICs verteilen Pakete per RSS über Queues, die ich anschließend mit IRQ‑Affinity an Cores binde. Details und Wirkprinzipien fasse ich unter Receive Side Scaling kompakt zusammen. Zusätzlich steuere ich RPS/RFS so, dass SoftIRQs nicht gegen die harte Pinning‑Logik arbeiten. Parallel passe ich Puffer über net.core.rmem_max und net.core.wmem_max an und prüfe TCP‑Optionen wie tcp_timestamps. Diese Bausteine zahlen auf dieselbe Zielgröße ein: geringe Latenz bei hoher Bitrate.
MSI‑X und Queue‑Layout richtig deuten
Moderne NICs nutzen MSI‑X und legen für jede RX/TX‑Queue eigene IRQs an. Ich prüfe zuerst, wie viele Queues und Kanäle der Treiber aktuell aktiviert hat, und passe das dem Core‑Budget der passenden NUMA‑Node an. So verhindere ich, dass zu viele Queues auf zu wenige Cores drängen oder umgekehrt Kapazität brachliegt.
# Queue- und Kanalanzahl prüfen und anpassen
ethtool -l ens192 # aktuelle Limits (RX/TX/combined)
ethtool -L ens192 combined 4 # z.B. 4 Queues aktivieren
# RSS-Indirection und Hash-Einstellung sichten
ethtool -x ens192 # Indirection-Table und Hash-Key anzeigen
Viele Treiber benennen IRQs sprechend (z.B. ens192-TxRx-0). Ich halte die Indirection‑Table konsistent zur Core‑Zuweisung, damit Flows stabil auf „ihre“ Queue fallen. Weicht die Hardware‑Verteilung ab, entstehen unnötige Wanderungen der SoftIRQs.
RPS/RFS und XPS stimmig einsetzen
RPS/RFS kann Pakete softwareseitig über Cores verteilen – gut für NICs ohne viele Queues, aber kontraproduktiv, wenn ich bereits per RSS und IRQ‑Affinity exakt binde. Ich entscheide daher bewusst: Entweder harte Bindung über RSS+IRQ‑Affinity und RPS aus, oder wenige Hardware‑Queues und RPS gezielt an. Zusätzlich richte ich XPS für die TX‑Richtung ein, damit ausgehende Pakete von den „richtigen“ Cores abgeschickt werden.
IF=ens192
# RPS komplett deaktivieren (bei sauberem IRQ-Pinning via RSS)
for q in /sys/class/net/$IF/queues/rx-*/rps_cpus; do echo 0 > "$q"; done
echo 0 > /proc/sys/net/core/rps_sock_flow_entries
# Alternativ: RPS gezielt aktivieren (Beispiel: CPUs 1-3)
for q in /sys/class/net/$IF/queues/rx-*/rps_cpus; do echo 0-0,0-0,0-0,0-0 > /dev/null; done
# Besser: smp_affinity_list-ähnliche Syntax nutzen:
for q in /sys/class/net/$IF/queues/rx-*/rps_cpus; do echo 1-3 > "$q"; done
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
echo 32768 > /proc/sys/net/ipv4/tcp_rfs_sock_flow_entries
# XPS passend zu den ausgewählten Worker-CPUs setzen (TX-Pfade)
for q in /sys/class/net/$IF/queues/tx-*; do echo 1-3 > "$q"/xps_cpus; done
Wichtig ist die Kohärenz: RX‑IRQ‑Core, ksoftirqd‑Last und Worker‑Thread sollten auf demselben Core (oder Core‑Paar) liegen. Damit verschwinden viele Cross‑Core‑Bounce‑Effekte.
SMT/Hyper‑Threading und Core‑Paare berücksichtigen
Auf Systemen mit SMT ergibt es oft Sinn, einen physischen Core für einen RX‑IRQ zu reservieren und den zugehörigen Worker auf den Sibling‑Thread zu legen – oder bewusst zu trennen, wenn der Workload rechenlastig ist. Ich ermittele die Thread‑Paare aus der Topologie und treffe dann eine klare Entscheidung statt zufälliger Verteilung.
# Sibling-Paare ermitteln
for c in /sys/devices/system/cpu/cpu*/topology/thread_siblings_list; do
echo "$(basename "$(dirname "$c")") : $(cat "$c")"
done
Teile ich mir RX‑IRQ und User‑Space‑Worker auf demselben physischen Core (verschiedene SMT‑Threads), entsteht gute L1/L2‑Lokalität, aber bei CPU‑Bound‑Workloads kann es Engpässe geben. Alternativ verteile ich IRQ auf Core X und Worker auf Core Y derselben NUMA‑Node, um echte Parallelität zu erhalten. Beide Varianten teste ich A/B und wähle, was die stabilere Latenz liefert.
smp_affinity_list, effective_affinity und Defaults nutzen
Neben Hex‑Masken schreibe ich gern in smp_affinity_list, da sich damit Bereiche wie 1-3,6,8-9 komfortabel setzen lassen. Zur Kontrolle prüfe ich effective_affinity bzw. effective_affinity_list, weil Kernel oder Treiber gewisse CPUs ausklammern können (z.B. offlined Cores oder „managed interrupts“).
# Menschlich lesbare Zuweisung
echo 1-3 > /proc/irq/181/smp_affinity_list
# Effektive Bindung verifizieren
cat /proc/irq/181/effective_affinity_list
Damit neue oder nach einem Treiber‑Reload hinzugekommene IRQs nicht wieder breit gestreut werden, setze ich bei Bedarf /proc/irq/default_smp_affinity auf einen sinnvollen Grundwert (z.B. alle Cores der relevanten NUMA‑Node, jedoch ohne CPU0). Einzelne kritische IRQs übersteuere ich anschließend gezielt.
Persistenz über Reboots und Reloads herstellen
Affinity‑Einstellungen sind flüchtig. Ich sichere sie per systemd‑Oneshot‑Unit, die nach dem Netzwerkinitialisierungs‑Ziel läuft, oder über ein kleines Skript, das IRQ‑Listen dynamisch ermittelt und abbildet. So bleiben Zuweisungen auch nach Kernel‑Updates und Link‑Resets erhalten.
# /usr/local/sbin/net-irq-pin.sh (Beispiel)
#!/bin/bash
set -euo pipefail
IF=${1:-ens192}
CPUS="1-3" # Ziel-CPUs (NUMA-konsistent wählen)
for irq in $(grep -i "$IF" /proc/interrupts | awk '{print $1}' | tr -d ':'); do
echo "$CPUS" > /proc/irq/$irq/smp_affinity_list || true
done
# systemd-Unit (skizziert)
# /etc/systemd/system/net-irq-pin.service
[Unit]
Description=Pin NIC IRQs
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/net-irq-pin.sh ens192
[Install]
WantedBy=multi-user.target
Wichtig: Ich rufe das Skript erneut auf, wenn der Treiber neu geladen oder die Queue‑Anzahl geändert wurde, da sich IRQ‑Nummern dann verschieben.
Virtualisierung und Container: Host und Gast zusammen denken
In KVM‑Umgebungen binde ich auf dem Host die physischen NIC‑IRQs an Cores der passenden NUMA‑Node. Parallel pinne ich vhost‑net‑Threads und den QEMU‑Prozess (oder einzelne vCPUs) ebenfalls dort, damit die Datenpfade host‑seitig kurz bleiben. Im Gast richte ich die IRQ‑Affinity der vNICs an jene vCPUs, die ich host‑seitig mit physischen Cores verheiratet habe. Container‑Workloads (cgroups/cpuset) profitieren, wenn die erlaubten CPUs der Container mit den RX/TX‑Cores des Hosts überlappen – sonst entstehen vermeidbare Fernzugriffe.
Analyse vertiefen: SoftIRQs, NAPI und Stau erkennen
Neben /proc/interrupts schaue ich in /proc/softirqs, um zu sehen, ob viel Arbeit im Kontext von ksoftirqd statt direkt im IRQ‑Handler erledigt wird – ein Hinweis auf anhaltend hohe Last. Mit napi_defer_hard_irqs (Kernel‑abhängig) und sauberen Queue‑Zuweisungen reguliere ich, wie aggressiv NAPI batcht. ethtool -S <IF> gibt mir pro Queue Statistiken für Drops, Busy‑Zustände und Packetrates; hier erkenne ich unbalancierte Queues und justiere Affinity oder RSS‑Indirection entsprechend.
# Schnellüberblick über SoftIRQ-Verteilung
cat /proc/softirqs | egrep 'NET_RX|NET_TX'
# Treiber-/Queue-Statistiken einsehen
ethtool -S ens192 | egrep -i 'rx|tx|drop|busy'
Praxis: Mapping für 4 Queues auf einer Node
Ein typisches Setup, das ich häufig einsetze: NIC auf NUMA‑Node0 mit 4 RSS‑Queues. Ich vermeide CPU0 und binde die Queues an CPU1‑4. Die zugehörigen Web‑ oder Proxy‑Worker lege ich ebenfalls auf 1‑4, XPS weise ich identisch zu, RPS bleibt aus. Damit erhalte ich kurze, konsistente Pfade in beide Richtungen.
IF=ens192
QUEUES=(181 182 183 184) # Beispiel-IRQs (vorher ermitteln)
CPUS="1-4"
# IRQ-Affinity und XPS setzen
for i in ${!QUEUES[@]}; do
echo "$CPUS" > /proc/irq/${QUEUES[$i]}/smp_affinity_list
done
for q in /sys/class/net/$IF/queues/tx-*; do echo "$CPUS" > "$q"/xps_cpus; done
# Worker pinnen (systemd oder taskset)
# systemd: CPUAffinity=1 2 3 4
# Einmalig: taskset -c 1-4 <dienst>
Skaliert die Last weiter, erhöhe ich die Queue‑Anzahl (ethtool -L) bis zur Zahl sinnvoller Cores der Node und verteile starr nach einem wiedererkennbaren Muster (z.B. Queue‑ID → Core‑ID), damit Flows im Zeitverlauf nicht „wandern“.
Best Practices für Multi‑Core‑Hosts
Ich entlaste CPU0, da dort häufig Timer und Kernel‑Hausdienste laufen, die unter Last stören. Kritische IRQs setze ich daher bevorzugt auf andere Cores und lasse CPU0 nur wenige, unkritische Quellen tragen. Auf NUMA‑Systemen bleibe ich konsequent und halte Adapter, IRQs, Prozesse und Speicherzugriffe auf derselben Node. In stark beanspruchten Umgebungen trenne ich I/O‑Cores von Applikations‑Cores und isoliere sie bei Bedarf. Alle Änderungen begleite ich mit fortlaufender Messung und passe die Zuweisungen iterativ an.
Messbar vorgehen: Analyse, Skripte und Rückfallebene
Vor Änderungen dokumentiere ich den Ist‑Zustand mit mpstat, htop, /proc/interrupts und Latenz‑Messungen per iperf3. Ich richte Skripte ein, die Affinity‑Einstellungen nach einem Reboot oder Treiber‑Reload automatisch anwenden. Für Rollbacks halte ich neutrale Masken bereit, damit ich bei Fehlverhalten sofort zurückschalte. In Staging teste ich Lastprofile, die meiner Produktion möglichst nahekommen und wiederhole die Messung nach jeder Anpassung. Erst dann aktiviere ich das Profil dauerhaft auf dem Zielhost.
Häufige Stolpersteine sauber vermeiden
Zu breite Masken verteilen Arbeit über zu viele CPUs und bremsen Caches aus, während zu enge Masken Queues verstopfen. Vergessene NUMA‑Besonderheiten erzeugen Fernspeicher‑Zugriffe, die Antwortzeiten schwanken lassen. Ein hartes Pinning kollidiert gelegentlich mit RPS/RFS‑Einstellungen, daher prüfe ich die SoftIRQ‑Lastverteilung explizit. Nach Kernel‑Updates oder Treiberwechseln validiere ich alle IRQ‑Nummern neu, weil sich Zuordnungen ändern können. Mit vorsichtigen Schritten und klarer Doku bleibe ich handlungsfähig.
Kurz zusammengefasst
Gezieltes IRQ‑Pinning bindet Netzwerk‑Interrupts an wenige passende Cores, senkt Overhead und stabilisiert Antwortzeiten. Ich stimme dazu IRQ‑ und CPU‑Affinity ab, beachte NUMA und kontrolliere die Wirkung mit Messreihen. Wo Automatik genügt, lasse ich irqbalance agieren, kritische Queues führe ich zu festen Cores. Mit RSS, RPS/RFS und angepassten Kernel‑Parametern entfaltet das Tuning seinen vollen Effekt. Wer diese Schritte diszipliniert umsetzt, holt auf Multi‑Core‑Linux‑Servern spürbar mehr Netzwerk‑Leistung heraus.


