Cómo AWS Lambda registra cada flujo en miles de microVMs por servidor usando eBPF y Rust

AWS Lambda ha desarrollado un sistema innovador que registra de forma precisa y eficiente cada comunicación de red en miles de microVMs por servidor, gracias a una arquitectura basada en eBPF y Rust que supera las limitaciones de su sistema anterior.

En cualquier plataforma informática, ante una alerta de seguridad, la pregunta fundamental siempre es la misma: ¿qué carga de trabajo se comunicó con ese endpoint, cuándo y cuánto intercambio de datos hubo? El problema es identificar qué buscar y dónde encontrarlo. En un solo servidor, miles de microVMs ejecutan procesos que duran apenas unos pocos cientos de milisegundos para luego apagarse. Cada microVM pertenece a un cliente distinto realizando una función específica, y los registros capturados durante esos escasos instantes son las únicas pruebas restantes.

Esta era la situación en AWS Lambda, donde se necesitaba un registro exhaustivo de cada flujo de red para cada inquilino, sin importar la duración del proceso ni la cantidad de tráfico generado. A continuación, explicamos cómo se sustituyó el sistema anterior por una solución diseñada a medida, basada en eBPF y Rust, poniendo fin a las limitaciones del sistema antiguo y permitiendo escalar acorde con las demandas masivas de Lambda.

El reto: un registro inalterable e imprescindible

Cada trabajador de Lambda es una instancia EC2 bare-metal que aloja numerosas microVMs aisladas mediante Firecracker. Estas microVMs se comunican con servicios como S3, otros servicios AWS, Internet pública y las redes privadas virtuales (VPC) de los clientes. Para garantizar la integridad y trazabilidad, es necesario llevar un registro completo y veraz de cada flujo de red.

Patrocinado

Este registro de flujos de red es esencial para investigaciones, respuesta ante incidentes, auditorías y reconstrucción de eventos. Es el sistema de referencia para saber qué ocurrió con cada paquete que atraviesa el sistema. Además, estos datos alimentan servicios de facturación y monitorización que requieren una precisión absoluta, y deben almacenarse para cumplimiento normativo y auditorías.

Las dos cualidades indispensables para estos registros son la completitud y la correcta atribución. No puede faltar ningún paquete y cada flujo debe asociarse de forma inequívoca a la microVM e inquilino que lo generó. En el contexto de Lambda, que maneja millones de solicitudes por segundo, perder o mal atribuir datos implica grandes problemas en facturación, observabilidad y monitorización, afectando la eficiencia y capacidad de servicio bajo demanda.

Por qué el sistema antiguo quedaba obsoleto

Al construir la nueva versión de Lambda basada en microVMs Firecracker multiinquilino, inicialmente se utilizó una solución heredada de un modelo EC2 más antiguo y de menor densidad. Consistía en una extensión del kernel que contaba paquetes y los asignaba a inquilinos y sandboxes, y un demonio en espacio de usuario que leía estos contadores, los agrupaba en registros, los serializaba y enviaba.

Este enfoque funcionaba bien para un pequeño número de microVMs, pero fallaba al aplicarlo a la densidad de Lambda por dos razones irremediables:

  • Explosión de reglas: iptables escanea sus reglas de forma lineal para cada paquete, y cada microVM añadía más reglas incrementando su número hasta superar las cien mil por host. Esto ralentizaba el procesamiento a medida que el servidor se saturaba, exactamente lo contrario a lo que se necesitaba para aumentar la densidad de microVMs.
  • Falta de soporte IPv6: el módulo de kernel heredado no era compatible con IPv6. Dado que Lambda debía soportar conexiones dual-stack, esta carencia invalidaba el modelo por completo, independientemente de posibles mejoras en rendimiento.

Nuevas bases de diseño: precisión, eficiencia y soporte IPv6

Al comenzar a reescribir el sistema, se definieron requisitos estrictos: atribución exacta de cada paquete, reducción significativa de la sobrecarga y soporte nativo para IPv6. También era vital mantener compatibilidad total con el formato Amazon Ion usado por sistemas internos para que no fuera necesario modificar el ecosistema consumidor de esos registros.

La clave para la correcta atribución fue implementar el aislamiento por microVM, ya que cada una dispone de su red propia — un namespace con dispositivos virtuales dedicados — lo que permitió organizar el registro alrededor de dicha unidad.

Arquitectura del nuevo sistema

El sistema se compone de tres elementos coordinados:

  • Capa de captura en kernel: usa eBPF para engancharse en el control de tráfico (tc) de los dispositivos virtuales de red de cada microVM, interceptando cada paquete para registrar un evento compacto en un buffer circular sin modificar ni bloquear el tráfico.
  • Procesos «taggers» en espacio usuario: un proceso no privilegiado por microVM, programado en Rust, que consume el buffer circular, agrupa eventos por flujo, atribuye correctamente a inquilinos y genera registros en formato Amazon Ion.
  • Orquestador: proceso privilegiado por host que carga los programas eBPF, configura tc, y gestiona el ciclo de vida de los taggers, exponiendo una API gRPC para controlar el inicio y parada de registros conforme las microVMs aparecen o desaparecen.

Esta separación permite capturar paquetes directamente en el kernel con mínima interferencia, realizar la agregación en usuariospace sin penalizar el rendimiento y mantener un único punto privilegiado para la coordinación y seguridad.

Captura sin atasco en el kernel

Los programas eBPF interceptan paquetes en las colas de tráfico (clsact qdisc) tanto de entrada como salida en cada dispositivo virtual de red, cuatro puntos por microVM. Al recibir un paquete, se parsean cabeceras Ethernet, IPv4/IPv6 y protocolos TCP, UDP o ICMP, y se genera un evento fijo (aprox. 24 bytes) que incluye versión IP, protocolo, dirección, puertos, flags TCP, dirección local y remota, y timestamp.

