Salta ai contenuti

eBPF per gli SRE: Guida al Profiling in Produzione

· 13 min read · default
ebpfperformanceprofilingsrelinuxobservabilitymonitoring-&-observability

Introduzione

Il profiling in produzione è sempre stato il dominio degli audaci. Per anni, gli SRE hanno affrontato un compromesso scomodo: collegare strumenti di profiling pesanti che introducevano latenza misurabile e rischiavano di destabilizzare i carichi di lavoro in produzione, oppure volare alla cieca affidandosi a dashboard di metriche che mostravano i sintomi ma mai le cause radice. L'emergere di eBPF come tecnologia mainstream del kernel Linux ha cambiato fondamentalmente questo calcolo. Con eBPF, puoi instrumentare quasi ogni livello del kernel e dello spazio utente con un overhead trascurabile, generando il tipo di dati di osservabilità profonda che in precedenza era disponibile solo in ambienti di staging.

eBPF, che sta per extended Berkeley Packet Filter, si è evoluto dalle sue origini come meccanismo di filtraggio dei pacchetti di rete fino a diventare una macchina virtuale general-purpose all'interno del kernel. I programmi scritti per eBPF vengono verificati dal kernel prima dell'esecuzione, garantendo che non possano causare il crash del sistema, entrare in loop infiniti o accedere a memoria non autorizzata. Questa garanzia di sicurezza è ciò che rende eBPF unicamente adatto al profiling in produzione. Puoi collegare sonde a funzioni del kernel, tracepoint, entry di funzioni dello spazio utente e contatori di performance hardware senza ricompilare il kernel o riavviare i servizi.

Questa guida copre i workflow pratici che i team SRE usano quotidianamente per diagnosticare problemi di performance nei sistemi Linux in produzione. Percorreremo il profiling CPU, l'analisi della latenza, il rilevamento di memory leak, il tracciamento di rete e il profiling I/O usando la toolchain standard eBPF. Ogni comando e script mostrato qui è stato usato in ambienti di produzione con kernel 5.15 e successivi.

Perché eBPF Cambia il Profiling in Produzione

Gli strumenti di profiling tradizionali impongono costi che li rendono impraticabili in produzione. Eseguire strace su un servizio occupato può rallentarlo di un fattore di 100x o più perché strace usa ptrace per intercettare ogni syscall, alternando il contesto tra il processo tracciato e il processo tracciante per ogni invocazione. Anche perf, che usa contatori di performance hardware ed è molto più leggero di strace, richiede comunque la scrittura di dati di campionamento su disco e può generare una pressione I/O sostanziale sotto alti tassi di campionamento.

eBPF cambia l'equazione in tre modi importanti. Primo, i programmi eBPF vengono eseguiti all'interno del kernel, eliminando l'overhead di context-switch degli strumenti basati su ptrace. Quando colleghi un kprobe o tracepoint, il programma eBPF si esegue inline con il percorso di codice del kernel, aggiungendo tipicamente solo decine di nanosecondi per invocazione. Secondo, i programmi eBPF possono aggregare dati nel kernel usando le map, il che significa che puoi calcolare istogrammi, conteggi e riepiloghi senza copiare ogni evento nello spazio utente. Un istogramma di latenza che genererebbe milioni di eventi al secondo con strace produce una singola lettura di map per intervallo con eBPF. Terzo, il verificatore eBPF garantisce la sicurezza: il tuo programma non può dereferenziare puntatori nulli, accedere a memoria fuori dai limiti o eseguire loop infiniti.

L'impatto pratico è drammatico. Strumenti come biolatency possono tracciare ogni richiesta I/O di blocco su un sistema che gestisce centinaia di migliaia di IOPS, producendo un istogramma di latenza con meno dell'1% di overhead CPU. Puoi eseguire funclatency su una funzione calda nel tuo server applicativo mentre servi traffico di picco. Questo semplicemente non era possibile con le generazioni precedenti di strumenti di tracciamento.

