El eBPF (extended Berkeley Packet Filter) es un subsistema del kernel Linux que permite cargar y ejecutar programas escritos por el usuario directamente en el espacio del kernel, sin necesidad de recompilar el kernel, cargar módulos, ni reiniciar nada. La clave está en la palabra verificado: antes de ejecutarse, cada programa pasa por un verificador estático que demuestra formalmente que el código termina, que no accede a memoria fuera de límites, y que no compromete la estabilidad del sistema. Si el verificador rechaza el programa, simplemente no se carga.
El diseño es deliberado. Históricamente, extender el kernel implicaba un módulo .ko que, si contenía un bug, volcaba el sistema. eBPF rompe ese modelo: el código corre en un sandbox dentro del kernel, usando un conjunto de instrucciones reducido (ISA propio de 64 bits) que el JIT del kernel compila a código nativo en tiempo de carga. El overhead es real pero mínimo — del orden de nanosegundos por evento — porque el código se ejecuta in situ, sin cambios de contexto al espacio de usuario para cada muestra.
Úsalo cuando necesitas respuestas que strace, perf stat o incluso perf record no pueden darte sin un overhead inaceptable en producción: latencia de una syscall específica filtrada por proceso, throughput de conexiones TCP por cgroup, flamegraphs de funciones del kernel a frecuencia real. La alternativa a eBPF en producción suele ser instrumentación a nivel de aplicación (que requiere deploys) o ptrace/strace (que serializa las syscalls y puede multiplicar la latencia por diez).
Cuando lo haces mal, los problemas son de dos tipos. Primero, el verificador rechaza el programa con mensajes crípticos sobre profundidad de stack o accesos a registros no inicializados — frustrante pero seguro. Segundo, y más importante en producción: los map de eBPF (estructuras de datos compartidas entre kernel y espacio de usuario) tienen memoria pinned en el kernel; si no los cierras explícitamente o el proceso que los gestiona muere sin limpieza, puedes fugar kilobytes por minuto en sistemas con alta creación de programas eBPF. Monitoriza /proc/sys/kernel/bpf_stats_enabled y el output de bpftool map list si despliegas agentes de larga duración.
Infraestructura mínima en Debian Bookworm
Debian 12 incluye un kernel 6.1 con eBPF completamente funcional. Las herramientas principales están en dos paquetes: bpftrace (lenguaje de scripting de alto nivel) y bpfcc-tools (colección de herramientas precompiladas del proyecto BCC). También necesitas las cabeceras del kernel en ejecución.
# Instalación base
apt install -y bpftrace bpfcc-tools linux-headers-$(uname -r)
# Verificar que el kernel expone el subsistema correctamente
bpftool feature probe kernel 2>/dev/null | grep -E "^eBPF|program_type|map_type" | head -20
# ── ESCENARIO: diagnóstico de latencia en un servidor web ──────────────
#
# El síntoma: las peticiones HTTP tienen spikes de latencia esporádicos.
# Sospecha: contención en llamadas a write()/sendfile() o bloqueos en
# operaciones de red. Vamos a medir tiempo de ejecución de syscalls
# específicas usando bpftrace, y luego correlacionar con conexiones TCP
# usando tcptracer de BCC.
# ── Paso 1: latencia de write() y sendfile() por PID ──────────────────
# Este script bpftrace mide el tiempo (en microsegundos) que tarda cada
# llamada a write() o sendfile() para un proceso dado.
# Lo guardamos en un archivo para poder relanzarlo fácilmente.
cat > /tmp/syscall_lat.bt << 'EOF'
#!/usr/bin/env bpftrace
// Recibe el PID del servidor como argumento: bpftrace syscall_lat.bt <PID>
// Si no se pasa argumento, traza todos los procesos (más overhead).
BEGIN
{
printf("Trazando write/sendfile — Ctrl-C para volcar histogramas\n");
// @target guarda el PID filtro; 0 significa "todos"
@target = (uint64)$1;
}
// tracepoint es más estable que kprobe: la ABI del tracepoint está
// garantizada entre versiones de kernel; kprobe puede cambiar con
// cualquier actualización menor.
tracepoint:syscalls:sys_enter_write,
tracepoint:syscalls:sys_enter_sendfile64
/@target == 0 || pid == @target/
{
// Guardamos timestamp de entrada; la clave incluye tid para evitar
// colisiones cuando el proceso es multihilo.
@start[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_write,
tracepoint:syscalls:sys_exit_sendfile64
/@start[tid]/
{
// Histograma logarítmico en microsegundos.
// El operador >> 10 equivale a dividir por 1024 ≈ convertir a µs.
@lat_us = hist((nsecs - @start[tid]) >> 10);
delete(@start[tid]);
}
END
{
// Al salir, bpftrace imprime automáticamente todos los maps @
// con sus valores acumulados.
clear(@target);
clear(@start);
}
EOF
chmod +x /tmp/syscall_lat.bt
# Ejecutar: pasa el PID de nginx/apache/lo que tengas.
# Ejemplo con nginx (ajusta el PID real):
NGINX_PID=$(pgrep -o nginx)
echo "PID objetivo: $NGINX_PID"
# Descomentar para lanzar:
# bpftrace /tmp/syscall_lat.bt $NGINX_PID
# ── Paso 2: mientras el script anterior corre, en otra terminal ────────
# execsnoop-bpfcc muestra cada exec() en tiempo real — útil para detectar
# si hay procesos hijo inesperados que el servidor lanza bajo carga.
# La versión de BCC en Debian se llama execsnoop-bpfcc (no execsnoop a secas).
execsnoop-bpfcc -T 2>/dev/null &
EXEC_PID=$!
# opensnoop-bpfcc: cada open()/openat() con latencia — detecta si el
# servidor está reabriendo archivos de configuración o logs en cada request.
opensnoop-bpfcc -p $NGINX_PID -T 2>/dev/null &
OPEN_PID=$!
# ── Paso 3: trazar conexiones TCP nuevas ──────────────────────────────
# tcptracer-bpfcc imprime cada connect/accept/close con dirección y puerto.
# Util para ver si hay conexiones a backends (DB, caché) que se abren y
# cierran en cada request en lugar de usar un pool.
tcptracer-bpfcc -v 2>/dev/null &
TCP_PID=$!
# Dejar correr 30 segundos bajo carga real y luego matar los tracers
sleep 30
kill $EXEC_PID $OPEN_PID $TCP_PID 2>/dev/null
# ── Paso 4: flamegraph de CPU on-CPU con bpftrace ─────────────────────
# Captura stack traces del kernel + espacio de usuario a 99 Hz durante
# 20 segundos para el proceso objetivo. 99 Hz en lugar de 100 Hz evita
# el efecto de aliasing con timers internos que corren a frecuencias
# múltiplos de 100.
bpftrace -e "
profile:hz:99
/pid == $NGINX_PID/
{
@[ustack, kstack] = count();
}
interval:s:20 { exit(); }
" --output text 2>/dev/null | \
# stackcollapse convierte el output de bpftrace al formato de flamegraph.pl
# Requiere: apt install flamegraph (o clonar https://github.com/brendangregg/FlameGraph)
stackcollapse-bpftrace.pl 2>/dev/null | \
flamegraph.pl --title "nginx on-CPU 20s" > /tmp/nginx_flamegraph.svg
echo "Flamegraph generado: /tmp/nginx_flamegraph.svg"
# ── Introspección del estado eBPF en el sistema ───────────────────────
# Ver todos los programas eBPF cargados actualmente (incluidos los de
# agentes externos como Datadog, Cilium, etc.)
bpftool prog list
# Ver todos los maps y cuánta memoria ocupan
bpftool map list
# Si un agente externo está usando eBPF, aquí verás sus programas.
# La columna "loaded_at" te dice hace cuánto se cargaron.
# pids muestra qué proceso los tiene abiertos (si aún vive alguno).
Lo que está pasando por debajo
El script syscall_lat.bt usa tracepoints en lugar de kprobes. La diferencia no es cosmética: los tracepoints son puntos de instrumentación estáticos que el propio kernel mantiene como parte de su ABI estable. Cuando ejecutas ese script, el verificador compila el cuerpo de cada bloque a código nativo y lo engancha a los tracepoints sys_enter_write y sys_exit_write sin modificar el código en ejecución del kernel — el kernel llama a tu función a través de un trampolín JIT. Los kprobes, en cambio, parchean bytes de instrucción en tiempo de ejecución (técnica breakpoint/int3) y son más frágiles ante actualizaciones del kernel.
El mapa @start[tid] es un BPF hash map: una tabla hash residente en el kernel indexada por thread ID. El par enter/exit del tracepoint comparte estado a través de ese mapa sin ningún lock explícito porque el acceso por tid está implícitamente serializado — dos entradas del mismo hilo no pueden estar en el kernel simultáneamente. El mapa @lat_us es un BPF histogram map, una estructura especial que agrega valores en buckets logarítmicos directamente en el kernel; cuando bpftrace imprime el histograma al salir, lo está leyendo del kernel via read() sobre el file descriptor del mapa — un único syscall, no uno por muestra.
El bloque profile:hz:99 del flamegraph usa el tipo de programa perf_event de eBPF. El kernel muestrea el PC (program counter) y captura el call stack completo — kernel y usuario — con bpf_get_stackid(). La profundidad máxima de stack que eBPF puede capturar está limitada por perf_event_max_stack (/proc/sys/kernel/perf_event_max_stack, por defecto 127 frames). Si ves stacks cortados en el flamegraph, sube ese valor: sysctl -w kernel.perf_event_max_stack=512.
bpftool prog list y bpftool map list son tu herramienta de auditoría cuando un agente externo (Datadog Agent con eBPF habilitado, Falco, Cilium) está corriendo en el sistema. Cada programa eBPF tiene un id, un tipo, y una lista de PIDs que mantienen el file descriptor abierto. Si el PID desaparece pero el programa sigue listado, alguien hizo pin del programa en el filesystem BPF (/sys/fs/bpf/) — búscalo con find /sys/fs/bpf/ -type f y elimina lo que no reconozcas, o tendrás memoria del kernel nunca liberada.
Un detalle que sorprende la primera vez: en Debian Bookworm, bpfcc-tools instala los binarios con el sufijo -bpfcc (execsnoop-bpfcc, opensnoop-bpfcc, tcptracer-bpfcc). Esto evita conflictos con otros paquetes. Los scripts de BCC en Python cargan el código C del programa eBPF, lo compilan con LLVM en tiempo de ejecución, y lo cargan vía bpf() syscall — por eso necesitan linux-headers-$(uname -r): el compilador los usa para incluir las definiciones de estructuras del kernel exactas de tu versión en ejecución.
[Ubuntu]: En Ubuntu 20.04+ los binarios de BCC están sin sufijo (execsnoop, opensnoop) y el paquete es bpfcc-tools igual, pero la ruta de las cabeceras puede diferir en kernels HWE. En Ubuntu 22.04+ también está disponible linux-tools-generic con bpftrace integrado en el metapaquete.
[otras familias]: En RHEL 9/Fedora, bpftrace y bcc-tools están en los repos base pero requieren kernel-devel en lugar de linux-headers-*; los binarios de BCC no usan sufijo.
Cuando veas Cilium como CNI en un clúster Kubernetes, toda la política de red, el balanceo de carga de servicios (reemplazando kube-proxy) y la telemetría de conexiones entre pods está corriendo como programas eBPF de tipo XDP y TC (traffic control) — el mismo mecanismo que acabas de usar para medir latencia de syscalls, aplicado a procesar paquetes a velocidad de línea antes de que el stack TCP del kernel los toque.