5 Punkte von GN⁺ 2024-07-29 | 1 Kommentare | Auf WhatsApp teilen
  • Beim Tuning der Linux-Netzwerk-Performance geht es darum, den Weg eines Pakets durch NIC-Ringpuffer, IRQ, NAPI, softIRQ, qdisc und TCP-Puffer bis zum Anwendungssocket anhand möglicher Engpässe zu interpretieren.
  • Im Empfangspfad schreibt die NIC Pakete per DMA in den RAM und löst einen HardIRQ aus; der Treiber plant NAPI ein, leert im NET_RX_SOFTIRQ den Ringpuffer und übergibt die Pakete anschließend an die IP/TCP-Schichten und den Socket-Empfangspuffer.
  • ethtool, /proc/net/softnet_stat, ss, netstat und sysctl sind die Ausgangspunkte für Beobachtung und Anpassung; die wichtigsten Stellschrauben sind Interrupt Coalescing, IRQ affinity, RSS/RPS/RFS/aRFS, netdev_budget, netdev_max_backlog, txqueuelen sowie TCP-read/write-Puffer.
  • Es gibt keine einzelne Einstellung, die für alle Systeme passt: Größere Ringpuffer können Drops reduzieren, aber die Latenz erhöhen; Interrupt Coalescing kann CPU-Auslastung und HardIRQs senken, erzeugt dafür jedoch Latenzkosten.
  • Hochperformante Paketverarbeitung lässt sich mit Optionen wie PACKET_MMAP, DPDK, PF_RING und XDP/AF_XDP erweitern, doch je nach Kernel-Bypass, Zero-Copy oder schnellem Pfad im Kernel unterscheiden sich Hardware-Abhängigkeiten, CPU-Belegung und Anforderungen an die Kernel-Version.

Linux-Empfangspfad: von der NIC bis zum Socket

  • Netzwerkgeräte lösen IRQs aus, um das Eintreffen von Paketen zu signalisieren; das IRQ-Mapping unter Linux ist in /proc/interrupts gespeichert.
  • IRQ-Handler laufen mit sehr hoher Priorität und können weitere IRQs blockieren; daher verschieben Treiber längere Arbeiten aus dem IRQ-Kontext heraus.
  • Für diese verzögerte Verarbeitung werden softIRQs verwendet; bei der Netzwerk-Empfangsverarbeitung werden pro CPU ein Kernel-Thread ksoftirqd/<cpu-number>, softnet_data und poll_list angelegt.
  • net_dev_init registriert NET_RX_SOFTIRQ im softIRQ-System; der zugehörige Handler ist net_rx_action.
  • Paketeingang und NAPI-Verarbeitung

    • Die NIC schreibt die aus dem Netzwerk empfangenen Daten per DMA in einen Ringpuffer im RAM.
    • Manche NICs sind Multiqueue-NICs mit mehreren Ringpuffern.
    • Wenn die NIC einen HardIRQ auslöst, wird der IRQ-Handler des Treibers ausgeführt.
    • Der Treiber quittiert den IRQ der NIC und ruft napi_schedule auf, um die NAPI-softIRQ-Poll-Schleife zu starten.
    • napi_schedule fügt die NAPI-Poll-Struktur des Treibers zur poll_list der aktuellen CPU hinzu und setzt das softIRQ-pending-Bit.
    • Wenn ksoftirqd __do_softirq aufruft, wird der Handler net_rx_action des ausstehenden NET_RX_SOFTIRQ ausgeführt.
  • GRO und Eintritt in den Protokoll-Stack

    • net_rx_action prüft die NAPI-Poll-Liste und kontrolliert budget sowie die verstrichene Zeit, damit softIRQ die CPU nicht monopolisiert.
    • Die poll-Funktion des Treibers erntet Pakete aus dem Ringpuffer im RAM.
    • Die Pakete werden an napi_gro_receive übergeben.
    • GRO (Generic Receive Offloading) ist ein softwarebasiertes Offloading-Verfahren, das kleine Pakete wieder zu größeren Paketen zusammensetzt und so die Anzahl der Pakete reduziert, die die Anwendung verarbeiten muss.
    • Wenn GRO das Paket nicht zurückhält, wandert es über netif_receive_skb nach oben in den Protokoll-Stack.
  • Verzweigung je nachdem, ob RPS aktiviert ist

    • Wenn RPS deaktiviert ist:
      • netif_receive_skb übergibt die Daten an __netif_receive_core.
      • __netif_receive_core leitet die Daten an tap und die registrierten Handler der Protokollschichten weiter.
    • Wenn RPS aktiviert ist:
      • netif_receive_skb übergibt die Daten an enqueue_to_backlog.
      • Das Paket gelangt in die CPU-spezifische input queue.
      • Die NAPI-Struktur der entfernten CPU wird zur poll_list dieser CPU hinzugefügt, und ein IPI wird in die Warteschlange gestellt, um den softIRQ-Thread der entfernten CPU aufzuwecken.
      • ksoftirqd auf der entfernten CPU erntet mit der Poll-Funktion process_backlog Pakete aus der CPU input queue.
  • IP, TCP und Socket-Empfangspuffer

    • Pakete werden in der IPv4-Schicht mit ip_rcv empfangen und durchlaufen netfilter sowie Routing-Optimierungen.
    • Daten, die an das aktuelle System gerichtet sind, werden an höhere Protokollschichten wie UDP oder TCP weitergegeben.
    • Im TCP-Empfangspfad gelangen sie über tcp_v4_rcv, die TCP finite state machine und die Socket-Suche in den Empfangspuffer.
    • Die Größe des Empfangspuffers folgt den Regeln von tcp_rmem.
    • Wenn tcp_moderate_rcvbuf aktiviert ist, passt der Kernel den Empfangspuffer automatisch an.
    • tcp_rmem enthält Mindestwert, Standardwert und Höchstwert für den Empfangspuffer eines TCP-Sockets.
    • Wird SO_RCVBUF verwendet, wird die automatische Anpassung des Empfangspuffers für diesen Socket deaktiviert.
    • net.core.rmem_max ist die Obergrenze für die Größe des TCP-Empfangspuffers; größere Fenster erlauben es, vor dem Senden eines ACK mehr Daten zu übertragen, was die Latenz verringern und den Durchsatz erhöhen kann.

