Servidor de socket C ++: no se puede saturar la CPU

Desarrollé un mini servidor HTTP en C ++, usando boost :: asio, y ahora estoy cargando la prueba con múltiples clientes y no he podido acercarme a la saturación de la CPU. Estoy probando en una instancia de Amazon EC2 y obteniendo aproximadamente el 50% de uso de una CPU, el 20% de otra, y las otras dos están inactivas (según htop).

Detalles:

  • El servidor activa un hilo por núcleo
  • Las solicitudes se reciben, analizan, procesan y se escriben las respuestas
  • Las solicitudes son para datos, que se leen de memoria (solo lectura para esta prueba)
  • Estoy ‘cargando’ el servidor usando dos máquinas, cada una ejecutando una aplicación java, ejecutando 25 hilos, enviando solicitudes
  • Veo unas 230 solicitudes / seg de rendimiento (esto es solicitudes de aplicaciones , que se componen de muchas solicitudes HTTP)

Entonces, ¿qué debería mirar para mejorar este resultado? Dado que la CPU está prácticamente inactiva, me gustaría aprovechar esa capacidad adicional para obtener un mayor rendimiento, digamos 800 solicitudes / seg. O lo que sea.

Ideas que he tenido:

  • Las solicitudes son muy pequeñas, y a menudo se cumplen en unos pocos ms, podría modificar el cliente para enviar / redactar solicitudes más grandes (tal vez mediante el procesamiento por lotes)
  • Podría modificar el servidor HTTP para usar el patrón Seleccionar diseño, ¿es apropiado aquí?
  • Podría hacer algunos perfiles para tratar de entender cuáles son los cuellos de botella.

boost :: asio no es tan fácil de usar como cabría esperar – hay un gran locking alrededor del código epoll en boost / asio / detail / epoll_reactor.hpp lo que significa que solo un hilo puede llamar al epoll syscall del kernel a la vez . Y para solicitudes muy pequeñas, esto hace toda la diferencia (lo que significa que solo verá el rendimiento aproximado de un único subproceso).

Tenga en cuenta que esto es una limitación de cómo boost :: asio utiliza las funciones del kernel de Linux, no necesariamente el núcleo de Linux en sí. El epoll syscall admite varios subprocesos cuando se usan eventos desencadenados por flancos, pero hacerlo bien (sin un locking excesivo) puede ser bastante complicado.

Por cierto, he estado trabajando un poco en esta área (combinando un ciclo de eventos epoll desencadenado por borde totalmente multiproceso con hebras / hilos progtwigdos por el usuario) e hice disponible algún código bajo el proyecto nginetd .

Como está utilizando EC2, todas las apuestas están desactivadas.

Pruébelo con hardware real, y luego podrá ver lo que está sucediendo. Tratar de hacer pruebas de rendimiento en máquinas virtuales es básicamente imposible.

Todavía no he averiguado para qué sirve EC2, si alguien lo descubre, házmelo saber.

230 solicitudes / seg parece muy bajo para tales solicitudes asincrónicas simples. Como tal, el uso de múltiples hilos probablemente sea una optimización prematura: haz que funcione correctamente y sintonizado en un solo hilo, y mira si aún los necesitas. Solo deshacerse del locking innecesario puede hacer que las cosas se aceleren.

Este artículo contiene algunos detalles y discusiones sobre las estrategias de E / S para el rendimiento del servidor web en el año 2003. ¿Alguien tiene algo más reciente?

De sus comentarios sobre la utilización de la red,
Parece que no tienes mucho movimiento de red.

3 + 2.5 MiB/sec está alrededor del parque de bolas de 50Mbps (comparado con su puerto de 1Gbps).

Diría que estás teniendo uno de los siguientes dos problemas:

  1. Carga de trabajo insuficiente (baja tasa de solicitud de sus clientes)
    • Bloqueo en el servidor (generación de respuesta interferida)

Mirando las notas de cmeerw y las cifras de utilización de la CPU
(al ralentí al 50% + 20% + 0% + 0% )
parece ser una limitación en la implementación de su servidor.
I segunda respuesta de cmeerw (+1).

ASIO está bien para tareas pequeñas y medianas, pero no es muy bueno para aprovechar la potencia del sistema subyacente. Tampoco lo son las llamadas de socket raw, o incluso IOCP en Windows, pero si tiene experiencia, siempre será mejor que ASIO. De cualquier manera, hay muchos gastos generales con todos esos métodos, solo más con ASIO.

Por lo que vale la pena. El uso de llamadas de socket sin procesar en mi HTTP personalizado puede atender solicitudes dinámicas de 800 K por segundo con un I7 de 4 núcleos. Está sirviendo desde la RAM, que es donde debe estar para ese nivel de rendimiento. En este nivel de rendimiento, el controlador de red y el sistema operativo consumen aproximadamente el 40% de la CPU. Utilizando ASIO puedo obtener alrededor de 50 a 100K solicitudes por segundo, su rendimiento es bastante variable y está principalmente vinculado en mi aplicación. La publicación de @cmeerw explica en gran parte por qué.

Una forma de mejorar el rendimiento es implementar un proxy UDP. Al interceptar solicitudes HTTP y luego enrutarlas a través de UDP a su servidor UDP-HTTP de fondo, puede omitir una gran cantidad de TCP en las stacks del sistema operativo. También puede tener interfaces que se conectan en UDP, lo cual no debería ser demasiado difícil. Una ventaja de un proxy HTTP-UDP es que le permite usar cualquier buena interfaz sin modificaciones, y puede cambiarlas a voluntad sin ningún impacto. Solo necesitas un par de servidores más para implementarlo. Esta modificación en mi ejemplo redujo el uso de la CPU del sistema operativo al 10%, lo que aumentó mis solicitudes por segundo a poco más de un millón en ese backend único. Y FWIW Siempre debe tener una configuración frontend-backend para cualquier sitio de rendimiento porque las interfaces pueden almacenar en caché los datos sin ralentizar el back-end de las solicitudes dinámicas más importantes.

El futuro parece ser escribir su propio controlador que implementa su propia stack de red para que pueda acercarse lo más posible a las solicitudes e implementar allí su propio protocolo. Lo cual probablemente no es lo que la mayoría de los progtwigdores quieren escuchar, ya que es más complicado. En mi caso, podría usar un 40% más de CPU y pasar a más de 1 millón de solicitudes dinámicas por segundo. El método de proxy UDP puede acercarlo al rendimiento óptimo sin necesidad de hacerlo, sin embargo, necesitará más servidores, aunque si está haciendo muchas solicitudes por segundo, generalmente necesitará múltiples tarjetas de red y múltiples interfaces para manejar el ancho de banda de modo que tenga un par de proxies UDP livianos no son un gran problema.

Espero que algo de esto te pueda ser útil.

¿Cuántas instancias de io_service tienes? Boost asio tiene un ejemplo que crea un io_service por CPU y los usa a la manera de RoundRobin.

Aún puede crear cuatro hilos y asignar uno por CPU, pero cada hilo puede sondear en su propio io_service.