...

Affinità IRQ su Linux nei sistemi multiprocessore: guida pratica per un’ottimizzazione ottimale della rete

Mostrerò in modo pratico come l’affinità IRQ, sui sistemi multiprocessore, assegni in modo mirato gli interrupt di rete ai core della CPU, riducendo la latenza e aumentando la velocità di trasmissione. Attraverso passaggi chiari, esempi e una tabella, illustrerò come selezionare le maschere esadecimali, tenere conto della struttura NUMA e assegnare i processi in modo appropriato.

Punti centrali

  • Affinità IRQ indirizza in modo mirato gli interrupt hardware alle CPU e riduce l'overhead.
  • Affinità della CPU Fissare i servizi agli stessi core mantiene le cache locali.
  • NUMA Si noti che l’adattatore e i core utilizzano lo stesso nodo.
  • irqbalance valutare: distribuzione automatica o regolazione manuale di precisione.
  • Monitoraggio e l'adattamento iterativo garantisce latenze stabili.

Comprendere l'affinità IRQ: nozioni di base ed effetti

Sugli host Linux, i pacchetti in entrata, l’I/O su disco e i timer IRQ che il kernel distribuisce tra i core della CPU. Tramite i file /proc/irq//smp_affinity e .../smp_affinity_list, quali CPU possono accedere a una sorgente. Una maschera standard ampia sembra inizialmente flessibile, ma in caso di carico genera cache miss, costosi cambi di contesto e softIRQ dispersi su molti CPU. Assegno le code critiche delle singole schede di rete a core definiti, alleggerisco il carico sugli hotspot e mantengo brevi i percorsi dei dati. Questa gestione migliora sensibilmente la stabilità non appena sono attivi contemporaneamente molti flussi.

Ottimizzazione dell’IRQ e dell’affinità della CPU: mantenere i dati in locale

Sono riuscito a collegare il IRQ-Gestione delle code NIC in base all'appartenenza alla CPU dei worker interessati. A tal fine, assegno i thread del server web o del proxy tramite set di compiti oppure CPUAffinity= in systemd su quei core che gestiscono anche gli interrupt RX/TX. In questo modo le cache line rimangono locali e riduco al minimo la comunicazione tra CPU, il che Latenza uniforma. Proprio i backend API, i servizi in tempo reale e gli stack virtualizzati traggono vantaggio da questa coerenza. Testo l’integrazione sotto carico di produzione finché i flussi, gli SoftIRQ e lo spazio utente non si integrano perfettamente tra loro.

Modalità automatica contro regolazione fine: come interpretare correttamente irqbalance

Il servizio irqbalance distribuisce automaticamente gli interrupt tra i core disponibili, il che funziona bene sui server generici. Tuttavia, in configurazioni di rete sottoposte a carico elevato, questa distribuzione riduce la località della cache e rende più difficile il pinning mirato. Limito l’irqbalance o lo disattivo in modo selettivo quando determinate code richiedono core fissi. Per una comprensione di base e per trovare i profili adatti, trovo utile questa guida su Configurare irqbalance. Di conseguenza, il sistema automatico gestisce gli IRQ non critici, mentre io assegno manualmente quelli sensibili.

Passaggio: rendere visibili gli IRQ rilevanti

Comincio con uno sguardo a /proc/interruzioni e filtra in base al nome del dispositivo, ad esempio ens192, eno1 oppure eth0. Gli adattatori moderni creano diverse code RX e TX, pertanto individuo un gruppo di numeri IRQ assegnati alla stessa scheda di rete. Presto attenzione ai valori dei contatori per individuare rapidamente i punti critici e assegnare per primi le code sottoposte a carico elevato. Controllo regolarmente questa panoramica durante i test di carico, in modo che l’assegnazione sia sostenibile nel tempo. Inoltre, verifico le denominazioni dei driver, poiché forniscono indicazioni sulle funzionalità RSS e sull’offloading.

# Visualizzare tutti gli interrupt
cat /proc/interrupts