Linux-Sendepfad: von der Anwendung bis zur NIC

  • Der Sendepfad ist einfacher als der Empfangspfad, umfasst aber qdisc, TCP-write buffer, DMA und IRQ.
  • Wenn eine Anwendung mit einem Aufruf wie sendmsg eine Nachricht sendet, weist der TCP-Sendepfad einen skb_buff zu.
  • Pakete gelangen in den Socket-write buffer mit der Größe von tcp_wmem.
    • tcp_wmem enthält Mindestwert, Standardwert und Höchstwert für den Sendepuffer eines TCP-Sockets.
    • Der Kernel passt die Größe des TCP-Sendepuffers dynamisch zwischen Mindest- und Höchstwert an.
    • Wird SO_SNDBUF verwendet, wird die automatische Anpassung des Sendepuffers für diesen Socket deaktiviert.
    • net.core.wmem_max ist die Obergrenze für die Größe des TCP-Sendepuffers.
  • TCP-Header und IP-Header werden erzeugt; nach LOCAL_OUT, Routing, POST_ROUTING und fragmentation wird mit dev_queue_xmit die L2-Sendefunktion aufgerufen.
  • Die ausgehende qdisc verwendet die Länge txqueuelen und den Algorithmus default_qdisc.
  • Der Treiber legt Pakete in den TX-Ringpuffer und führt nach einem tx-usecs-Timeout oder nach tx-frames NET_TX_SOFTIRQ aus.
  • Die NIC holt die Pakete per DMA aus dem RAM und sendet sie; nach Abschluss der Übertragung löst sie einen HardIRQ aus.
  • Der Treiber verarbeitet diesen IRQ und plant das NAPI-Poll-System ein, um RAM freizugeben.