Configurazione della Toolchain eBPF

L'ecosistema eBPF si è consolidato attorno a tre set di strumenti principali: bcc-tools, bpftrace e programmi CO-RE basati su libbpf. Ognuno serve un caso d'uso diverso, e una workstation SRE ben equipaggiata dovrebbe avere tutti e tre disponibili.

Su sistemi Ubuntu e Debian, installa la toolchain completa:

sudo apt-get update
sudo apt-get install -y bpfcc-tools bpftrace linux-headers-$(uname -r)
sudo apt-get install -y libbpf-dev bpftool

Su sistemi RHEL e Fedora:

sudo dnf install -y bcc-tools bpftrace kernel-devel-$(uname -r)
sudo dnf install -y libbpf-devel bpftool

Verifica che il tuo kernel supporti BTF, necessario per le funzionalità moderne di bpftrace e i programmi CO-RE:

ls /sys/kernel/btf/vmlinux
bpftool btf dump file /sys/kernel/btf/vmlinux format raw | head -c 100

Se BTF non è disponibile, dovrai assicurarti che il tuo kernel sia stato compilato con CONFIG_DEBUG_INFO_BTF=y. La maggior parte dei kernel delle distribuzioni dal 2022 in poi include il supporto BTF.

Il pacchetto bcc-tools fornisce dozzine di strumenti pronti all'uso che coprono gli scenari di profiling più comuni. Questi sono tipicamente installati come eseguibili con un suffisso -bpfcc sui sistemi basati su Debian o direttamente sotto /usr/share/bcc/tools/ su RHEL. Bpftrace fornisce un linguaggio di scripting di alto livello per scrivere one-liner e script brevi personalizzati. Libbpf e i programmi CO-RE (Compile Once, Run Everywhere) sono usati per costruire strumenti eBPF portabili di livello produzione che vengono distribuiti come binari autonomi.

Workflow di Profiling CPU

Il profiling CPU è il punto di partenza più comune quando si investigano problemi di performance. L'obiettivo è identificare quali funzioni consumano più tempo CPU, sia nello spazio kernel che utente, e generare flame graph che rendano gli hotspot visivamente ovvi.

L'approccio più semplice usa lo strumento profile di bcc per campionare le stack trace a una frequenza fissa:

sudo profile-bpfcc -F 99 -a --stack-storage-size 16384 30 > /tmp/cpu-stacks.txt

Questo campiona tutte le CPU a 99 Hz per 30 secondi. La frequenza di 99 Hz evita l'aliasing con attività basate su timer che spesso girano a 100 Hz. L'output contiene stack trace ripiegate che possono essere alimentate direttamente negli strumenti FlameGraph di Brendan Gregg:

git clone https://github.com/brendangregg/FlameGraph.git
cat /tmp/cpu-stacks.txt | FlameGraph/stackcollapse-bpf.pl | FlameGraph/flamegraph.pl > cpu-flame.svg

Per un profiling più mirato, bpftrace ti permette di profilare un processo specifico e filtrare per CPU:

sudo bpftrace -e 'profile:hz:99 /pid == 12345/ { @[ustack(perf), comm] = count(); }' > stacks.bt

Quando hai bisogno di capire il comportamento di scheduling CPU piuttosto che il tempo di esecuzione, lo strumento cpudist mostra per quanto tempo i thread girano sulla CPU prima di essere de-schedulati:

sudo cpudist-bpfcc -p 12345 10 1

Questo stampa un istogramma delle durate on-CPU per il processo 12345 su un intervallo di 10 secondi. Tempi brevi on-CPU combinati con alti context switch suggeriscono contesa di lock. Tempi lunghi on-CPU con basso throughput suggeriscono colli di bottiglia computazionali.

Per investigare problemi di migrazione CPU nei sistemi NUMA, puoi tracciare eventi dello scheduler:

sudo bpftrace -e 'tracepoint:sched:sched_migrate_task {
    printf("pid=%d comm=%s from_cpu=%d to_cpu=%d\n",
        args->pid, args->comm, args->orig_cpu, args->dest_cpu);
}'