# Visualizzare solo le righe relative alla scheda di rete (esempio: ens192)
grep -i ens192 /proc/interrupts

Utilizzare in modo intelligente la topologia NUMA

Sugli host con più nodi, preferisco trasferire gli IRQ sui core di quelli NUMA-Nodo a cui è fisicamente collegata la scheda di rete. Lo verifico con lscpu e numactl --hardware e contrassegno i set di CPU adeguati per la successiva creazione delle maschere. Anche i processi che utilizzano questi percorsi di rete li associo allo stesso nodo e, tramite le politiche di memoria, garantisco che Memoria-Assegnazioni. In questo modo evito costosi accessi remoti attraverso i collegamenti QPI/UPI. Questa disciplina porta rapidamente a vantaggi misurabili nei test di latenza.

Scegliere le maschere di bit in modo sicuro: la logica esadecimale in sintesi

Il file smp_affinity accetta maschere di bit esadecimali che corrispondono direttamente agli ID dei core e coprono anche sistemi di grandi dimensioni. Spesso inizio con modelli semplici: CPU0 è 0x1, CPU1 è 0x2, CPU2 è 0x4, CPU3 è 0x8 ecc., mentre 0xF comprende i core da 0 a 3. Su macchine con molti core scrivo più blocchi da 32 bit, separati da virgole, in modo che il Maschera riporta correttamente tutti gli ID. Questa panoramica mi aiuta a effettuare assegnazioni senza errori ed evitare spostamenti involontari. Utilizzo spesso la tabella seguente come promemoria.

Numero della CPU Bit (binario) Maschera Hex Suggerimento
0 …0001 0x1 CPU0 spesso alleggerire il carico e utilizzarle con parsimonia.
1 …0010 0x2 IRQ su CPU1 pinnen.
2 …0100 0x4 Collegare l'IRQ alla CPU2.
3 …1000 0x8 Collegare l'IRQ alla CPU3.
0–3 …1111 0xF Se si attivano tutti e quattro i core, la latenza spesso aumenta leggermente.
0–7 11111111 0xFF Distribuzione ampia, la località della cache ne risente.

Impostare l'affinità IRQ: come associare le code ai core

Dopo aver identificato gli IRQ in coda della scheda di rete, li distribuisco su canali dedicati Nuclei come 1, 2 e 3, per scalare correttamente l’elaborazione parallela. In questo modo ogni coda RX/TX rimane associata al proprio core, evitando il cross-talk e garantendo la coerenza del funzionamento degli SoftIRQ. Durante la messa a punto, confronto il throughput e la latenza finché la distribuzione non risulta affidabile. Per un approfondimento mi è d’aiuto un breve Guida pratica con varianti a seconda dell’adattatore. Eseguo i comandi appositamente durante le finestre di manutenzione e li metto in sicurezza tramite uno script di avvio.

Esempio #: associare tre IRQ di coda alle CPU 1-3
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)

Process pinning: assegnare i servizi agli stessi core

Assegno i worker dell'applicazione a quelli CPU, che gestiscono gli IRQ corrispondenti, in modo che i percorsi dei dati rimangano brevi. Con systemd utilizzo CPUAffinity=1 2 3 oppure avvia una sola volta con taskset -c 1-3. Per i server multi-worker, assegno un numero fisso di core a ciascun gruppo di worker, in modo da evitare la concorrenza. Questo abbinamento garantisce valori costantemente più bassi Latenze, perché le cache della CPU rimangono adeguatamente piene. Dopo aver apportato delle modifiche, controllo i thread, i socket e gli SoftIRQ con htop, ss e perf.

Ottimizzazione della rete: integrare RSS, RPS/RFS e i parametri del kernel