Beobachtungs-Tools und grundlegende Prüfpunkte

  • /proc/net/softnet_stat

    • Jede Zeile in /proc/net/softnet_stat steht für einen CPU-Kern, beginnend mit CPU0
    • Die Statistiken der einzelnen Spalten werden hexadezimal angegeben
    • Die 1. Spalte ist die Anzahl der Frames, die der Interrupt-Handler empfangen hat
    • Die 2. Spalte ist die Anzahl der Frames, die wegen Überschreitung von netdev_max_backlog verworfen wurden
    • Die 3. Spalte ist die Anzahl der Fälle, in denen noch Arbeit zu erledigen war, ksoftirqd aber netdev_budget oder die CPU-Zeit ausgeschöpft hatte
    • Die übrigen Spalten können je nach Linux-Version variieren
  • /proc/net/sockstat und ss

    • In /proc/net/sockstat prüft man das Feld mem
    • Dieser Wert wird berechnet, indem sk_buff->truesize über alle Sockets aufsummiert wird
    • ss ist ein Tool zum Dumpen von Socket-Statistiken und kann ähnliche Informationen wie netstat sowie zusätzliche TCP- und Zustandsinformationen anzeigen
    • ss -tm wird genutzt, um die Speichernutzung von TCP-Sockets zu prüfen
    • rmem_alloc: Speicher, der für empfangene Pakete alloziert wurde
    • rcv_buf: gesamter Speicher, der für empfangene Pakete alloziert werden kann
    • wmem_alloc: Speicher, der für ausgehende Pakete verwendet wird, die bereits an Layer 3 übergeben wurden
    • snd_buf: gesamter Speicher, der für ausgehende Pakete alloziert werden kann
    • wmem_queued: Speicher, der für ausgehende Pakete alloziert wurde, die noch nicht an Layer 3 übergeben wurden
    • sock_drop: Anzahl der Pakete, die verworfen wurden, bevor sie auf einen Socket demultiplexed wurden
  • netstat und sysctl

    • netstat ist ein Kommandozeilen-Tool, das offene Netzwerkverbindungen und Statistiken des Protokoll-Stacks ausgibt und seine Informationen aus dem Dateisystem /proc/net/ bezieht
    • /proc/net/dev: Geräteinformationen
    • /proc/net/tcp: TCP-Socket-Informationen
    • /proc/net/unix: Informationen zu Unix-Domain-Sockets
    • sysctl ist ein Befehl zum Ändern von System- und Netzwerkeinstellungen, statt Werte direkt in das /proc-Dateisystem zu schreiben
    • sysctl -w variable=value wird für temporäre Änderungen verwendet; für dauerhafte Änderungen bearbeitet man /etc/sysctl.conf und wendet sie anschließend mit sysctl -p an

NIC-Ringpuffer und Interrupt-Tuning

  • NIC-Ringpuffer

    • Der RX-Ringpuffer ist ein FIFO-Ringpuffer fester Größe im RAM
    • Der Ringpuffer selbst enthält keine Paketdaten, sondern Deskriptoren, die auf per DMA in den RAM geschriebene skb zeigen
    • Wenn Drops oder Overruns zu sehen sind, kann man die Queue-Größe erhöhen, als Nebenwirkung kann jedoch die Latenz steigen
    • In vielen Fällen reicht es aus, die Größe des Empfangspuffers zu erhöhen, um Paket-Drops zu verhindern; dadurch bekommt der Kernel etwas mehr Zeit, den Puffer zu leeren
    • Prüfung und Änderung erfolgen mit ethtool
    • ethtool -g eth3: aktuelle RX/TX-Ringgrößen und Maximalwerte prüfen
    • ethtool -G eth3 rx 8192 tx 8192: RX/TX-Puffer auf den Maximalwert erhöhen
    • Die Überwachung erfolgt mit ethtool -S eth3 und Countern wie err, drop, over, miss, timeout, reset, collis
  • Hardware-Interrupt-Coalescing

    • Die NIC kann Paket-Referenzen im RX-Ringpuffer sammeln, bis ein rx-usecs-Timeout oder die Bedingung rx-frames erreicht ist, und dann einen HardIRQ auslösen; dies nennt man Interrupt coalescence
    • Werden Interrupts zu früh ausgelöst, unterbricht der Kernel laufende Arbeit zu häufig, was die Systemleistung verschlechtert
    • Werden Interrupts zu spät ausgelöst, kann die NIC den Traffic möglicherweise nicht schnell genug abarbeiten, wodurch Überschreibungen und Traffic-Verluste entstehen können
    • Das Tuning des Interrupt-Coalescing kann CPU-Nutzung und HardIRQs reduzieren und den Durchsatz erhöhen, kann aber Latenzkosten verursachen
    • Mit ethtool -c eth3 prüft man die Coalesce-Parameter; sie lassen sich etwa mit ethtool -C eth3 adaptive-rx off rx-usecs 0 rx-frames 0 ändern
    • Im Adaptive Mode schätzt die Karte die Coalescing-Einstellungen dynamisch anhand der Traffic-Muster und der Empfangsmuster des Kernels