Analisi della Latenza

L'analisi della latenza è dove eBPF brilla veramente perché può misurare il tempo tra eventi arbitrari senza perturbare il percorso di codice misurato. La collezione bcc-tools include diversi strumenti di latenza costruiti appositamente.

La latenza I/O di blocco viene misurata con biolatency, che traccia il tempo dalla richiesta I/O di blocco al completamento:

sudo biolatency-bpfcc -D 10 1

Il flag -D suddivide la latenza per dispositivo disco, rendendo facile identificare quali unità sono lente. L'output è un istogramma a potenze di due che mostra la distribuzione delle latenze in microsecondi.

La latenza della coda di esecuzione, che misura quanto tempo i thread aspettano nella coda dello scheduler prima di ottenere tempo CPU, viene misurata con runqlat:

sudo runqlat-bpfcc -p 12345 10 1

Un'alta latenza della coda di esecuzione significa che i tuoi processi stanno aspettando la CPU, il che indica saturazione CPU. Se vedi latenze superiori a 10 millisecondi durante l'operazione normale, hai bisogno di più capacità CPU o devi investigare cosa sta consumando CPU.

La latenza di funzione misura il tempo di esecuzione di una specifica funzione del kernel o dello spazio utente:

sudo funclatency-bpfcc -p 12345 'c:malloc' 10 1

Questo traccia le chiamate malloc nella libc del processo 12345 e mostra un istogramma di latenza. Puoi tracciare qualsiasi funzione che abbia un simbolo nel binario o nella libreria condivisa. Per le funzioni del kernel:

sudo funclatency-bpfcc 'vfs_read' 10 1

Per il tracciamento della latenza a livello applicativo con bpftrace, puoi misurare il tempo tra due punti di sonda:

sudo bpftrace -e '
uprobe:/usr/bin/myapp:process_request { @start[tid] = nsecs; }
uretprobe:/usr/bin/myapp:process_request /@start[tid]/ {
    @latency_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}
END { clear(@start); }
'

Analisi della Memoria

I problemi di memoria in produzione vanno da leak graduali che causano OOM kill nel corso dei giorni a inefficienze della cache che degradano la performance. eBPF fornisce diversi strumenti per ogni categoria.

Lo strumento memleak traccia le chiamate di allocazione e rilascio di memoria, monitorando le allocazioni in sospeso per identificare i leak:

sudo memleak-bpfcc -p 12345 --combined-only 30

Questo si collega al processo 12345 e dopo 30 secondi stampa le stack trace delle allocazioni non rilasciate, ordinate per il totale dei byte in sospeso. Questo è inestimabile per catturare leak nei servizi a lunga esecuzione senza riavviarli con il debugging specializzato dell'allocatore abilitato.

Per il tracciamento dell'allocazione di memoria del kernel:

sudo memleak-bpfcc --combined-only 30

Senza il flag pid, memleak traccia le allocazioni del kernel tramite kmalloc e kfree, il che può identificare memory leak del kernel causati da driver o moduli del kernel.

Il comportamento della cache ha un impatto enorme sulla performance dell'applicazione. Lo strumento cachestat fornisce statistiche al secondo sulla page cache:

sudo cachestat-bpfcc 5

L'output include hit, miss, pagine dirty e rapporti lettura/scrittura. Un alto rapporto di miss indica che il tuo working set supera la memoria disponibile, e dovresti considerare se la tua applicazione necessita di più RAM o migliori pattern di accesso.

Gli OOM kill possono essere tracciati in tempo reale per capire quali processi vengono terminati e perché:

sudo bpftrace -e 'kprobe:oom_kill_process {
    printf("OOM kill: pid=%d comm=%s pages=%d\n",
        ((struct task_struct *)arg1)->pid,
        ((struct task_struct *)arg1)->comm,
        arg0);
}'

Per un approccio più semplice, lo strumento oomkill di bcc cattura questo automaticamente:

sudo oomkill-bpfcc

Questo stampa una riga ogni volta che l'OOM killer viene invocato, includendo i dettagli del processo scatenante e del processo terminato.

Tracciamento di Rete

I problemi di performance di rete sono notoriamente difficili da diagnosticare perché coinvolgono interazioni tra l'applicazione, lo stack TCP del kernel e l'infrastruttura di rete. eBPF fornisce strumenti chirurgici per ogni livello.

Lo strumento tcplife traccia i tempi di vita delle sessioni TCP, mostrando quando le connessioni vengono stabilite e chiuse insieme ai byte trasferiti:

sudo tcplife-bpfcc -D

Il flag -D include i timestamp. Ogni riga mostra il PID, il nome del processo, gli indirizzi locale e remoto, le porte, la durata e i byte inviati/ricevuti. Questo è essenziale per identificare il churn delle connessioni, connessioni inaspettatamente di breve durata o servizi che mantengono le connessioni aperte più del previsto.

Le ritrasmissioni TCP sono un indicatore critico della salute della rete:

sudo tcpretrans-bpfcc -l

Il flag -l include i tail loss probe. Ogni evento di ritrasmissione viene stampato con gli indirizzi sorgente e destinazione, lo stato e la funzione del kernel che ha innescato la ritrasmissione. Cluster di ritrasmissioni verso una destinazione specifica indicano problemi nel percorso di rete.

I pacchetti scartati a livello TCP possono essere tracciati con tcpdrop:

sudo tcpdrop-bpfcc

Ogni pacchetto scartato viene stampato con la sua stack trace del kernel, che ti dice esattamente perché il kernel ha scartato il pacchetto. Le cause comuni includono overflow del buffer del socket, reset delle connessioni e pressione di memoria.

Per un'analisi dettagliata dello stato delle connessioni TCP, puoi scrivere uno script bpftrace che traccia le transizioni di stato:

sudo bpftrace -e '
tracepoint:tcp:tcp_set_state {
    printf("pid=%d sport=%d dport=%d oldstate=%d newstate=%d\n",
        pid, args->sport, args->dport, args->oldstate, args->newstate);
}
'

La latenza di risoluzione DNS è spesso una fonte trascurata di latenza applicativa. Puoi tracciare le ricerche DNS a livello di resolver:

sudo bpftrace -e '
uprobe:/lib/x86_64-linux-gnu/libc.so.6:getaddrinfo { @start[tid] = nsecs; }
uretprobe:/lib/x86_64-linux-gnu/libc.so.6:getaddrinfo /@start[tid]/ {
    @dns_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}
'

Profiling I/O

L'I/O di storage è una delle fonti più comuni di latenza in produzione, e eBPF fornisce strumenti che tracciano l'I/O a più livelli dello stack di storage.

Lo strumento biosnoop traccia le operazioni I/O di blocco individuali con timestamp, latenze e attribuzione dei processi:

sudo biosnoop-bpfcc -d sda 10

Questo traccia tutta l'I/O di blocco verso il dispositivo sda per 10 secondi, mostrando il PID di ogni operazione, il nome del processo, il disco, il tipo di operazione, il settore, i byte e la latenza. Questo è lo strumento preferito quando hai bisogno di capire esattamente cosa sta generando I/O su un disco specifico.

Il tracciamento a livello di filesystem fornisce un contesto di livello superiore. Lo strumento ext4slower traccia le operazioni ext4 che superano una soglia di latenza:

sudo ext4slower-bpfcc 10

Questo stampa tutte le operazioni ext4 più lente di 10 millisecondi, incluse letture, scritture, aperture e sync. Questo identifica immediatamente le operazioni lente del filesystem senza richiedere di correlare le tracce a livello di blocco con i metadati del filesystem.

Per il tracciamento generale del filesystem attraverso tutti i tipi, fileslower serve lo stesso scopo:

sudo fileslower-bpfcc 10

I pattern di scrittura sono critici per capire come le applicazioni interagiscono con lo storage. L'equivalente bpftrace di filetop mostra quali file vengono letti e scritti più frequentemente:

sudo bpftrace -e '
tracepoint:syscalls:sys_enter_write {
    @writes[comm, pid] = count();
}
interval:s:5 { print(@writes); clear(@writes); }
'

Per carichi di lavoro pesanti di fsync come i database, tracciare le operazioni di sync può rivelare pattern di flush inaspettati:

sudo bpftrace -e '
kprobe:vfs_fsync_range {
    @fsync_latency[comm] = count();
}
kretprobe:vfs_fsync_range {
    printf("fsync completed: comm=%s\n", comm);
}
'

Costruire One-Liner Personalizzati con bpftrace

Il vero potere di eBPF viene dalla capacità di scrivere programmi di tracciamento personalizzati su misura per il tuo stack specifico. Il linguaggio di scripting di bpftrace rende questo accessibile a chiunque sappia scrivere uno script shell.

La struttura base di un one-liner bpftrace è una specifica di probe seguita da un blocco di azione. Le probe possono collegarsi a kprobe (funzioni del kernel), uprobe (funzioni dello spazio utente), tracepoint (eventi stabili del kernel) ed eventi software.

Contare le syscall per processo:

sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'

Istogramma delle dimensioni di lettura per processo:

sudo bpftrace -e 'tracepoint:syscalls:sys_exit_read /args->ret > 0/ {
    @read_bytes[comm] = hist(args->ret);
}'

Tracciare la creazione di nuovi processi con le linee di comando complete:

sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve {
    printf("exec: pid=%d ppid=%d %s\n", pid, curtask->real_parent->pid, str(args->filename));
}'

Tracciare le aperture di file con i percorsi completi:

sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat {
    printf("open: pid=%d comm=%s file=%s\n", pid, comm, str(args->filename));
}'

Misurare il tempo che la tua applicazione passa in una funzione specifica, aggregato per stack di chiamata:

sudo bpftrace -e '
uprobe:/opt/myapp/bin/server:DatabaseQuery { @start[tid] = nsecs; }
uretprobe:/opt/myapp/bin/server:DatabaseQuery /@start[tid]/ {
    @query_ns[ustack(5)] = hist(nsecs - @start[tid]);
    delete(@start[tid]);
}
'

Per eventi ad alta frequenza, usa le map per aggregare nel kernel ed evitare di sovraccaricare il buffer di output:

sudo bpftrace -e '
tracepoint:syscalls:sys_exit_read /args->ret > 0/ {
    @total_bytes[comm] = sum(args->ret);
    @total_calls[comm] = count();
}
interval:s:10 {
    printf("\n--- Top readers (10s window) ---\n");
    print(@total_bytes, 10);
    print(@total_calls, 10);
    clear(@total_bytes);
    clear(@total_calls);
}
'

Integrazione del Profiling Continuo

Mentre il profiling ad hoc con bpftrace e bcc-tools è essenziale per la risposta agli incidenti, il profiling continuo fornisce visibilità sulla performance sempre attiva. Due progetti open source guidano questo spazio: Parca e Pyroscope.

Parca usa eBPF per campionare continuamente le stack trace su tutti i processi di un host con overhead minimo. L'agente Parca gira come DaemonSet in Kubernetes o come servizio systemd su bare metal:

sudo parca-agent --remote-store-address=parca-server:7070 \
  --node=production-host-01 \
  --sampling-ratio=1.0 \
  --http-address=:7071

L'agente scopre automaticamente tutti i processi in esecuzione, risolve i simboli dalle informazioni di debug e dai dati BTF, e trasmette i dati di profiling al server Parca. Il server memorizza i profili in un formato colonnare ottimizzato per query di serie temporali e fornisce una UI per esplorare flame graph, confrontare profili tra finestre temporali e identificare regressioni.