Molte schede di rete distribuiscono i pacchetti tramite RSS sulle code, che poi associo ai core tramite l’affinità IRQ. Riassumo i dettagli e i principi di funzionamento nella sezione Receive Side Scaling in modo compatto. Inoltre, gestisco RPS/RFS in modo che gli SoftIRQ non interferiscano con la logica di pinning rigida. Parallelamente, regolo i buffer tramite net.core.rmem_max e net.core.wmem_max e controlla le opzioni TCP come tcp_timestamps. Questi elementi contribuiscono tutti allo stesso obiettivo: bassa latenza con elevata Velocità di trasmissione.

Interpretare correttamente MSI-X e la struttura delle code

Le schede di rete moderne utilizzano MSI-X e creano IRQ dedicati per ogni coda RX/TX. Per prima cosa verifico quante code e quanti canali siano attualmente attivati dal driver e li adeguo al budget di core del nodo NUMA corrispondente. In questo modo evito che un numero eccessivo di code si concentri su un numero insufficiente di core o, al contrario, che la capacità rimanga inutilizzata.

Verificare e regolare il numero di code e canali #
ethtool -l ens192 # limiti attuali (RX/TX/combinato)
ethtool -L ens192 combined 4  #, ad esempio, attivare 4 code

# Verificare l'indirezione RSS e l'impostazione dell'hash
ethtool -x ens192 # Visualizza la tabella di indirezione e la chiave hash

Molti driver assegnano nomi descrittivi agli IRQ (ad es. ens192-TxRx-0). Mantengo la tabella di indirezione coerente con l'assegnazione del core, in modo che i flussi vengano indirizzati in modo stabile verso la „loro“ coda. Se la distribuzione hardware differisce, si verificano spostamenti inutili degli SoftIRQ.

Utilizzare in modo coerente RPS/RFS e XPS

RPS/RFS può distribuire i pacchetti a livello software tra i core: una soluzione utile per le schede di rete che non dispongono di molte code, ma controproducente se ho già impostato un’associazione precisa tramite RSS e IRQ-Affinity. Pertanto, scelgo consapevolmente: o un’associazione rigida tramite RSS+IRQ-Affinity e RPS disattivato, oppure poche code hardware e RPS in modo mirato. Inoltre, configuro XPS per la direzione TX, in modo che i pacchetti in uscita vengano inviati dai core „giusti“.

IF=ens192

# Disattivare completamente l'RPS (in caso di pinning IRQ corretto tramite 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

# In alternativa: attivare RPS in modo selettivo (esempio: CPU 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
# Meglio: utilizzare una sintassi simile a quella di smp_affinity_list:
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

Impostare # XPS in base alle CPU worker selezionate (percorsi TX)
for q in /sys/class/net/$IF/queues/tx-*; do echo 1-3 > "$q"/xps_cpus; done

L'importante è la coerenza: il core RX-IRQ, il carico di ksoftirqd e il thread di lavoro dovrebbero trovarsi sullo stesso core (o coppia di core). In questo modo si eliminano molti effetti di "cross-core bounce".

Tenere conto di SMT/Hyper-Threading e coppie di core

Sui sistemi con SMT, spesso è opportuno riservare un core fisico per un RX-IRQ e assegnare il worker corrispondente al Thread correlato da impostare – oppure da separare intenzionalmente, se il carico di lavoro è intensivo dal punto di vista computazionale. Individuo le coppie di thread in base alla topologia e poi prendo una decisione chiara, anziché ricorrere a una distribuzione casuale.

Individuare le coppie di thread affini #
for c in /sys/devices/system/cpu/cpu*/topology/thread_siblings_list; do
  echo "$(basename "$(dirname "$c")") : $(cat "$c")"
done

Se distribuisco RX-IRQ e i worker dello spazio utente sullo stesso core fisico (diversi thread SMT), si ottiene una buona località L1/L2, ma con carichi di lavoro CPU-bound possono verificarsi colli di bottiglia. In alternativa, distribuisco l’IRQ sul core X e il worker sul core Y dello stesso nodo NUMA, per ottenere un parallelismo effettivo. Esegui un test A/B su entrambe le varianti e scelgo quella che offre la latenza più stabile.

Utilizzo di smp_affinity_list, effective_affinity e Defaults