IRQ-Affinity und Lastverteilung zwischen CPUs

  • IRQ-Affinity

    • IRQs haben ein Attribut smp_affinity, das die CPU-Kerne definiert, auf denen der ISR dieses IRQs ausgeführt werden darf
    • Wenn man Interrupt-Affinity und die Affinity von Anwendungsthreads auf bestimmte CPU-Kerne abstimmt, kann sich die Anwendungsleistung durch gemeinsame Cache-Lines verbessern
    • Standardmäßig übernimmt der Daemon irqbalance die Steuerung
    • Vor manuellen Anpassungen muss irqbalance gestoppt werden
    • In /proc/irq/<IRQ_NUMBER>/smp_affinity ist eine hexadezimale Bitmaske gespeichert, die die CPU-Kerne repräsentiert
    • Auf einem Server mit 4 Kernen bedeutet der Standardwert f, dass der IRQ auf allen CPUs verarbeitet werden kann
    • echo 1 > /proc/irq/32/smp_affinity sorgt dafür, dass nur CPU0 verwendet wird
    • Auf Systemen mit mehr als 32 Kernen werden 32-Bit-Gruppen durch Kommas getrennt
    • IRQ-Affinity kann nur bei sehr spezifischen Konfigurationen und vorab definierten Workloads die Performance verbessern und ein zweischneidiges Schwert sein
  • RSS

    • Wenn eine schnelle NIC Pakete nur über eine einzelne Queue und eine einzelne CPU empfängt, kann ein Kern die gesamte Datenverarbeitung tragen, während die anderen Kerne im Leerlauf sind
    • RSS (Receive-side scaling) ist eine NIC-Technik, bei der die NIC Traffic auf mehrere Sende- und Empfangsqueues verteilt
    • Die NIC berechnet einen Hash auf Basis von Source-/Destination-IP sowie TCP/UDP-Source-/Destination-Port, weist Pakete desselben Flows einer einzelnen Queue zu und verteilt Flows gleichmäßig auf die Queues
    • RSS bietet in Multiprocessing-Umgebungen Vorteile durch parallele Empfangsverarbeitung
    • Laut Linux-Kernel-Dokumentation sollte RSS aktiviert werden, wenn Latenz wichtig ist oder die Verarbeitung von Empfangs-Interrupts zum Bottleneck wird; bei Low-Latency-Networking ist eine Konfiguration optimal, die so viele Queues zuweist, wie das System CPUs hat
  • RPS, RFS, aRFS

    • RPS (Receive Packet Steering) ist näherungsweise eine Software-Implementierung von RSS
    • Während RSS die Queue und CPU auswählt, auf der der Hardware-Interrupt-Handler läuft, wählt RPS die CPU aus, auf der die Protokollverarbeitung oberhalb des Interrupt-Handlers ausgeführt wird
    • CONFIG_RPS ist erforderlich und ist auf SMP-Systemen standardmäßig aktiviert
    • Die Konfiguration erfolgt über die CPU-Bitmap in /sys/class/net/<dev>/queues/rx-<n>/rps_cpus
    • Wenn RSS vorhanden ist, kann RPS überflüssig sein; wenn es aber mehr CPUs als Queues gibt, kann es nützlich sein
    • RFS (Receive Flow Steering) erweitert RPS um Application Locality
    • RPS verteilt Pakete Flow-basiert, berücksichtigt aber nicht, auf welcher CPU die Userspace-Anwendung läuft
    • RFS verwaltet mit rps_sock_flow_table eine globale Flow-to-CPU-Tabelle
    • Die Tabellengröße lässt sich mit net.core.rps_sock_flow_entries anpassen
    • Die per-Queue geführte rps_dev_flow_table wird genutzt, um Probleme mit vertauschter Reihenfolge durch verbleibende Pakete zu reduzieren, wenn der Scheduler eine Anwendung auf eine neue CPU verschiebt
    • aRFS (Accelerated RFS) ist ein hardwarebeschleunigter Mechanismus zur Lastverteilung für RFS
    • Da Pakete direkt an eine CPU nahe dem Thread gesendet werden, der die Daten konsumiert, kann aRFS eine bessere Performance als RFS erzielen
    • Erforderlich sind ndo_rx_flow_steer der NIC, ntuple-Filtering und CONFIG_RFS_ACCEL
    • Da die Zuordnung von CPUs zu Queues automatisch aus der IRQ-Affinity der jeweiligen Empfangsqueue abgeleitet wird, ist keine zusätzliche Konfiguration nötig