Este evento se introduce en un buffer circular dedicado, evitando duplicación de datos mediante syscall y limitando el consumo CPU. El kernel sólo produce eventos y nunca bloquea, modifica o descarta paquetes. Además, normaliza los campos locales/remotos para que usuario no deba diferenciar dirección del flujo.

Lograr esta complejidad manteniendo la aprobación del verificador eBPF fue crítico para garantizar seguridad y estabilidad, incluyendo manejo de cabeceras IPv6, fragmentos y super-packets, y usando pruebas formales en el código para evitar corrupción de registros.

Dimensionamiento del buffer circular

El tamaño del buffer circular compartido entre kernel y usuariospace es un compromiso clave. Debe ser suficientemente grande para no perder eventos en picos de tráfico, pero sin desperdiciar memoria que penalice el sistema en conjunto.

Se calculó el tamaño mínimo basándose en la tasa máxima esperada de paquetes por dirección y en la frecuencia con la que los procesos usuarios consumen el buffer (aprox. cada 100 ms). Para una tasa pico de unos 62.500 paquetes por segundo por dirección, se mide un espacio de 300 KB; ajustado a 512 KiB por necesidades de memoria alineada, aunque en producción se asignan unos pocos megabytes para margen.

La lectura se activa sólo cuando el buffer supera un 1 % de ocupación, evitando activaciones innecesarias y manteniendo bajo el consumo CPU.

Agregación en Rust para confiabilidad y eficiencia

Los taggers, escritos en Rust, agrupan eventos de paquetes en flujos, manteniendo conteos de bytes, paquetes y flags TCP. Rust aporta control de memoria sin recolector, lo que asegura latencias predecibles y evita errores de memoria comunes, fundamentales en miles de procesos corriendo simultáneamente con bajo consumo (alrededor de 1 MB de RAM cada uno).

Los taggers tienen un modelo asíncrono que lee eventos desde el buffer, actualiza estados de flujos y escribe registros Ion al disco en intervalos regulares y con garantías de consistencia. La meta es conservar compatibilidad byte a byte con registros anteriores para habilitar la integración con sistemas existentes sin alteraciones.

Seguridad y privilegios mínimos

Los taggers no tienen capacidades privilegiadas: no pueden cargar programas eBPF ni manipular tráfico. La única entidad con privilegios es el orquestador, responsable de la gestión centralizada. Este proceso abre los buffers de kernel y transfiere los descriptores de archivo a los taggers mediante una antigua técnica Unix (SCM_RIGHTS), posibilitando que procesos sin permisos puedan leer datos de captura sin riesgo.

Control del ciclo de vida de las microVMs

El orquestador gestiona el ciclo de vida del sistema mediante una API gRPC a través de un socket Unix, con métodos para crear, activar, reciclar y desmontar flujos según las microVMs se crean o destruyen. La creación es una tarea costosa (carga de programas eBPF y configuración), mientras que la activación es más rápida, con latencias inferiores a 2 ms en percentil 90, replicando el rendimiento del sistema previo.

Un compromiso necesario: reciclaje de procesos tagger

Inicialmente, para garantizar la ausencia total de mezcla de metadatos, cada reciclaje de flujo destruía el tagger y arrancaba uno nuevo. Sin embargo, la alta frecuencia de recambios generaba picos de CPU insostenibles a escala. Por ello, se añadió una opción para reutilizar taggers, sacrificando algo de rigidez estructural a cambio de estabilidad operativa. Ambos modos permanecen disponibles según necesidades.

Beneficios obtenidos

  • El problema del rendimiento lineal con iptables desaparece gracias a eBPF y mapas con acceso en tiempo constante, permitiendo duplicar la densidad de microVMs por host sin pérdida de rendimiento.
  • Se registra el tráfico IPv6 con la misma fidelidad que el IPv4, ampliando la cobertura sobre todo el espacio de direcciones.
  • Los procesos tagger consumen menos de 1 MB de RAM cada uno, permitiendo desplegar miles en el mismo host.
  • La latencia de activación se mantiene baja, bajo 2 ms en condiciones normales.
  • La capa de captura en kernel es observacional y formalmente verificada, y los procesos usuarios no tienen privilegios para modificar o corromper datos.
  • El formato de salida es idéntico byte a byte al sistema anterior, manteniendo compatibilidad total con los consumidores habituales.

Lecciones aplicables a otras plataformas

Esta experiencia tiene relevancia para Kubernetes, entornos edge, contenedores y sandboxes, especialmente donde múltiples inquilinos comparten un host y se requiere trazabilidad fiable del tráfico. Algunas conclusiones clave:

  • Capturar datos fuera del camino crítico del procesamiento es vital para minimizar el impacto al usuario.
  • Dimensionar buffers basándose en datos reales asegura evitar pérdidas bajo picos de actividad.
  • Separar flujos por inquilino desde el punto de captura simplifica la atribución y evita confusiones posteriores.
  • El uso de primitivas clásicas de Unix como la transferencia de descriptores ayuda a limitar privilegios y mejorar la seguridad.
  • Preservar la compatibilidad binaria facilita el reemplazo de sistemas sin necesidad de cambiar todo el ecosistema.

En definitiva, AWS Lambda demuestra cómo un diseño moderno y eficiente basado en eBPF y Rust puede superar los retos de escala y complejidad, proporcionando un registro de red completo, seguro y compatible que mantiene la calidad y confiabilidad crucial para operaciones en la nube a gran escala.

Add a Comment

Deja una respuesta

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

Patrocinado