Ir al contenido

WhyStack Profiler

Las métricas y los logs muestran el síntoma: subió la latencia, aparecieron errores. No te dicen qué cambió dentro del proceso entre la versión que funcionaba y la que no funciona.

El WhyStack Profiler mide eso. Observa un proceso durante una ventana corta después de cada deploy y guarda una firma de comportamiento compacta: cuánta memoria asigna el programa y dónde, qué llamadas al sistema hace, cómo se distribuyen las esperas por locks, cuántos peers de red tiene. Comparar dos versiones nos permite decir “este es el cambio” en lugar de “algo empeoró”.

Eso cuesta un permiso que el resto de WhyStack nunca pide. Esta página existe para que decidas con la lista en la mano.

Activar un flag en el Agent sería más simple. No lo hacemos porque un flag vuelve invisible el privilegio: quien herede el entorno meses después no tiene forma de saber, mirando el deployment, que ese contenedor ahora puede leer la memoria de otros procesos. Con dos artefactos, el privilegio aparece en la puerta de entrada.

La separación no depende de la disciplina. El Agent se niega a arrancar si detecta una capability de kernel en su propio proceso. Y el Profiler no se puede configurar para leer tus integraciones: su imagen no registra ningún connector de datos, así que ninguna variable lo convierte en un lector de tu stack. Una variable que lo pida se ignora, y él lo dice en el log de boot. Ninguno de los dos se convierte en el otro por un error de configuración.

Hasta la versión 1.15.0 esta página prometía más de lo que el código entregaba, en la dirección menos conveniente: el Profiler arrancaba sirviendo el conjunto de datos sintético que existe para desarrollo. No se leía nada de tu infraestructura (ese conjunto es fabricado), pero ocupaba el lugar de una fuente en la flota. Corregido de raíz: decide la clase del artefacto, no la configuración del deployment.

La barrera más dura aquí es el kernel, y rechaza hosts que nadie espera ver rechazados; por eso es la primera pregunta de la calificación, no la última. Un host que no la supera nunca llega a la POC; descubrirlo durante la POC cuesta la POC.

Ejecuta el preflight en el host candidato. Solo lee /proc y /sys, no cambia nada y no necesita root:

Ventana de terminal
curl -O https://docs.whystack.ai/profiler-preflight.sh
sh profiler-preflight.sh

Descárgalo y léelo antes de ejecutarlo. El script completo está publicado justo abajo, y nadie debería ejecutar el script de otro sin leerlo. Por eso no ofrecemos un curl | sh.

Barrera Por qué rechaza, y cómo fallaría en silencio
Kernel ≥ 5.8 BTF, el ring buffer y CAP_BPF llegan todos en esa versión. El filtro es el kernel, no la distribución: RHEL 7 / 3.10 queda fuera, y el fallback sin eBPF es solo /proc.
BTF compilado Una versión ≥ 5.8 no implica BTF: un kernel compilado sin CONFIG_DEBUG_INFO_BTF supera la barrera anterior y falla en esta. Por eso son dos barreras y no una.
cgroup v2 Sin la jerarquía unificada no hay forma de acotar una captura por cgroup.
tracefs montado Es donde la biblioteca de eBPF busca los tracepoints. Sin él, los colectores se cargan, no se acoplan, y la captura vuelve con ejes faltantes en lugar de vacíos, lo que se lee como “ese comportamiento se detuvo”.
Perfil de confinamiento docker-default solo permite ptrace entre contenedores con el mismo perfil. Heredarlo cuesta dos colectores, en silencio.

El script sale con tres códigos, y el del medio es lo que suele faltarles a los checklists de calificación: 0 todo pasó · 1 falló una barrera dura · 2 nada falló y algo no se pudo decidir desde ahí.

El caso normal de 2 son las cinco capabilities: el shell que ejecuta el script no es el workload del Profiler, así que lo que ese shell tiene no responde qué se le permitirá tener al Profiler. Esa es una pregunta de política para quien administra el clúster. Un checklist que contara esto como “ok” entregaría un aprobado que nadie verificó, que es exactamente la falla que el script existe para evitar. (Su primera versión cometió esa falla al revés: revisaba /sys/kernel/tracing/events con test -d, que falla para un usuario sin privilegios porque el directorio es 0700 root:root, rechazando un host perfectamente montado. Ahora lee /proc/mounts, que cualquiera puede leer.)