softIRQ, qdisc, TCP-Puffer-Tuning

  • softIRQ-Budget

    • NAPI-Polling-Routinen werden durch netdev_budget_usecs, netdev_budget und dev_weight begrenzt, damit softIRQ nicht die CPU monopolisiert
    • Der Standardwert von net.core.netdev_budget ist 300; das bedeutet, dass der softIRQ-Prozess 300 Nachrichten von der NIC abarbeitet, bevor er die CPU wieder freigibt
    • net.core.netdev_budget_usecs ist die maximale Anzahl an Mikrosekunden eines NAPI-Polling-Cycles
    • net.core.dev_weight ist die maximale Anzahl von Paketen pro CPU, die der Kernel in einem NAPI-Interrupt verarbeiten kann
    • Wenn in /proc/net/softnet_stat andere Spalten als die 1. Spalte ansteigen, kann eine Änderung des Budgets nötig sein; geringe Anstiege können normal sein
  • Ingress-qdisc und netdev_max_backlog

    • netdev_max_backlog ist eine kernelinterne Queue, in der Traffic nach dem Empfang durch die NIC und vor der Verarbeitung durch Protokoll-Stacks wie IP/TCP gespeichert wird
    • Pro CPU-Kern gibt es jeweils eine Backlog-Queue
    • Wenn ein Interface Pakete schneller empfängt, als der Kernel sie verarbeiten kann, füllt sich die INPUT-seitige Queue bis netdev_max_backlog; darüber hinausgehende Pakete werden verworfen
    • Der Standardwert ist 1000 und kann für mehrere 1-Gbps-Interfaces oder ein einzelnes 10-Gbps-Interface zu niedrig sein
    • Die 2. Spalte in /proc/net/softnet_stat ist ein Counter, der bei Backlog-Queue-Overflows ansteigt
    • Eine Änderung des Werts erfolgt mit sysctl -w net.core.netdev_max_backlog <value>
  • Egress-qdisc, txqueuelen, default_qdisc

    • txqueuelen legt die Anzahl der Pakete fest, die in der Kernel-Transmit-Queue eines Netzwerk-Interface-Geräts erlaubt sind
    • Der Standardwert kann je nach Interface-Treiber 1000 betragen
    • Ändern lässt sich der Wert mit ifconfig <interface> txqueuelen value; RX/TX dropped prüft man mit ip -s link
    • default_qdisc ist die standardmäßig für Netzwerkgeräte zu verwendende Queuing Discipline
    • Statt pfifo_fast können Alternativen wie sfq, codel oder fq_codel festgelegt werden
    • Mit tc -s qdisc ls dev <interface> prüft man Kennzahlen wie dropped, overlimits und requeues
  • TCP-Read/Write-Buffer und Verbindungs-Queues

    • tcp_rmem und tcp_wmem definieren jeweils Minimum, Standardwert und Maximum der TCP-Empfangs- bzw. TCP-Sendepuffer
    • Die Änderung erfolgt beispielsweise so
      • sysctl -w net.ipv4.tcp_rmem="min default max"
      • sysctl -w net.ipv4.tcp_wmem="min default max"
    • Mit /proc/net/sockstat prüft man den Zustand der Puffernutzung
    • Accept Queue und SYN Queue werden von net.core.somaxconn und net.ipv4.tcp_max_syn_backlog beeinflusst
    • net.core.somaxconn ist die Obergrenze für den backlog-Parameter von listen(); wenn dieser Wert geändert wird, muss auch die Anwendung auf einen kompatiblen Wert angepasst werden
    • net.ipv4.tcp_syncookies schaltet SYN Cookies ein oder aus, die zum Schutz vor SYN-Flood-Angriffen nützlich sind
    • net.ipv4.tcp_congestion_control legt den Congestion-Control-Algorithmus fest, der für neue Verbindungen verwendet wird

