Un cache de inodos bajó el costo de CPU de eBPF en un 90% y la lección es de manual

Un cache de inodos bajó el costo de CPU de eBPF en un 90% y la lección es de manual

Un cache de inodos bajó el costo de CPU de eBPF en un 90% y la lección es de manual

Hace unos días apareció en Hacker News un post que me gustó harto: unos hermanos que mantienen un agente de seguridad escrito en eBPF se fijaron que la parte más cara de su código no era decidir si permitir o bloquear un archivo, sino figurarse qué política le aplica. La solución fue un cache de inodos y el costo de CPU del kernel bajó de 28 mil millones de ciclos a unos 3 mil millones en su benchmark. Un 90% menos. Y ojo: no lo generó una IA, lo resolvieron a mano perfilando con perf.

El problema clásico de siempre

El escenario es típico. Su agente usa un hook LSM de eBPF que se dispara cada vez que se abre un archivo. Como las políticas son por ruta, tienen que reconstruir el path, subir por los dentries padres y revisar en cada nivel si hay una regla que aplique. Eso significa que si Postgres abre 20 archivos bajo /var/lib/postgres/data, recorren el mismo árbol 20 veces. Trabajo repetido a lo tonto, pagado en ciclos de kernel.

La arregla es de manual pero bien ejecutada: un mapa LRU de eBPF con clave (mount namespace, mount ID, número de inode) y de valor el índice de política ya resuelto. Primer acceso: camino lento, se calcula y se guarda. Siguientes accesos al mismo archivo: lookup directo y listo. En los flamegraphs las funciones de recorrido de rutas pasan de dominar el stack a un 0,02%, prácticamente invisibles.

Los detalles que valen

Lo que me parece más rescatable del post no es el número bonito, sino los casos borde que tuvieron que considerar:

Inodos no son rutas. Con hardlinks, dos paths distintos comparten un mismo inode. Cacheear eso podría dar resultados incorrectos, y en seguridad la exactitud manda sobre el rendimiento. Su solución: leer el link count y si es mayor a 1, botar el cache y tomar el camino lento. Un workaround honesto, no una solución elegante, y lo dicen explícitamente.

No se puede cachear el inode solo. Los números de inode son únicos dentro de un mount, no a nivel de máquina. Por eso la clave incluye el mount ID y el namespace de montaje. Detalles así son los que separan un cache que funciona de uno que te muerde en producción.

La lección para el resto de nosotros

Tres takeaways que me llevo de acá:

Primero, perfilar antes de optimizar sigue siendo la jugada correcta en 2026. Ellos descubrieron que el problema no era el hook de eBPF en sí, sino el recorrido de dentries repetido. Sin flamegraphs habrían optimizado la parte equivocada.

Segundo, en sistemas de seguridad puedes permitirte un cache solo si puedes garantizar que jamás da una respuesta incorrecta. Su regla de «si el inode tiene más de un link, no cacheo» es exactamente el tipo de trade-off que hay que hacer explícito, no esconder debajo de la alfombra.

Tercero, los mapas de eBPF son limitados (no puedes guardar punteros a dentries, por ejemplo), y esas restricciones te empujan a diseños más simples y más rápidos. A veces la limitación de la plataforma es una bendición disfrazada.

El proyecto está open source en GitHub, así que todo el post es replicable con el código en la mano. Si estás metido en observabilidad, seguridad de endpoints o simplemente te gusta el rendimiento de kernel, el post completo con los flamegraphs antes/después vale la lectura completa. A mí me dejó con ganas de sacar perf en algunos scripts eBPF propios que seguro tienen la misma paja escondida.

Fuente de inspiración: Dropping eBPF CPU Cost by About 90% With Memoization (Not AI Gen)

Comentarios

Aún no hay comentarios. ¿Por qué no comienzas el debate?

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *