Cuando el CPU está al 100% y no sabes por qué, tienes dos caminos: adivinar o medir. perf y bpftrace son las herramientas que eliminan la adivinanza. perf es el subsistema de profiling integrado en el kernel Linux, expuesto a través de la syscall perf_event_open; no es un añadido externo ni un agente de terceros, sino parte del árbol del kernel desde la versión 2.6.31. bpftrace es un lenguaje de alto nivel sobre eBPF que permite instrumentar prácticamente cualquier punto del sistema —funciones de kernel, funciones de espacio de usuario, trazas estáticas— sin recompilar nada y con un overhead que en la mayoría de escenarios es inferior al 1%.
La razón por la que estas herramientas funcionan de forma tan eficiente es el mecanismo subyacente: perf utiliza contadores de hardware de rendimiento (PMU, Performance Monitoring Unit), que son registros físicos en el propio procesador. Cuando le pides a perf que cuente cache misses o branch mispredictions, el contador hardware genera una interrupción cada N eventos; el kernel anota el puntero de instrucción en ese momento y continúa. El proceso profiled apenas nota la diferencia. bpftrace va un paso más allá: compila programas eBPF al vuelo, los verifica formalmente en el kernel para garantizar que no cuelgan ni corrompen memoria, y los ejecuta en puntos de traza sin modificar el código objetivo.
El momento de usar perf stat es cuando quieres diagnosticar el comportamiento microarquitectural: una aplicación que debería ser rápida y no lo es, sin causa aparente en el código. perf record + perf report (y su visualización como flame graph) son para encontrar hot paths: qué funciones consumen el tiempo real de CPU. bpftrace entra cuando necesitas responder preguntas dinámicas que perf no puede responder solo: ¿qué argumentos se pasan a esta función? ¿cuánto tarda cada llamada a read() en este proceso específico? ¿qué stack trace genera estas llamadas al sistema?
Si usas perf sin los símbolos correctos, el resultado es una columna de direcciones hexagonales sin nombre; si ejecutas perf record sobre un binario sin información de frame pointers o DWARF, el flame graph aparece plano y no refleja la jerarquía de llamadas real. Con bpftrace, el error más común es atacar un punto de traza que el compilador ha inlineado: el probe no dispara nunca y parece que nada ocurre.
Escenario completo: proceso con CPU al 100%
El escenario es real: un proceso llamado servidor está consumiendo un núcleo entero de forma sostenida. Vamos a identificar qué función es responsable, entender el comportamiento de caché, y confirmar el diagnóstico con bpftrace.
# Instalar las herramientas necesarias en Debian Bookworm
sudo apt install linux-perf bpftrace linux-base \
linux-image-$(uname -r)-dbg \
--no-install-recommends
# El paquete se llama 'linux-perf' en Debian; el binario resultante
# incluye un wrapper que selecciona la versión correcta para el kernel activo.
# ── PASO 1: perf top — visión en tiempo real ──────────────────────────
# Similar a 'top' pero muestra funciones, no procesos.
# -g activa el unwinding de call stacks (necesita frame pointers o DWARF)
sudo perf top -g -p $(pgrep servidor)
# Si el binario no tiene símbolos, añadir el paquete -dbgsym o compilar con:
# gcc -O2 -g -fno-omit-frame-pointer -o servidor servidor.c
# ── PASO 2: perf stat — contadores de hardware ───────────────────────
# Capturar durante 10 segundos sobre el proceso ya en ejecución.
# Estos eventos son nombres portables; perf los mapea al hardware real.
sudo perf stat \
-e cycles,instructions,cache-references,cache-misses,branch-misses \
-p $(pgrep servidor) \
-- sleep 10
# La ratio cache-misses/cache-references > 5% ya es una señal de alarma.
# instructions/cycles < 1.0 (IPC bajo) indica stalls, frecuentemente por caché.
# ── PASO 3: perf record + flame graph ────────────────────────────────
# Capturar 30 segundos con frecuencia de muestreo de 999 Hz.
# 999 en lugar de 1000 evita resonancia con timers del sistema.
sudo perf record \
-F 999 \
-g \
--call-graph dwarf \
-p $(pgrep servidor) \
-o /tmp/perf.data \
-- sleep 30
# Convertir a formato texto para flamegraph.pl (herramienta de Brendan Gregg)
sudo perf script -i /tmp/perf.data > /tmp/perf.script
# Si tienes flamegraph instalado (apt install flamegraph en Bookworm):
stackcollapse-perf.pl /tmp/perf.script | flamegraph.pl > /tmp/flame.svg
# Ver el flame graph en el navegador
xdg-open /tmp/flame.svg
# Si prefieres quedarte en terminal, perf report es suficiente:
sudo perf report -i /tmp/perf.data --stdio | head -40
# ── PASO 4: bpftrace — confirmar el hot path con contexto ────────────
# Medir la distribución de latencias de una función específica.
# Supongamos que perf report señala 'procesar_peticion' como responsable.
# bpftrace puede adjuntarse a ella sin recompilar ni reiniciar el proceso.
# uprobe: probe en espacio de usuario, sobre el binario del proceso.
# El path debe ser el binario real, no un wrapper.
sudo bpftrace -e '
uprobe:/usr/local/bin/servidor:procesar_peticion
{
@inicio[tid] = nsecs;
}
uretprobe:/usr/local/bin/servidor:procesar_peticion
/@inicio[tid]/
{
@latencia_us = hist((nsecs - @inicio[tid]) / 1000);
delete(@inicio[tid]);
}
END { clear(@inicio); }
'
# ── PASO 5: bpftrace — detectar llamadas de sistema lentas ───────────
# Si el proceso pasa tiempo en kernel (sys%), este one-liner
# muestra los read() que tardan más de 1 ms, con stack trace.
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_read
/pid == '"$(pgrep servidor)"'/
{
@ts[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_read
/@ts[tid] && (nsecs - @ts[tid]) > 1000000/
{
printf("read lento: %d us\n", (nsecs - @ts[tid]) / 1000);
printf("%s\n", ustack);
delete(@ts[tid]);
}
'
Qué está ocurriendo en cada paso
perf top -g -p hace muestreo estadístico: cada ~1 ms interrumpe el proceso, anota el RIP (instruction pointer) y, si -g está activo, recorre el stack. Lo que ves en pantalla es la distribución acumulada de dónde estaba el CPU en esos momentos. Una función al 60% significa que en el 60% de los muestreos el CPU estaba dentro de ella, no necesariamente que esa función sea lenta en cada llamada individual.
perf stat con los eventos cache-misses y cache-references usa los registros PMU directamente. La métrica crítica aquí es el IPC (Instructions Per Cycle): en código moderno bien optimizado esperamos valores entre 1.5 y 4. Si ves 0.3 o 0.4, el pipeline está parado esperando memoria. Un cache-miss en L3 cuesta entre 100 y 300 ciclos en hardware moderno; si tu función hace muchos de ellos, ninguna optimización algorítmica compensa esa penalización hasta que resolves el problema de localidad de datos.
perf record --call-graph dwarf es más costoso que --call-graph fp (frame pointers) porque lee la información DWARF del binario para reconstruir el stack, pero funciona incluso con binarios compilados con -fomit-frame-pointer, que es el default en GCC con -O2. Si controlas la compilación del binario que analizas, añadir -fno-omit-frame-pointer y usar --call-graph fp reduce el overhead y produce flame graphs más limpios.
El flame graph resultante en /tmp/flame.svg muestra el tiempo de CPU en el eje X (anchura = porcentaje de tiempo) y la profundidad del stack en el eje Y. Las torres anchas en la parte superior son los hot paths. Un flame graph plano —stacks de un solo nivel— suele indicar un problema de símbolos o de unwinding, no que el código sea realmente así.
uprobe y uretprobe en bpftrace funcionan reemplazando temporalmente el primer byte de la función con una instrucción int3 (breakpoint x86). Cuando el proceso llega ahí, el kernel ejecuta el programa eBPF adjunto y restaura la instrucción original; el proceso continúa sin saber que fue interceptado. Por eso procesar_peticion debe existir como símbolo no inlineado en el binario: si el compilador la inlinea dentro de su llamador, no hay punto de entrada al que adjuntar el probe. Verificarlo con nm /usr/local/bin/servidor | grep procesar_peticion antes de ejecutar bpftrace te ahorra la confusión de un probe que no dispara.
El filtro /@ts[tid]/ en el bloque uretprobe es un guard que descarta entradas donde el uretprobe dispara antes de que lo haya hecho el uprobe correspondiente —posible si adjuntas bpftrace mientras la función ya está en ejecución— evitando latencias negativas o desbordamientos en el histograma.
La combinación de perf stat para entender el comportamiento microarquitectural, perf record + flame graph para localizar el hot path, y bpftrace para instrumentarlo con contexto cubre el 95% de los problemas de CPU que encontrarás en producción, con un overhead que puedes justificar dejar activo durante minutos mientras el sistema sigue sirviendo tráfico real.