NUMA und Netzwerk-Performance

  • NUMA (Non-uniform memory access) ist eine Speicherarchitektur, bei der ein Prozessor schneller auf lokalen Speicher zugreifen kann als auf nicht lokalen Speicher
  • Da die CPU bei der Netzwerkverarbeitung auf Ringpuffer-Speicher zugreifen muss, kann NUMA Locality die Netzwerk-Performance beeinflussen
  • NUMA teilt CPU, Speicher und Geräte in mehrere Nodes auf und verhält sich wie mehrere kleine Computer mit schnellem Interconnect und gemeinsamem OS
  • In NUMA-Systemen besteht das Tuning-Ziel darin, die Interrupts eines Geräts auf die CPU-Kerne des einzelnen Nodes zu bündeln, zu dem dieses Gerät gehört
  • Allerdings können NUMA-Systeme ungünstig mit Echtzeitanwendungen interagieren und unerwartete Event-Latenzen verursachen
  • NUMA-Nodes prüft man mit /sys/devices/system/node/node*
  • Die Geräte-Locality prüft man mit /sys/class/net/<interface>/device/numa_node
    • -1 bedeutet, dass die Hardwareplattform kein echtes NUMA ist, der Kernel NUMA emuliert oder das Gerät keine NUMA Locality hat
  • Der Linux-Kernel unterstützt NUMA seit 2.5; RedHat- und Debian-basierte Distributionen stellen numactl und numad bereit
  • numad überwacht die System-Topologie und Ressourcennutzung und versucht, Prozesse mit ausreichend großem Speicher- und CPU-Bedarf an einer effizienten NUMA Locality zu platzieren

Optionen für schnellere Paketverarbeitung

  • AF_PACKET v4 und PACKET_MMAP

    • AF_PACKET v4 ist eine schnelle Paketschnittstelle unter Linux, die System Calls aus dem Datenpfad entfernt und standardmäßig den Copy-Mode verwendet
    • Mit PACKET_ZEROCOPY lässt sich ein echter Zero-Copy-Modus nutzen, bei dem DMA-Paketpuffer in den Userspace gemappt werden
    • Der übliche Pfad aus File-Read und anschließendem Socket-Send erfordert Context Switches zwischen User Mode und Kernel Mode sowie mehrere Datenkopien
    • Zero-Copy erhöht die Performance, indem redundante Datenkopien vermieden werden
    • PACKET_MMAP ist eine Linux-API für schnelles Packet Sniffing
    • Sie stellt einen mmapped Ring Buffer bereit, der von Userspace und Kernel gemeinsam genutzt wird
    • Sie reduziert System Calls sowie Kopien zwischen Userspace und Kernel bei gesendeten und empfangenen Paketen
  • DPDK

    • DPDK (Data Plane Development Kit) ist ein Framework aus Userspace-Bibliotheken und Treibern für schnelle Paketverarbeitung
    • Ziel ist es, Netzwerkpakete zwischen NIC und User-Anwendung mit nativer Geschwindigkeit zu senden und zu empfangen
    • Zielhardware sind 10Gb- oder 40Gb-NICs; Geschwindigkeit ist das wichtigste Kriterium
    • Der Fokus liegt nicht auf einem Netzwerk-Stack, sondern auf Packet Forwarding
    • Wenn DPDK die NIC kontrolliert, umgeht sämtlicher Traffic den Kernel, und die betreffende NIC ist für den Kernel nicht sichtbar
    • Ports werden vom Linux-Kernel-Treiber ungebunden und von Treibern wie vfio_pci, igb_uio oder uio_pci_generic verwaltet
    • Anschließend übernimmt der DPDK PMD die Kommunikation zwischen Anwendung und NIC
    • DPDK erfordert eine Hugepages-Konfiguration, um große Speicher-Chunks zu allozieren
    • Hauptkomponenten:
      • EAL: generisches Interface, das Unterschiede zwischen Umgebungen verbirgt
      • librte_ring: lockless Multi-Producer-, Multi-Consumer-FIFO-API
      • librte_mempool: Allokation von Pools für Speicherobjekte
      • librte_mbuf: Erzeugung und Manipulation von Buffern für Netzwerkpakete
      • librte_timer: Timer-Service für die asynchrone Ausführung von Funktionen
      • PMD: Treiber, bei dem die CPU die NIC kontinuierlich pollt, statt Interrupts zu nutzen
    • Einschränkungen:
      • Starke Hardwareabhängigkeit
      • Für die Ausführung eines PMD muss ein CPU-Core exklusiv zugewiesen werden; er nutzt 100% CPU
  • PF_RING

    • PF_RING ist ein Linux-Kernelmodul und Userspace-Framework, das Pakete mit hoher Geschwindigkeit verarbeitet und Paketverarbeitungsanwendungen eine konsistente API bietet
    • PF_RING pollt Pakete von der NIC über Linux NAPI
    • NAPI kopiert Pakete von der NIC in den PF_RING Circular Buffer, und die Userspace-Anwendung liest Pakete aus dem Ring
    • In dieser Struktur gibt es zwei Poller – die Anwendung und NAPI –, sodass CPU-Zyklen für Polling aufgewendet werden
    • Ein Vorteil ist, dass eingehende Pakete gleichzeitig auf mehrere Ringe verteilt werden können
    • Durch die modulare Struktur lassen sich zusätzliche Komponenten wie ZC Module, FPGA-based Card Module, Stack Module, Timeline Module und Sysdig Module verwenden
    • PF_RING hat die Kosten für Packet Capture und Userland Forwarding reduziert, ist in der Maximalleistung jedoch durch die Zwei-Akteur-Struktur begrenzt, bei der der Kernel von der NIC in den Ring kopiert und Userland aus dem Ring liest und verarbeitet
    • PF_RING enthält seit Version 7.5 Unterstützung für einen AF_XDP-Adapter
  • XDP und AF_XDP

    • XDP (eXpress Data Path) ist eine eBPF-Implementierung, die Pakete sehr früh im Linux-Netzwerkdatenpfad abfängt
    • XDP verarbeitet die RX-Packet-Page direkt innerhalb der RX-Funktion des Device-Treibers, noch bevor ein SKB allokiert wird
    • eBPF ist benutzerdefinierter, sandboxed Bytecode, der im Kernel ausgeführt wird
    • Es verwendet 11 64-Bit-Register und einen 512-Byte-Stack
    • Über das LLVM-Backend kann aus C, Lua, Go, P4, Rust und weiteren Sprachen nach eBPF kompiliert werden
    • Es bietet einen In-Kernel-Verifier und einen JIT-Compiler und unterstützt Funktionen wie Maps, Tail Calls und Helper
    • Einsatzbereiche von XDP:
      • Pre-Stack-Filtering zur DoS-Abmilderung
      • Forwarding und Load Balancing
      • Batching-Verfahren wie GRO
      • Flow Sampling und Monitoring
      • ULP Processing
    • XDP ist kein Kernel Bypass, sondern ein Fast Path innerhalb des Kernel-Stacks, und ersetzt den TCP/IP-Stack nicht
    • Es ist keine dedizierte Hardware erforderlich, allerdings gibt es Anforderungen wie Multi-Queue-NIC, TX/RX Checksum Offload, RSS und TSO
    • Vorteile von XDP gegenüber DPDK:
      • Wahl zwischen Busy Polling und interruptgesteuertem Networking
      • Keine Huge Pages erforderlich
      • Keine speziellen Hardwareanforderungen
      • Dedizierte CPU nicht zwingend erforderlich
      • Pakete müssen von 3rd-party Userspace Applications nicht erneut in den Kernel injiziert werden
      • Kein neues Security Model für den Zugriff auf Netzwerkhardware erforderlich
      • Kein 3rd-party Code/Licensing erforderlich
    • AF_XDP ist ein neuer Socket-Typ, der mit Linux 4.18 eingeführt wurde
    • Ohne den Kernel vollständig zu umgehen, kann es Kernel-Funktionen nutzen, um eine DPDK oder AF_PACKET ähnliche Struktur aufzubauen
    • Ein XDP-Programm kann Frames per eBPF in einen Userspace-Memory-Buffer umleiten
    • DMA-Transfers unterstützen Zero-Copy über Userspace-Speicher
    • Gegenüber AF_PACKET lässt sich eine Performance-Steigerung um das 3- bis 20-Fache erreichen
    • Einschränkungen:
      • Relativ junges Projekt
      • Für vollständige Unterstützung ist Linux-Kernel 5.4 oder neuer erforderlich