Oltre alle maschere esadecimali, mi piace scrivere in smp_affinity_list, poiché ciò consente di affrontare ambiti quali 1-3,6,8-9 si possa sistemare comodamente. Per controllare, verifico affinità_effettiva rispettivamente elenco_di_affinità_effettivo, poiché il kernel o i driver possono escludere determinate CPU (ad esempio, core offline o „interrupt gestiti“).

# Assegnazione leggibile dall'utente
echo 1-3 > /proc/irq/181/smp_affinity_list

# Verifica dell'affinità effettiva
cat /proc/irq/181/effective_affinity_list

Per evitare che gli IRQ nuovi o aggiunti in seguito al ricaricamento di un driver vengano nuovamente distribuiti in modo dispersivo, se necessario imposto /proc/irq/default_smp_affinity a un valore di base ragionevole (ad esempio, tutti i core del nodo NUMA in questione, ma esclusa la CPU0). Successivamente, sovrascrivo in modo mirato i singoli IRQ critici.

Garantire la persistenza dopo i riavvii e i ricaricamenti

Le impostazioni di Affinity sono temporanee. Le salvo tramite un'unità systemd one-shot, che viene eseguita dopo l'obiettivo di inizializzazione della rete, oppure tramite un piccolo script che determina e mappa dinamicamente gli elenchi IRQ. In questo modo le assegnazioni vengono mantenute anche dopo gli aggiornamenti del kernel e i reset dei collegamenti.

# /usr/local/sbin/net-irq-pin.sh (esempio)
#!/bin/bash
set -euo pipefail
IF=${1:-ens192}
CPUS="1-3"   CPU di destinazione # (selezionare in modo coerente con NUMA)
for irq in $(grep -i "$IF" /proc/interrupts | awk '{print $1}' | tr -d ':'); do
  echo "$CPUS" > /proc/irq/$irq/smp_affinity_list || true
done

Unità systemd # (schema)
# /etc/systemd/system/net-irq-pin.service
[Unit]
Description=Fissare gli IRQ della scheda di rete
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/net-irq-pin.sh ens192

[Install]
WantedBy=multi-user.target

Importante: rieseguo lo script se il driver viene ricaricato o se il numero di code viene modificato, poiché in tal caso i numeri IRQ cambiano.

Virtualizzazione e container: considerare insieme host e guest

Negli ambienti KVM, monto sul Ospite gli IRQ fisici delle schede di rete ai core del nodo NUMA corrispondente. Parallelmente, collego vhost-rete‑Anche i thread e il processo QEMU (o le singole vCPU) vanno collocati lì, in modo che i percorsi dei dati rimangano brevi dal lato host. Nel Ospite Assegno l’affinità IRQ delle vNIC alle vCPU che ho associato ai core fisici sul lato host. I carichi di lavoro dei container (cgroups/cpuset) traggono vantaggio quando le CPU consentite ai container si sovrappongono ai core RX/TX dell’host; in caso contrario, si verificano accessi remoti evitabili.

Approfondimento dell'analisi: SoftIRQ, NAPI e rilevamento degli ingorghi

Inoltre /proc/interruzioni guardo in /proc/softirqs, per vedere se c'è molto lavoro da fare nel contesto di ksoftirqd anziché essere eseguito direttamente nell’handler dell’IRQ – un’indicazione di un carico elevato e prolungato. Con napi_defer_hard_irqs (A seconda del kernel) e tramite assegnazioni corrette delle code regolo l'intensità con cui NAPI raggruppa i batch. ethtool -S mi fornisce statistiche per ogni coda relative a drop, stati di occupato e velocità di trasmissione dei pacchetti; in questo modo riesco a individuare le code sbilanciate e ad adeguare di conseguenza l’affinità o l’indirezione RSS.

# Panoramica rapida sulla distribuzione degli SoftIRQ
cat /proc/softirqs | egrep 'NET_RX|NET_TX'

# Visualizzazione delle statistiche relative al driver e alla coda
ethtool -S ens192 | egrep -i 'rx|tx|drop|busy'

