3× Más Conexiones. El Mismo Núcleo. Dentro de la Investigación de Rendimiento Detrás del Motor Accept de FlashProxy

Cómo FlashProxy utilizó un optimizador de IA autónomo para descubrir un motor de aceptación TCP que utiliza 3× menos instrucciones de CPU que Go. Código abierto y licenciado bajo MIT.
Existe una clase de problemas de rendimiento que solo emerge a escala, y es profundamente humillante cuando lo hace. No estás limitado por tu lógica de enrutamiento. Ni por el cifrado. Ni por nada que tu aplicación debería estar haciendo realmente. Estás limitado por la infraestructura que la rodea. Por la tubería.
Exactamente eso fue lo que nos sucedió.
Alrededor de 40 000 conexiones por segundo, nuestro proxy Go en producción comenzó a alcanzar los límites de CPU en la ruta de aceptación. El culpable fue el modelo estándar de Go para servicios de red: una gorrutina por conexión, una llamada al sistema por operación. Cada conexión de corta duración (un health check, una prueba de balanceador de carga, un pequeño redireccionamiento) toca el kernel cuatro veces. accept, read, write, close. Con una alta rotación de conexiones, esos cuatro viajes de ida y vuelta dejan de ser sobrecarga y pasan a ser todo el costo. El código de la aplicación es casi gratuito. Mover bytes dentro y fuera del kernel no lo es.
La solución que todos buscan es io_uring, la interfaz asincrónica de E/S de Linux que agrupa operaciones del kernel y reduce drásticamente el número de veces que tienes que cruzar el límite usuario/kernel. Lo sabíamos. La pregunta más difícil fue cómo optimizarlo.
Por Qué No Lo Optimizamos Nosotros Mismos
io_uring tiene una larga lista de optimizaciones documentadas: multishot accept, descriptores de archivo registrados, DEFER_TASKRUN, completion chains, coalescing MSG_MORE. El problema es que estas técnicas interactúan entre sí de formas genuinamente difíciles de predecir. Algunas combinaciones se refuerzan mutuamente. Algunas se anulan entre sí. Algunas regresiones son completamente invisibles hasta que haces benchmark bajo carga realista en hardware real.
Un desarrollador sentado con perf stat puede probar un puñado de combinaciones en un día. Pero el espacio de búsqueda real, cubriendo combinaciones, ordenamientos, valores de parámetros e interacciones de versiones de kernel, es mucho más grande que eso. Más importante aún, la intuición humana es un lastre aquí. Tendemos a aferrarnos a teorías que se ven bien en papel, incluso cuando los datos dicen lo contrario.
Así que construimos un bucle de medición cerrado y entregamos el problema de búsqueda a un agente de IA.
Cómo Funcionó el Bucle de Investigación
Configuramos dos implementaciones de servidor ejecutándose una al lado de la otra. El control era un servidor Go que modelaba exactamente la ruta de aceptación de nuestro proxy en producción: gorrutina por conexión, fan-out SO_REUSEPORT. Se mantuvo congelado durante todo el experimento. El tratamiento fue un servidor C usando liburing, comenzando desde un bucle de aceptación io_uring simple de una sola etapa. Ese era el único código que podía ser modificado.
Ambos servidores sirvieron el mismo contrato: aceptar una conexión, leer bytes de solicitud, escribir una respuesta HTTP 200 fija, cerrar. Mismo hardware, mismo kernel, misma carga. La única variable fue cómo la manejaron.
Claude Code se ejecutó sin interfaz gráfica como optimizador. En cada iteración leyó el campeón actual, el historial completo de mutaciones anteriores almacenadas como commits de git, una base de conocimientos de lecciones acumuladas entre ejecuciones, y una base de datos de datos de perfilado. Formó una única hipótesis e hizo su edición. Luego se desentendió completamente.
Un arnés bash separado, que la IA no podía tocar, construyó la mutación, la fijó a un núcleo de CPU aislado, ejecutó el generador de carga y puntuó el resultado. La fórmula de puntuación fue:
score = 1 000 000 000/mean(instrucciones por conexión)
Los conteos de instrucciones de CPU son una medida exacta de hardware. A diferencia de los números de rendimiento, son inmunes al estrangulamiento térmico y a la variación de frecuencia de reloj. Te dicen precisamente cuánto trabajo está haciendo la CPU por conexión, que es lo que realmente determina la capacidad a escala.
Si una mutación mejoraba la puntuación en más del 3 % sobre el campeón actual, era promovida. Si no, se eliminaba con git reset --hard y el bucle continuaba. La IA escribió las hipótesis. El arnés tomó cada decisión de mantener o revertir. Ninguno interfería en el trabajo del otro.
Para evitar que el optimizador jugara con el benchmark, cada ejecución puntuada fue validada: los bytes de respuesta tenían que ser exactamente correctos, las conexiones tenían que completarse de extremo a extremo, y la tasa de fallo tenía que mantenerse por debajo del 0,01 %. Cualquier violación puntuaba cero.
Lo Que Encontramos
El optimizador se ejecutó durante dos días y convergió en seis cambios que juntos explican toda la brecha de rendimiento.
Flags de anillo DEFER_TASKRUN y SINGLE_ISSUER. Estos mueven el trabajo de tareas de completación hacia el bucle de eventos propio del hilo de trabajo, eliminando despertares entre CPUs. Este fue el salto individual más grande en toda la ejecución, una reducción directa en el costo de CPU del kernel por operación, no un truco de rendimiento.
Descriptores de archivo registrados. Con descriptores directos, las conexiones aceptadas viven en la tabla del propio anillo en lugar de la tabla de descriptores de archivo del proceso. Esto omite la instalación de fd-table en accept y la búsqueda en cada operación posterior. El optimizador probó múltiples tamaños de tabla y encontró que 4 096 entradas era la configuración óptima.
Multishot accept. En lugar de reactivar la operación de accept después de cada conexión, la activas una vez y el kernel publica una completación para cada nueva conexión automáticamente. Esto redujo las llamadas io_uring_enter por conexión a 0,34.
Freelist de conexión por trabajador. Preasignar 128 objetos de conexión por trabajador eliminó completamente malloc en la ruta crítica. El porcentaje medido de instrucciones de CPU destinadas a libc se redujo del 1,32 % al 0,92 %.
Fusión de respuesta MSG_MORE y FIN. Enviar la respuesta con MSG_MORE la mantiene en la cola de escritura TCP para que el FIN de la conexión pueda subirse a ella, enviando ambos como un único segmento TCP. Un timbre de NIC en lugar de dos. El optimizador encontró esto notando que una función de escritura de bajo nivel del kernel estaba consumiendo el doble de su porcentaje esperado de instrucciones y rastreándolo de vuelta a la división de segmento innecesaria.
Completaciones procesadas por lotes con CQE_SKIP_SUCCESS. Etiquetar operaciones de envío y cierre para que no generen eventos de completación en caso de éxito significa que solo accept y receive producen completaciones, aproximadamente dos por conexión en lugar de cuatro. Una sola llamada submit-and-wait impulsa muchas conexiones simultáneamente.
El Fallo Que Más Enseñó
En un momento, el optimizador intentó encadenar las operaciones de receive, send y close, una técnica que hace que cada una se active automáticamente cuando la anterior se completa. El objetivo era reducir los viajes de ida y vuelta del kernel, y funcionó: las llamadas enter por conexión se redujeron de 1,90 a 1,40.
La puntuación cayó un 34 %. Se revirtió inmediatamente.
La lección que entró en la base de conocimientos: minimizar las entradas del kernel no es la palanca. La serialización en cadena cuesta más que las entradas que ahorra.
Este es exactamente el tipo de resultado que rompe la intuición humana. La métrica que parecía ser el cuello de botella no era el cuello de botella real. Un ingeniero probablemente habría defendido esa optimización más tiempo. El bucle la midió, la rechazó y continuó.
Dónde Se Detuvo la Búsqueda
Después de los seis éxitos, el optimizador exploró aproximadamente una docena de candidatos más. Todos fueron revertidos, no porque regresionaran, sino porque el ruido de medición superó el umbral de promoción del 3 %. No había nada más que encontrar.
En la configuración campeona, aproximadamente el 94 % del CPU restante pertenece a la pila TCP del kernel de Linux. Alrededor del 1 % es código de aplicación. Alrededor del 4 % aprovecha internals. No queda código de espacio de usuario para optimizar de manera significativa. La investigación identificó correctamente el piso y se detuvo en él.
Los Resultados
En un núcleo fijado, limitado por CPU, con 512 conexiones fijas en vuelo en loopback:
Go gorrutina por conexión: 83 250 instrucciones por conexión
Línea base inicial vanilla io_uring: 59 931 instrucciones por conexión
Motor accept de FlashProxy: 27 363 instrucciones por conexión
Eso es 3,04× menos instrucciones de CPU por conexión que Go, y 2,19× menos que la línea base io_uring. En un único núcleo saturado, eso se traduce en aproximadamente seis veces el rendimiento de conexión en comparación con el modelo de gorrutina.
Estos son benchmarks de loopback diseñados para aislar el costo de CPU limpiamente. Las ratios son lo que importa, no los números absolutos. Las técnicas subyacentes (multishot accept, DEFER_TASKRUN, descriptores registrados) están documentadas en los idioms de io_uring. Lo que la investigación produjo fue prueba de qué combinaciones realmente funcionan juntas, y cuáles se ven bien en papel pero te cuestan en la práctica.
La Biblioteca de Código Abierto
El diseño ganador es ahora flashaccept, una biblioteca C de código abierto que empaqueta las seis optimizaciones detrás de una API simple. Le das un puerto y un manejador de solicitudes. Ejecuta un bucle de aceptación io_uring optimizado por núcleo automáticamente.
Requiere Linux y liburing 2,3 o posterior. En kernels más antiguos se degrada elegantemente. El caso de uso previsto es conexiones de alto flujo y corta duración: health checks, redirectores, pruebas de balanceador de carga, pequeñas respuestas RPC. El rig de investigación completo, incluyendo la línea base, el arnés, la configuración del optimizador y la base de conocimientos completamente acumulada, está incluido y totalmente reproducible.
Licenciado bajo MIT. Disponible ahora en github.com/thealonlevi/flashaccept.
Esta investigación provino directamente de la construcción de la infraestructura proxy de FlashProxy. Si estás ejecutando un servicio Linux de alto flujo y la ruta de accept es tu cuello de botella, construimos esto exactamente para ese problema. Si quieres extenderlo o ejecutar el rig de investigación tú mismo, el repositorio tiene todo lo que necesitas.
Preguntas Frecuentes
¿Qué es Flashaccept?
Flashaccept es una biblioteca C de código abierto construida por FlashProxy que proporciona un motor de aceptación TCP de alto rendimiento para Linux. Utiliza io_uring bajo el capó y acepta conexiones con 3,04× menos instrucciones de CPU que un servidor estándar de gorrutina por conexión de Go. Está licenciado bajo MIT y disponible en GitHub.
¿Para qué cargas de trabajo está diseñado?
Flashaccept está construido para conexiones de alto flujo y corta duración donde el ciclo solicitud-respuesta-cierre ocurre en alto volumen: endpoints de health check, pruebas de balanceador de carga, redirectores HTTP y pequeñas respuestas RPC. No está diseñado para conexiones keep-alive o sesiones de múltiples intercambios en v1.
¿Qué versión de Linux y versión de liburing necesito?
Necesitas Linux con liburing versión 2,3 o posterior, que viene con Ubuntu 24,04 y posteriores. La ruta rápida (multishot accept, descriptores directos) requiere kernel 5,19 o más reciente. En kernels más antiguos la biblioteca se degrada elegantemente a accept de una sola etapa y descriptores de archivo regulares.
¿Qué es io_uring y por qué importa para el rendimiento de proxy?
io_uring es una interfaz de kernel de Linux introducida en el kernel 5,1 que permite a las aplicaciones enviar y recibir operaciones de E/S de manera asincrónica usando anillos de búfer de memoria compartida, reduciendo drásticamente el número de llamadas al sistema requeridas. Para una infraestructura proxy que maneja decenas de miles de conexiones de corta duración por segundo, el costo de cruzar el límite usuario/kernel repetidamente se convierte en el gasto de CPU dominante. io_uring colapsa ese costo significativamente.
¿Es flashaccept lo que FlashProxy ejecuta en producción?
La investigación provino directamente de nuestro trabajo de escalado en producción. La infraestructura proxy en FlashProxy opera en más de 190 países y maneja un volumen de conexión sustancial. La optimización de la ruta de accept fue un requisito de ingeniería real, no un ejercicio de investigación. flashaccept es el resultado destilado de ese trabajo, de código abierto para que otros puedan usarlo.
¿Puedo usar flashaccept hoy?
Sí. Es versión 1,0.1, licenciado bajo MIT, y disponible a través de GitHub, vcpkg, Conan y Arch AUR. La biblioteca ha sido probada bajo AddressSanitizer y UBSan en más de 400 000 conexiones en los cuatro caminos de configuración.
¿Dónde puedo aprender más sobre la infraestructura de FlashProxy?
Puedes leer más sobre cómo construimos y escalamos infraestructura proxy en el blog de FlashProxy o explorar nuestra red proxy directamente.