1 Kommentare

 
GN⁺ 2024-07-29
Hacker-News-Kommentare
  • Das wäre vor ein paar Wochen wirklich nützliches Material für mich gewesen.
    Ich wollte L2-Link-Verschlüsselung zwischen Rechenzentren umsetzen und habe mir Angebote für Hardware-Appliances von mehreren Anbietern eingeholt, fand die Kosten aber viel zu hoch und habe es deshalb selbst gebaut.
    Ich habe Ethernet-Frames über ein WireGuard-Overlay-Netzwerk auf Standardhardware transportiert und so 10 Gbit/s erreicht. Nach etwa zehn Tagen Arbeit war die Lösung rund 70 % günstiger als das billigste Angebot und etwa 95 % günstiger als das teuerste, aber dafür waren viel Detaildokumentation und viele Experimente nötig.
    Ich würde den Inhalt dieses Artikels gern nutzen, um zu prüfen, ob mein Verständnis korrekt ist; auf den ersten Blick wirkt er vielversprechend und umfassend.

    • Mich würde interessieren, für welchen Anwendungsfall ein L3-Tunnel nicht ausgereicht hat.
    • Wenn du den Code teilen kannst, würde ich ihn gern sehen. Mich interessiert sehr, wie du das umgesetzt hast.
  • Wenn es so viele einstellbare Werte gibt, wäre es dann nicht sinnvoll, Auto-Tuning-Software zu bauen?
    Ein Ansatz ähnlich dem Gradientenabstieg scheint möglich: zufällig Parameter aus einer Whitelist auswählen, sie innerhalb eines zulässigen Bereichs leicht erhöhen oder senken, die Performance eine Weile messen und dann zurückrollen, wenn sie schlechter wird, oder weiter anpassen, wenn sie besser wird.

  • Interessant, aber als Softwareentwickler habe ich fast nie die Gelegenheit, die im Artikel genannten Befehle tatsächlich auszuführen.
    Die Systeme laufen meist in irgendeinem abgespeckten Linux-Container, es gibt keinen Shell-Zugriff auf die Produktionssysteme, und Entwicklungs- oder QA-Umgebungen unterscheiden sich hinsichtlich Last und anderer Aspekte so stark von der Produktion, dass das Reproduzieren von Bugs meistens kaum hilft.
    Letztlich hat man nur dann Gelegenheit, die Befehle aus dem Artikel auszuprobieren, wenn man am eigenen System arbeitet; als Platform Engineer wäre das wahrscheinlich nützlicher.

    • Die meisten Low-Level-Features funktionieren wahrscheinlich ohnehin nicht oder bringen wenig. Implementierungen von Container Network Interfaces zwingen einen meist dazu, sich mit veth-Paaren herumzuschlagen, und erledigen im User Space allerlei seltsame Dinge.
      Einer der Punkte, die mir an Kubernetes weniger gefallen, ist das Networking-Modell. Es geht davon aus, dass es nur eine Netzwerkkarte gibt und dass Anwendungen so einfach sind, dass sie kein Wissen über die darunterliegenden Schichten brauchen.
      Das gesamte Networking-Modell scheint noch viel Raum für eine grundlegende Modernisierung und Verbesserung im Stil der 2020er zu haben.
    • Wenn es eine Staging-Umgebung gibt, die der Produktion möglichst ähnlich ist, kann es je nach Situation hilfreich sein, in einer produktionsnahen Umgebung mit Zugriffsmöglichkeiten zu experimentieren und Analysen durchzuführen.
  • Es heißt, net.core.wmem_max sei die Obergrenze für die Größe des TCP-Sendepuffers, und es gibt auch net.ipv4.tcp_wmem, daher frage ich mich zwei Dinge:

    1. Warum gibt es keinen entsprechenden Wert für IPv6? 2. Worin unterscheidet es sich von net.core.wmem_max?
    • net.core.wmem_max ist, wie der Name sagt, der Maximalwert.
      net.ipv4.tcp_wmem ist ein Dreifachwert mit Minimum, Standard und Maximum; der dort angegebene Maximalwert kann den zuvor genannten net.core.wmem_max nicht überschreiten.
      TCP sollte dasselbe Protokoll sein, unabhängig davon, ob es über IPv4 oder IPv6 transportiert wird.
      Beispiel: https://docs.redhat.com/en/documentation/red_hat_data_grid/7...
  • Was hier etwas fehlt, ist Debugging und Tuning für Durchsatz jenseits von 100 Gbit/s.
    Wenn man auf diesem Niveau HTTP ausliefert, ist der erste Flaschenhals oft die Speicherbandbreite, weshalb man häufig kTLS braucht.
    Tools wie AMD μProf sind fürs Debugging sehr nützlich, und eBPF-basiertes kontinuierliches Profiling hilft ebenfalls dabei zu verstehen, was genau im Kernel und im User Space passiert.

  • Sieht ziemlich cool aus. In meiner bisherigen Laufbahn habe ich, sobald Performance nötig war, meist direkt mit Kernel-Bypass angefangen.