Pyroscope adotta un approccio simile ma supporta modalità di profiling aggiuntive oltre alla CPU, incluso il profiling dell'allocazione di memoria, il profiling della contesa dei lock e il profiling delle goroutine per le applicazioni Go:

# pyroscope-agent.yaml
server-address: http://pyroscope-server:4040
log-level: info
targets:
  - service-name: api-server
    spy-name: ebpfspy
    application-name: api-server
  - service-name: worker
    spy-name: ebpfspy
    application-name: worker-pool

Entrambi gli strumenti si integrano con Grafana per la creazione di dashboard e alerting. Una configurazione tipica di profiling continuo include:

# Grafana data source configuration for Parca
curl -X POST http://grafana:3000/api/datasources \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Parca",
    "type": "parca",
    "url": "http://parca-server:7070",
    "access": "proxy"
  }'

Il vero valore del profiling continuo emerge nel tempo. Quando viene deployata una regressione di performance, puoi confrontare il profilo attuale con una baseline da prima del deploy e vedere immediatamente quali funzioni stanno consumando più CPU. Questo trasforma il debugging della performance da un'investigazione reattiva in un sistema di rilevamento proattivo.

Best Practice e Sicurezza in Produzione

Le garanzie di sicurezza di eBPF non significano che puoi eseguire qualsiasi programma eBPF su qualsiasi sistema di produzione senza pensarci. Sebbene il verificatore prevenga i crash del kernel, i programmi eBPF scritti male possono ancora consumare CPU eccessiva, generare output travolgente o interferire con la performance del sistema.

Imposta sempre limiti di tempo su bpftrace e bcc-tools. Ogni sessione di profiling dovrebbe avere una durata esplicita:

# Buono: durata esplicita di 30 secondi
sudo biolatency-bpfcc 30 1

# Pericoloso: gira fino all'arresto manuale
sudo biolatency-bpfcc

Sii cauto con le probe ad alta frequenza. Collegare un kprobe a una funzione che si attiva milioni di volte al secondo aggiungerà overhead misurabile anche se il programma eBPF stesso è triviale. Prima di collegare una probe in produzione, stima la frequenza:

sudo bpftrace -e 'kprobe:vfs_read { @count = count(); } interval:s:1 { print(@count); clear(@count); }'

Se la funzione si attiva più di qualche centinaia di migliaia di volte al secondo, considera se puoi usare un tracepoint invece di un kprobe, aggiungere un filtro per ridurre il numero di eventi elaborati, o campionare piuttosto che tracciare ogni evento.

Usa il flag --dry-run di bpftrace per validare gli script prima dell'esecuzione:

sudo bpftrace --dry-run -e 'kprobe:vfs_read { @[comm] = count(); }'

Limita le dimensioni delle map per prevenire la crescita illimitata della memoria:

sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }' --map-max-entries=10000

Per i sistemi di produzione, stabilisci un runbook di strumenti e script eBPF pre-approvati. La collezione bcc-tools è ben testata e sicura per l'uso in produzione. Gli script bpftrace personalizzati dovrebbero essere revisionati dal team e testati in staging prima dell'uso in produzione.

Considera l'esecuzione degli strumenti eBPF dentro container con le capability appropriate piuttosto che con accesso root completo:

docker run --rm -it --privileged \
  -v /sys/kernel/debug:/sys/kernel/debug:ro \
  -v /sys/kernel/btf:/sys/kernel/btf:ro \
  -v /proc:/proc:ro \
  quay.io/iovisor/bpftrace:latest \
  bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'

Infine, documenta i tuoi workflow di profiling. Quando un incidente si verifica alle 3 di notte, non vuoi dover scrivere script bpftrace da zero. Mantieni un repository di script di profiling testati organizzati per sintomo: alta CPU, alta latenza, crescita della memoria, errori di rete e saturazione I/O. Ogni script dovrebbe includere commenti che spiegano cosa misura, l'overhead previsto e come interpretare l'output. eBPF ti dà superpoteri in produzione, ma solo se hai praticato il loro uso prima che arrivi l'emergenza.