Chiunque gestisca server Linux in produzione, prima o poi, si scontra con lo stesso muro: l’hardware non è il collo di bottiglia, lo sono i valori di default del kernel. Un server con dischi NVMe e una scheda di rete da 10 Gbps che fatica a superare poche centinaia di connessioni simultanee, o che soffre di latenza inspiegabile sotto carico, nella stragrande maggioranza dei casi non ha un problema di risorse: ha un problema di tuning.
Il kernel Linux espone centinaia di parametri regolabili a runtime tramite l’interfaccia sysctl, che agisce sull’albero /proc/sys/. Sono impostazioni pensate per andare bene “in media” su qualunque macchina, dal Raspberry Pi al server con 512 GB di RAM. Per un ambiente di produzione, però, “in media” spesso non basta. Vediamo quali parametri contano davvero, perché, e come applicarli senza rischiare di rompere il sistema.
Come funziona sysctl
Prima di toccare qualunque valore è utile ripassare i comandi base. sysctl permette di leggere e modificare i parametri kernel senza riavviare la macchina:
# Elencare tutti i parametri disponibili
sysctl -a
# Leggere un singolo parametro
sysctl net.ipv4.tcp_syncookies
# Modificarlo temporaneamente (non sopravvive al riavvio)
sudo sysctl -w net.ipv4.tcp_syncookies=1
Per rendere le modifiche permanenti, la pratica corretta è creare un file dedicato sotto /etc/sysctl.d/ — evitando di editare direttamente /etc/sysctl.conf, che su molte distribuzioni convive male con i pacchetti che installano le proprie regole:
sudo nano /etc/sysctl.d/99-tuning.conf
sudo sysctl --system # applica tutti i file in /etc/sysctl.d/
Rete: buffer TCP e gestione delle connessioni
Sui server con traffico sostenuto, i buffer TCP di default sono quasi sempre troppo piccoli. Il kernel alloca dinamicamente memoria per i socket entro i limiti impostati da questi tre valori (minimo, default, massimo, in byte):
net.ipv4.tcp_rmem = 4096 87380 67108864
net.ipv4.tcp_wmem = 4096 65536 67108864
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
Alzare il massimo a 64 MB ha senso soprattutto su link ad alta banda e alta latenza (data center distanti, CDN, replica di database geografica), dove il bandwidth-delay product richiede finestre TCP più ampie per saturare il collegamento.
Altrettanto importante è la gestione della coda di connessioni in ingresso, che su un server esposto a picchi di traffico determina quante richieste vengono accettate prima di iniziare a scartarle:
net.ipv4.tcp_max_syn_backlog = 8192
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 16384
net.ipv4.tcp_syncookies = 1
somaxconn in particolare va allineato al backlog configurato lato applicazione (nginx, HAProxy, il socket listener della tua app .NET o Node): se l’applicazione chiede una coda più grande di quella permessa dal kernel, il valore effettivo resta quello più basso.
Attivare BBR come algoritmo di congestion control
Il cambiamento con il rapporto costo/beneficio più alto per la maggior parte dei server moderni è probabilmente il passaggio da CUBIC a BBR come algoritmo di controllo della congestione. CUBIC reagisce alla perdita di pacchetti: aumenta la finestra di invio finché non rileva un drop, poi la dimezza. È un approccio reattivo che su reti con perdita “di fondo” non dovuta a congestione (Wi-Fi, mobile, certi link satellitari) penalizza inutilmente il throughput.
BBR, sviluppato da Google e stabile nei kernel a partire dalla serie 4.9, funziona in modo diverso: stima attivamente il bandwidth-delay product del percorso di rete e modula l’invio di conseguenza, senza aspettare la perdita di pacchetti come segnale. Sui kernel 5.15 e 6.x l’implementazione è ulteriormente raffinata (il set di funzionalità informalmente indicato come “BBRv3”), ma non serve aggiornare il kernel apposta: se hai già un kernel recente, BBR c’è già, va solo abilitato.
# Verifica gli algoritmi disponibili
sysctl net.ipv4.tcp_available_congestion_control
# Se necessario, carica il modulo
sudo modprobe tcp_bbr
echo tcp_bbr | sudo tee /etc/modules-load.d/tcp-bbr.conf
E nel file di configurazione:
net.ipv4.tcp_congestion_control = bbr
net.core.default_qdisc = fq
Il secondo parametro non è opzionale: BBR è stato progettato e testato insieme alla queue discipline fq (fair queue con pacing), e usarlo con la pfifo_fast di default riduce buona parte del beneficio. Dopo l’attivazione, verifica con:
sysctl net.ipv4.tcp_congestion_control
ss -tin | grep bbr
Memoria: swap, cache e writeback
Il secondo fronte dove i default fanno più danni è la gestione della memoria, in particolare su server con molta RAM dedicati a database o applicazioni in-memory.
vm.swappiness = 10
vm.vfs_cache_pressure = 50
swappiness (range 0-200, default 60) determina quanto aggressivamente il kernel sposta pagine di memoria su swap invece di liberare cache. Un valore basso come 10 dice al kernel di preferire fortemente la RAM fisica e di ricorrere allo swap solo quando davvero necessario — comportamento quasi sempre desiderabile su un server con database, dove uno swap-in inatteso su una query critica si traduce in latenza a due cifre percentuali più alta. vfs_cache_pressure più basso del default (100) fa sì che il kernel trattenga più a lungo la cache di metadati e dentry, utile su filesystem con molti file piccoli.
Altrettanto rilevante è il comportamento di scrittura delle pagine “sporche” (dirty), cioè modificate in memoria ma non ancora sincronizzate su disco:
vm.dirty_ratio = 10
vm.dirty_background_ratio = 5
vm.dirty_expire_centisecs = 1500
vm.dirty_writeback_centisecs = 250
Con i default (rispettivamente 20 e 10, spesso più alti su alcune distribuzioni), un server con molta RAM può accumulare diversi gigabyte di dati non ancora scritti su disco prima che il kernel forzi il flush — e quando lo fa, l’intero sistema può bloccarsi per secondi mentre scrive tutto in una volta. Abbassare le soglie distribuisce il lavoro di scrittura in modo più uniforme, a costo di un overhead I/O leggermente maggiore ma costante. Su macchine con centinaia di GB di RAM, conviene passare dai valori percentuali a limiti assoluti in byte (vm.dirty_bytes, vm.dirty_background_bytes), perché una percentuale del 10% su 512 GB di RAM è comunque troppo.
File descriptor e limiti di sistema
Un errore classico su server che gestiscono molte connessioni concorrenti (web server, message broker, database con molti client) è l’esaurimento dei file descriptor disponibili:
fs.file-max = 2097152
fs.nr_open = 1048576
fs.inotify.max_user_watches = 524288
Quest’ultimo parametro merita attenzione particolare: inotify.max_user_watches troppo basso è la causa più comune dell’errore “too many open files” riportato da strumenti come Webpack dev server, editor con file watching, o sistemi di sincronizzazione che monitorano grandi alberi di directory — non ha nulla a che fare con i socket di rete, ma con il numero di file che il kernel può tenere sotto osservazione per notifiche di modifica.
Sicurezza di rete
Un piccolo set di parametri riduce la superficie d’attacco a livello di stack di rete senza impatto sulle prestazioni:
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.log_martians = 1
rp_filter (reverse path filtering) merita una nota: impostato a 1 (strict mode) rifiuta pacchetti il cui indirizzo sorgente non è raggiungibile tramite l’interfaccia su cui sono arrivati, mitigando IP spoofing. È la scelta giusta per la maggior parte dei server, ma su macchine con routing asimmetrico — tipicamente setup multihomed, alcune configurazioni VPN o BGP — va impostato a 2 (loose mode), altrimenti si rischia di scartare traffico legittimo.
Un parametro storico da NON usare
Vale la pena segnalarlo esplicitamente perché gira ancora in vecchie guide copiate e incollate da un blog all’altro: net.ipv4.tcp_tw_recycle è stato rimosso dal kernel a partire dalla versione 4.12 perché causava problemi seri dietro NAT (connessioni rifiutate in modo intermittente e difficile da diagnosticare). Se lo trovate in un file sysctl.conf ereditato da un vecchio sistema, va rimosso: su kernel recenti l’impostazione viene semplicemente ignorata, ma la sua presenza è un segnale che quella configurazione non è stata rivista da anni.
Applicare e verificare
Mettendo insieme i parametri discussi in un unico file di configurazione:
# /etc/sysctl.d/99-tuning.conf
## Rete: buffer TCP
net.ipv4.tcp_rmem = 4096 87380 67108864
net.ipv4.tcp_wmem = 4096 65536 67108864
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
## Rete: congestion control
net.ipv4.tcp_congestion_control = bbr
net.core.default_qdisc = fq
## Rete: gestione connessioni
net.ipv4.tcp_max_syn_backlog = 8192
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 16384
net.ipv4.tcp_syncookies = 1
## Memoria
vm.swappiness = 10
vm.vfs_cache_pressure = 50
vm.dirty_ratio = 10
vm.dirty_background_ratio = 5
## File descriptor
fs.file-max = 2097152
fs.inotify.max_user_watches = 524288
## Sicurezza
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.log_martians = 1
Dopo l’applicazione con sudo sysctl --system, alcuni controlli utili per confermare che tutto sia entrato in vigore:
sysctl net.ipv4.tcp_congestion_control
cat /proc/sys/fs/file-nr
cat /proc/meminfo | grep -i dirty
sudo dmesg | tail -20
Conclusione
Nessuno di questi parametri va applicato alla cieca copiando un file da internet, incluso questo. Ogni ambiente ha un profilo di carico diverso — un database transazionale, un reverse proxy ad alto traffico e un batch processor hanno esigenze di memoria e rete molto differenti — e il modo corretto di procedere è cambiare pochi parametri alla volta, misurare, e tenere traccia di cosa è stato modificato e perché, magari versionando il file sysctl.d insieme al resto della configurazione infrastrutturale. Il vantaggio di sysctl è proprio questo: ogni modifica è reversibile a runtime, il che rende il tuning un processo iterativo e sicuro invece che un salto nel buio a ogni riavvio.
Fonte originale: Linux Kernel Parameters Tuning for Better Performance, LinuxBlog.io.