Esempio pratico: mappatura di 4 code su un nodo

Una configurazione tipica che utilizzo spesso: scheda di rete (NIC) sul nodo NUMA 0 con 4 code RSS. Evito la CPU0 e associo le code alle CPU da 1 a 4. Assegno anche i relativi worker web o proxy alle CPU da 1 a 4, assegno gli XPS in modo identico e lascio disattivato l’RPS. In questo modo ottengo percorsi brevi e coerenti in entrambe le direzioni.

IF=ens192
QUEUES=(181 182 183 184)   IRQ di esempio per # (da determinare in precedenza)
CPUS="1-4"

Impostare l'affinità IRQ e XPS per #
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

# Fissare il worker (systemd o taskset)
# systemd: CPUAffinity=1 2 3 4
# Una tantum: taskset -c 1-4

Se il carico continua ad aumentare, aumenterò il numero di code (ethtool -L) fino al numero ragionevole di core del nodo e distribuiscili in modo rigido secondo uno schema riconoscibile (ad es. ID coda → ID core), in modo che i flussi non „migrino“ nel corso del tempo.

Migliori pratiche per host multi-core

Alleggerisco il carico CPU0, poiché lì spesso sono in esecuzione timer e servizi interni del kernel che causano interferenze sotto carico. Preferisco quindi assegnare gli IRQ critici ad altri core e lasciare che la CPU0 gestisca solo poche sorgenti non critiche. Sui sistemi NUMA mantengo una politica coerente e tengo adattatori, IRQ, processi e accessi alla memoria sullo stesso Nodo. In ambienti sottoposti a forte carico, separo i core I/O dai core delle applicazioni e, se necessario, li isolo. Accompagno tutte le modifiche con misurazioni continue e adeguo le assegnazioni in modo iterativo.

Adottare un approccio misurabile: analisi, script e piano di riserva

Prima di apportare modifiche, documento lo stato attuale con mpstat, htop, /proc/interruzioni e misurazioni della latenza tramite iperf3. Configuro degli script che applicano automaticamente le impostazioni di affinità dopo un riavvio o il ricaricamento dei driver. Per i rollback tengo a disposizione delle maschere neutre, in modo da poter tornare immediatamente alla configurazione precedente in caso di malfunzionamenti. Nell’ambiente di staging testo profili di carico il più possibile simili a quelli della mia produzione e ripeto il Misurazione dopo ogni modifica. Solo allora attivo il profilo in modo permanente sull'host di destinazione.

Evitare gli ostacoli più comuni in modo pulito

Le mascherine troppo larghe distribuiscono il lavoro su troppe persone CPU e rallentano le cache, mentre le maschere troppo restrittive intasano le code. Le peculiarità NUMA trascurate generano accessi alla memoria remota che causano fluttuazioni nei tempi di risposta. Un pinning rigido a volte entra in conflitto con le impostazioni RPS/RFS, pertanto verifico esplicitamente la distribuzione del carico SoftIRQ. Dopo gli aggiornamenti del kernel o le sostituzioni dei driver, convalido nuovamente tutti i numeri IRQ, poiché le assegnazioni possono cambiare. Con passaggi cauti e una chiara Documentario rimango in grado di agire.

Riassumendo brevemente

Il "pinning" mirato degli IRQ associa gli interrupt di rete a un numero limitato di IRQ compatibili Nuclei, riduce l'overhead e stabilizza i tempi di risposta. A tal fine, ottimizzo l'affinità IRQ e CPU, tengo conto della NUMA e verifico l'efficacia tramite serie di misurazioni. Laddove è sufficiente la modalità automatica, lascio agire irqbalance, mentre assegno le code critiche a core fissi. Con RSS, RPS/RFS e parametri del kernel ottimizzati, l’ottimizzazione dispiega appieno il suo Effetto. Chi segue questi passaggi con rigore otterrà prestazioni di rete notevolmente superiori sui server Linux multi-core.

Articoli attuali