Acoplar un colector privilegiado a software de terceros (de un proveedor, COTS, legado que nadie mantiene) puede violar o anular el contrato de soporte que tienes con quien lo vende. Esa es una pregunta legal, y por eso va antes de las técnicas: ningún resultado del script la reemplaza. El script la imprime junto con las otras dos que no puede responder (hostPID y la lista de objetivos), para que no se olvide solo porque todo lo demás salió en verde.

#!/bin/sh
# WhyStack Profiler: host preflight.
#
# Answers one question, before anyone installs anything or grants a capability:
# CAN this host run the Profiler at all? It reads; it changes nothing.
#
# Written in English on purpose: this is one canonical file served to readers of
# both the Portuguese and the English documentation, and a single script cannot
# be bilingual. Each check is explained, in your language, on the page that
# publishes this script verbatim: https://docs.whystack.ai/security/profiler/
#
# Exit codes. A check that could not run is NOT a pass:
# 0 every gate passed, and nothing was left unknown
# 1 at least one hard gate FAILED: this host is out until it is fixed
# 2 no gate failed, but something could not be determined from here
#
# Run it as the least privileged user you have. It needs no root: every gate it
# can decide is decidable by reading /proc and /sys.
set -u
PASS=0
FAIL=0
UNKNOWN=0
pass() { printf ' PASS %s\n' "$1"; PASS=$((PASS + 1)); }
fail() { printf ' FAIL %s\n ↳ %s\n' "$1" "$2"; FAIL=$((FAIL + 1)); }
unknown() { printf ' UNKNOWN %s\n ↳ %s\n' "$1" "$2"; UNKNOWN=$((UNKNOWN + 1)); }
printf 'WhyStack Profiler: host preflight\n'
printf 'host: %s kernel: %s arch: %s\n\n' "$(uname -n)" "$(uname -r)" "$(uname -m)"
# ── 1. Kernel ≥ 5.8 ───────────────────────────────────────────────────────────
# BTF, the BPF ring buffer and CAP_BPF all land in 5.8. Below it the Profiler
# refuses to boot rather than run with half its collectors.
release=$(uname -r)
major=$(echo "$release" | cut -d. -f1)
minor=$(echo "$release" | cut -d. -f2 | cut -d- -f1)
case "$major$minor" in
*[!0-9]*|'')
unknown "kernel >= 5.8" "could not parse a version out of '$release'" ;;
*)
if [ "$major" -gt 5 ] || { [ "$major" -eq 5 ] && [ "$minor" -ge 8 ]; }; then
pass "kernel >= 5.8 ($release)"
else
fail "kernel >= 5.8 ($release)" \
"this host is out. The gate is the kernel, not the distribution: RHEL 7 / 3.10 cannot run this, and --no-ebpf is only /proc."
fi ;;
esac
# ── 2. BTF actually compiled in ───────────────────────────────────────────────
# Version >= 5.8 does NOT imply BTF: a kernel built without CONFIG_DEBUG_INFO_BTF
# passes check 1 and fails here. This is the gate that surprises people, so it is
# separate from the version rather than folded into it.
if [ -r /sys/kernel/btf/vmlinux ]; then
pass "kernel BTF present (/sys/kernel/btf/vmlinux)"
else
fail "kernel BTF present" \
"no /sys/kernel/btf/vmlinux. The kernel was built without CONFIG_DEBUG_INFO_BTF; the version alone never proved it."
fi
# ── 3. cgroup v2 ──────────────────────────────────────────────────────────────
if [ -r /sys/fs/cgroup/cgroup.controllers ]; then
pass "cgroup v2 unified hierarchy"
else
fail "cgroup v2 unified hierarchy" \
"no /sys/fs/cgroup/cgroup.controllers. A v1-only or hybrid host cannot scope a capture by cgroup."
fi
# ── 4. tracefs mounted ────────────────────────────────────────────────────────
# This one earns its own gate because of HOW it fails: without tracefs the eBPF
# programs load and never attach, so the capture comes back with axes MISSING
# rather than empty, which reads like "nothing changed in this deploy".
#
# It is read from /proc/mounts, which is world-readable, and NOT with
# `test -d /sys/kernel/tracing/events`. That was the first version, and it is
# wrong in the direction that matters: tracefs is 0700 root:root, so descending
# into it as an unprivileged user fails on a host where it is perfectly mounted.
# A check that reports FAIL when it could not look is the failure this whole
# script exists to prevent.
if [ -r /proc/mounts ]; then
mounts=$(cat /proc/mounts)
if echo "$mounts" | grep -qE '^tracefs +/sys/kernel/tracing '; then
pass "tracefs mounted (/sys/kernel/tracing)"
elif echo "$mounts" | grep -qE '^(debugfs|tracefs) +/sys/kernel/debug '; then
pass "tracefs reachable under /sys/kernel/debug/tracing"
else
fail "tracefs mounted" \
"no tracefs in /proc/mounts. Collectors would load, attach nothing, and the capture would come back missing axes instead of empty."
fi
else
unknown "tracefs mounted" "could not read /proc/mounts from here"
fi
# ── 5. The five capabilities ──────────────────────────────────────────────────
# What this shell holds is NOT the answer. The Profiler runs as its own
# workload, and whether the five can be granted to it is a policy question for
# whoever owns the cluster (PodSecurity / PSP / the container runtime). So this
# reports and does not decide.
caps=$(grep -m1 '^CapEff:' /proc/self/status 2>/dev/null | awk '{print $2}')
if [ -n "${caps:-}" ]; then
unknown "CAP_BPF, CAP_PERFMON, CAP_SYS_PTRACE, CAP_DAC_READ_SEARCH, CAP_SYS_ADMIN grantable" \
"not decidable from this shell (it holds CapEff=$caps). Ask whoever owns the cluster whether a workload may be granted all five. All five are required: the Profiler fails boot naming the one that is missing, and there is no reduced mode."
else
unknown "the five capabilities grantable" \
"could not read /proc/self/status. Ask whoever owns the cluster whether a workload may be granted all five."
fi
# ── 6. Confinement profile ────────────────────────────────────────────────────
# Measured, not theorised: Docker's default seccomp/AppArmor profile denies
# ptrace against an unconfined target, and that alone kills two collectors.
lsm=$(cat /proc/self/attr/current 2>/dev/null | tr -d '\0')
case "${lsm:-}" in
''|unconfined)
pass "no restrictive LSM profile on this shell (${lsm:-none})" ;;
*)
unknown "confinement profile" \
"this shell runs under '$lsm'. Docker's default profile denies ptrace against an unconfined target, which silently costs two collectors. The Profiler needs its own profile decided, not inherited." ;;
esac
# ── 7. Questions this script cannot answer ────────────────────────────────────
printf '\nNot decidable from a host, and not optional:\n'
printf ' • Does your support contract with the vendor of the observed software\n'
printf ' ALLOW attaching a privileged collector to it? Doing so can void support.\n'
printf ' This is a legal question, and it comes before the technical ones.\n'
printf ' • Can the Profiler run with hostPID, so it can see the target process?\n'
printf ' • Which processes go on the target list? It is required, it is yours,\n'
printf ' and there is no default that means "everything on the node".\n'
printf '\n%d passed, %d failed, %d unknown\n' "$PASS" "$FAIL" "$UNKNOWN"
if [ "$FAIL" -gt 0 ]; then
printf 'VERDICT: this host does NOT qualify yet.\n'
exit 1
fi
if [ "$UNKNOWN" -gt 0 ]; then
printf 'VERDICT: no gate failed, and %d item(s) could not be decided from here.\n' "$UNKNOWN"
printf 'Not a pass. Answer them before the POC, not during it.\n'
exit 2
fi
printf 'VERDICT: this host qualifies.\n'
exit 0
Permiso Para qué
CAP_BPF Cargar los programas eBPF que cuentan llamadas al sistema, esperas por locks, asignaciones de memoria y eventos de red.
CAP_PERFMON Muestrear la CPU vía perf_event, a 100 Hz por núcleo.
CAP_SYS_PTRACE Acoplar esos programas a un proceso que corre como otro usuario (leer /proc/<pid>/ns/* y /proc/<pid>/exe). Sin ella, eBPF se carga y ningún colector se acopla: la captura vuelve vacía, lo que es indistinguible de “nada cambió en este deploy”.
CAP_DAC_READ_SEARCH Leer /sys/kernel/tracing (que es 0700 root:root) y el propio /proc/self/mem del colector. Lo segundo parece raro y no lo es: un binario que recibe capabilities como file capabilities (que es como este evita correr como root) termina con su propio /proc/self/* perteneciendo a root. Sin ella fallan los tracepoints y la detección de la versión del kernel, que son siete de los doce colectores.
CAP_SYS_ADMIN Crear el perf_uprobe detrás del probe de heap, el eje que trae func y file:line. Equivale a root en un contenedor con hostPID. Lee el bloque de abajo antes de aprobar.
hostPID o shareProcessNamespace Ver el proceso objetivo. Sin esto, solo se ve a sí mismo.
/sys/kernel/tracing y /sys/kernel/debug montados Donde la biblioteca de eBPF busca los tracepoints. Sin ellos el colector se carga y no se acopla, y la captura vuelve con ejes faltantes en lugar de vacíos.
Un perfil de AppArmor dedicado docker-default solo permite ptrace entre contenedores con el mismo perfil, y tu objetivo normalmente no lo tiene. Publicamos un perfil que es docker-default más una línea (ptrace (read) peer=unconfined), y no apparmor=unconfined, que desharía todo el confinamiento por una sola regla.
get · list · watch sobre pods Todavía no se pide. Será la forma en que detecte un deploy cuando exista el disparador automático, y solo en los namespaces de tu allowlist. Hoy el Profiler no necesita ningún permiso de Kubernetes.
Kernel ≥ 5.8, cgroup v2 BTF, ring buffer y CAP_BPF. Por debajo de eso se niega a arrancar en lugar de correr medio a ciegas.
Red de salida Solo el túnel de WhyStack. Ningún otro destino.

Lo que no pide importa igual: ningún permiso de escritura en Kubernetes. En particular, no pide create sobre pods/ephemeralcontainers, el permiso que necesitaría el enfoque alternativo, y uno incómodamente genérico: quien puede agregar un contenedor efímero puede agregar cualquier imagen a cualquier pod del namespace.

Tú declaras la lista, y es obligatoria. No hay un valor por defecto que signifique “cualquier cosa en el nodo”: un agente que ve todos los procesos por omisión es un privilegio más allá de lo necesario, y la omisión nunca debería ser la forma de otorgar el alcance más amplio posible.

La lista vive en un archivo que despliegas junto al Profiler, por namespace y workload. Fuera de ella, el Profiler no se acopla, y el rechazo aparece en su log, nombrando el objetivo, para que “no capturó” nunca se confunda con “capturó y no encontró nada”.

Se diferencia de la denylist de tools en algo que vale la pena entender: la denylist se compone en el control-plane y se aplica en el borde; esta lista es enteramente tuya y nunca pasa por nosotros. No dice qué puede leer WhyStack. Dice cuáles de tus procesos puede tocar quien sea.

Durante una ventana corta (60 segundos por defecto) sobre un proceso a la vez. No mide de forma continua.

Hoy el disparador es explícito. Una captura se solicita: tu equipo durante una investigación, o tu pipeline después de un deploy. El Profiler recibe la solicitud por la misma conexión que ya usa para todo lo demás, verifica el objetivo contra tu lista antes de mirar cualquier proceso, mide y devuelve el resultado.

Ambos pasan por la misma cola, por puertas distintas. Desde la app, en Profiling, cualquier miembro de la organización la solicita mediante un formulario: el acceso es el mismo que abre la página, y la decisión de owner/admin ya se tomó antes, cuando se activó el Profiler. Desde el pipeline, mediante una llamada autenticada que lleva un token rpcap_ que emite un owner o admin, y que solo encola una solicitud de captura: no lee nada.

Los pasos, los campos y los límites están en Instalar el Profiler → Solicitar una captura. En ambos casos el registro de la captura guarda quién la pidió (la persona o el pipeline, por nombre) junto al resultado.

En Docker no habrá disparador automático, ni siquiera más adelante. Podríamos detectar deploys escuchando el socket de Docker, y elegimos no pedirlo: montar el socket de Docker equivale a root en la máquina, y sería el permiso más grande de toda esta página, todo por una comodidad.

Lo que cruza es la firma agregada (números y nombres, nunca lo que el proceso maneja):

  • símbolo, archivo y módulo del punto del código que asignó memoria;
  • nombre y cantidad de llamadas al sistema, con latencia promedio;
  • la forma de la contención de locks, sin direcciones de memoria;
  • contadores de red: cuántos peers, errores por tipo, latencia promedio;
  • los peers de salida, identificados por IP:port tal como los ve el socket, hasta los 64 con más tráfico, cada uno con conexiones, bytes y latencia. Es lo que hace visible un deploy que cambió una dependencia (dejó de llamar a la caché y empezó a llamar directamente a la base de datos), algo que el conteo por sí solo no muestra. Quien llama al proceso se convierte solo en un número, sin dirección;
  • distribuciones de memoria y CPU;
  • nombre del proceso, versión y entorno;
  • qué instancia se midió: el UID del pod, el id del contenedor, o el PID cuando no hay nada más estable. Una captura siempre es de un proceso; en Kubernetes tu workload tiene varias réplicas, y sin ese identificador dos capturas de réplicas distintas entrarían en el mismo conjunto como si fueran lo mismo observado dos veces.

La firma no pasa por la redacción de PII del borde. Los peers salen con la IP de tu servicio de destino: una dirección de tu infraestructura, la misma clase de dato que el estado aprendido guarda sobre tus registros DNS (ver Redacción de PII).

Los símbolos y las rutas de archivo de tu código cruzan la frontera. Es la misma clase de dato que el connector de código ya lee una vez que lo conectas, y lo decimos explícitamente porque no se puede redactar: borrar el nombre de la función destruye lo único que hace útil a la firma. Si esa clase de dato no puede salir de tu frontera, el camino correcto es no instalar el Profiler, en lugar de instalarlo y confiar en una máscara.

Nunca sale, bajo ninguna configuración:

  • el contenido de solicitudes, respuestas o logs, ni ningún payload;
  • el texto plano de TLS: el colector puede capturarlo, y la opción está apagada y bloqueada;
  • la dirección de quien llama al proceso (los peers de entrada se convierten solo en un conteo);
  • el argv y las variables de entorno del proceso;
  • ninguna lectura de disco fuera del /proc del propio objetivo.

Se escribe en el control-plane de WhyStack, aislada por organización como todo lo demás que guardamos: ningún tenant lee la firma de otro, y borrar la organización borra las firmas con ella.

El Profiler no guarda nada. Mide, envía y olvida. No hay base de datos dentro del artefacto privilegiado, por eso reiniciarlo no pierde ni acumula nada.

Lo que interpreta la firma es un servicio nuestro que corre junto al control-plane. Recibe los documentos en la solicitud, calcula la comparación y responde: sin base de datos, sin ninguna de tus credenciales y sin retener lo que recibió.

La firma no alimenta ningún acervo compartido entre clientes. Lo que WhyStack promueve entre tenants es anonimizado y categórico (roles de servicio, tipos de métrica); una firma de comportamiento lleva símbolos y rutas de tu código, así que se queda donde se escribió.

Un archivo JSON, al que apunta RUNNER_PROFILE_TARGETS_FILE, montado en el contenedor:

{
"targets": [
{ "workload": "checkout-api" },
{ "namespace": "production", "workload": "pricing-engine" }
]
}

namespace es un concepto de Kubernetes y es opcional: en Docker no existe.

Tres cosas ante las que el Profiler se niega a arrancar, en lugar de levantarse y capturar lo que no debería:

  • archivo ausente: no hay un valor por defecto que signifique “todo lo que hay en el nodo”;
  • lista vacía ("targets": []): un deployment que se cree configurado y no observa nada;
  • "workload": "*": un comodín es la omisión con otro nombre.

El schema es estricto: cualquier clave que no conozca hace fallar el boot. Eso incluye un campo de comentario bien intencionado. Si la lista necesita explicación, ponla en un archivo al lado.

Un archivo y no una variable de entorno, a propósito: en tu repositorio de infraestructura se le puede hacer diff, pasa por revisión y alguien lo mira. Una lista enterrada en un values.yaml no la revisa nadie, y esta es literalmente la lista de dónde puede mirar el contenedor privilegiado.

Dos cosas, y se comportan de forma muy distinta.

El volumen es la mitad fácil. Cada captura produce ~30 KB: la firma es un agregado, no eventos, y su tamaño no crece con la duración de la ventana.

El costo de CPU depende de una elección que haces tú, y la elección es el probe de asignación de memoria. Lo medimos, y la respuesta honesta es una tabla:

Asignaciones por segundo del proceso Todos los probes Sin el probe de heap
11 mil +14.8% +0.1%
114 mil +109.9% +0.2%
1.2 millones +404.4% −0.9%
14.4 millones +3214% +2.3%

Tiempo de CPU del objetivo por unidad de trabajo. aarch64 de 2 núcleos, kernel 7.0, piso de ruido medido de ±4.9%. Datos y harness en bench/ en el repositorio del colector.

Lee primero la segunda columna: todo lo demás es gratis, a cualquier tasa. Llamadas al sistema, contención de locks, red, CPU: nada de eso aparece en la medición. El probe de heap es todo el costo, y cuesta en proporción a cuánta memoria asigna tu proceso.

Es el eje que trae func y file:line, que es lo que permite señalar la línea cuyo comportamiento cambió, en lugar de solo que el comportamiento cambió. Es el más caro porque es el único que se dispara una vez por cada asignación.

Capturar con él desactivado conserva las llamadas al sistema, la forma de la contención, la red y la CPU: sigues sabiendo qué cambió, y renuncias a dónde. Para un servicio que asigna mucha memoria, ese es probablemente el intercambio correcto; para uno que asigna poca, activarlo cuesta poco y rinde bien.

La recomendación práctica: actívalo por servicio, no de forma global, y empieza con uno no crítico para medir en tu propio entorno antes de decidir.

Una distinción que vale la pena hacer aquí, porque las dos cosas se parecen: el costo lo eliges tú, el privilegio no. Desactivar el probe de heap en un servicio ahorra la CPU de ese servicio; no cambia lo que el Profiler necesita para arrancar. Los cinco permisos de la tabla se exigen de todos modos (incluso a quien nunca vaya a activar el heap), porque eso es lo que mantiene todas tus capturas comparables entre sí.

Encenderlo es una decisión registrada, y no solo nuestra. Un Profiler solo obtiene un certificado de la clase privilegiada si tu organización tiene el Profiler habilitado en el control-plane. Sin eso, el enrollment se rechaza, y el rechazo es el comportamiento por defecto: parte apagado. Tanto la activación como la desactivación quedan en tu log de auditoría, con quién y cuándo.

La clase vive en el certificado, decidida por nosotros contra esa política en el enrollment. No es un campo que el proceso declara. Un Agent no puede anunciarse como Profiler ni siquiera por un error de configuración, y un Profiler que pierde el permiso deja de renovar.

Para apagarlo:

Desde el control-plane: desactiva el Profiler en la configuración de tu organización. Esto no tumba el contenedor de inmediato: su certificado es válido por minutos y la renovación pasa por la misma barrera, así que se detiene al final del certificado actual. La pantalla indica la ventana.

Desinstalándolo: elimina el deployment. La recolección se detiene con él, y WhyStack sigue entero: nada más se ve afectado. Las capturas que ya tienes siguen siendo legibles, porque lo que las guarda es el control-plane y no el Profiler. Las tools de profiling siguen en la superficie del agente y empiezan a responder que ese proceso no tiene captura.

Quitando el proceso de la lista de objetivos: el Profiler rechaza todo lo que no esté en la lista y nombra el objetivo rechazado en su log. Es la más fina de las tres: apaga un proceso sin apagar los demás, y la lista es tuya, en tu infraestructura.

No existe ningún camino por el cual nosotros encendamos el Profiler. Es un artefacto que tú despliegas, con una lista de objetivos que tú escribes, bajo un permiso que tú otorgas.