GNU/Linux >> Tutoriales Linux >  >> Linux

Programación TCP Socket de alto rendimiento en .NET C#

Debido a que esta pregunta recibe muchas visitas, decidí publicar una "respuesta", pero técnicamente no es una respuesta, sino mi conclusión final por ahora, así que la marcaré como respuesta.

Sobre los enfoques:

El async/await las funciones tienden a producir asincronismo esperable Tasks asignado al TaskScheduler del tiempo de ejecución de dotnet, por lo que tener miles de conexiones simultáneas, por lo tanto, miles de operaciones de lectura/escritura iniciarán miles de tareas. Hasta donde yo sé, esto crea miles de StateMachines almacenadas en RAM e innumerables cambios de contexto en los subprocesos a los que están asignados, lo que genera una sobrecarga de CPU muy alta. Con unas pocas conexiones/llamadas asíncronas, está mejor equilibrado, pero a medida que crece el número de tareas pendientes, se ralentiza exponencialmente.

El BeginReceive/EndReceive/BeginSend/EndSend Los métodos de socket son técnicamente métodos asincrónicos sin tareas pendientes, pero con devoluciones de llamada al final de la llamada, lo que en realidad optimiza más el subprocesamiento múltiple, pero aún así la limitación del diseño dotnet de estos métodos de socket es pobre en mi opinión, pero para soluciones simples (o número limitado de conexiones) es el camino a seguir.

El SocketAsyncEventArgs/ReceiveAsync/SendAsync El tipo de implementación de socket es el mejor en Windows por una razón. Utiliza el IOCP de Windows en segundo plano para lograr las llamadas de socket asíncronas más rápidas y usar la E/S superpuesta y un modo de socket especial. Esta solución es la "más simple" y la más rápida bajo Windows. Pero bajo mono/linux, nunca será tan rápido, porque mono emula el IOCP de Windows usando linux epoll , que en realidad es mucho más rápido que IOCP, pero tiene que emular el IOCP para lograr la compatibilidad con dotnet, lo que genera cierta sobrecarga.

Acerca de los tamaños de búfer:

Hay innumerables formas de manejar datos en sockets. La lectura es sencilla, los datos llegan, usted conoce su longitud, simplemente copia los bytes del búfer del socket a su aplicación y los procesa. El envío de datos es un poco diferente.

  • Puede pasar sus datos completos al zócalo y los cortará en pedazos, copiará los mandriles al búfer del zócalo hasta que no haya más para enviar y el método de envío del zócalo regresará cuando se envíen todos los datos (o cuando ocurre un error).
  • Puede tomar sus datos, cortarlos en fragmentos y llamar al método de envío del socket con un fragmento y, cuando regrese, enviar el siguiente fragmento hasta que no quede más.

En cualquier caso, debe considerar qué tamaño de búfer de socket debe elegir. Si está enviando una gran cantidad de datos, entonces cuanto más grande sea el búfer, menos fragmentos se deben enviar, por lo tanto, se deben llamar menos llamadas en su bucle (o en el bucle interno del socket), menos copia de memoria, menos sobrecarga. Pero la asignación de búferes de socket grandes y búferes de datos de programa resultará en un uso de memoria grande, especialmente si tiene miles de conexiones, y asignar (y liberar) memoria grande varias veces siempre es costoso.

En el lado de envío, el tamaño de búfer de socket de 1-2-4-8kB es ideal para la mayoría de los casos, pero si se está preparando para enviar archivos grandes (más de unos pocos MB) regularmente, entonces el tamaño de búfer de 16-32-64kB es el camino a seguir. Más de 64kB generalmente no tiene sentido.

Pero esto solo tiene ventajas si el lado del receptor también tiene búferes de recepción relativamente grandes.

Por lo general, a través de las conexiones de Internet (no de la red local) no tiene sentido superar los 32 kB, incluso 16 kB es lo ideal.

Pasar por debajo de 4-8kB puede dar como resultado un recuento de llamadas incrementado exponencialmente en el bucle de lectura/escritura, lo que provoca una gran carga de CPU y un procesamiento de datos lento en la aplicación.

Vaya por debajo de 4kB solo si sabe que Sus mensajes generalmente tendrán un tamaño inferior a 4kB, o muy raramente superarán los 4KB.

Mi conclusión:

Con respecto a mis experimentos, la clase/métodos/soluciones de socket incorporados en dotnet están bien, pero no son eficientes en absoluto. Mis sencillos programas de prueba de Linux C que usan sockets sin bloqueo podrían superar la solución más rápida y de "alto rendimiento" de sockets dotnet (SocketAsyncEventArgs ).

Esto no significa que sea imposible tener una programación rápida de sockets en dotnet, pero bajo Windows tuve que hacer mi propia implementación de Windows IOCP comunicándome directamente con el Kernel de Windows a través de InteropServices/Marshaling, llamando directamente a los métodos de Winsock2 , usando muchos códigos inseguros para pasar las estructuras de contexto de mis conexiones como punteros entre mis clases/llamadas, creando mi propio ThreadPool, creando subprocesos de controlador de eventos IO, creando mi propio TaskScheduler para limitar el conteo de llamadas asíncronas simultáneas para evitar mucho sin sentido cambios de contexto.

Esto fue mucho trabajo con mucha investigación, experimentación y pruebas. Si quieres hacerlo por tu cuenta, hazlo solo si realmente crees que vale la pena. Mezclar código inseguro/no administrado con código administrado es una molestia, pero al final valió la pena, porque con esta solución pude alcanzar con mi propio servidor http alrededor de 36000 solicitudes http/seg en una LAN de 1 gbit, en Windows 7, con un i7 4790.

Este es un rendimiento tan alto que nunca podría alcanzar con los sockets integrados de dotnet.

Cuando ejecuto mi servidor dotnet en un i9 7900X en Windows 10, conectado a un Intel Atom NAS 4c/8t en Linux, a través de una red LAN de 10 gbit, puedo usar el ancho de banda completo (por lo tanto, copio datos con 1 GB/s) sin importar si solo tengo 1 o 10000 conexiones simultáneas.

Mi biblioteca de sockets también detecta si el código se está ejecutando en Linux, y luego, en lugar de Windows IOCP (obviamente), está usando llamadas al kernel de Linux a través de InteropServices/Marshalling para crear, usar sockets y manejar los eventos de socket directamente con linux epoll, logró maximizar el rendimiento de las máquinas de prueba.

Consejo de diseño:

Resultó que es difícil diseñar una biblioteca de redes desde cero, especialmente una, que probablemente sea muy universal para todos los propósitos. Debe diseñarlo para que tenga muchas configuraciones, o especialmente para la tarea que necesita. Esto significa encontrar los tamaños de búfer de socket adecuados, el recuento de subprocesos de procesamiento de E/S, el recuento de subprocesos de trabajo, el recuento de tareas asíncronas permitidas, todo esto tiene que estar sintonizado con la máquina en la que se ejecuta la aplicación y con el número de conexiones y el tipo de datos que desea transferir a través de la red. Esta es la razón por la que los enchufes integrados no funcionan tan bien, porque deben ser universales y no te permiten establecer estos parámetros.

En mi caso, asignar más de 2 subprocesos dedicados al procesamiento de eventos de E/S en realidad empeora el rendimiento general, ya que solo usa 2 colas RSS y provoca más cambios de contexto de lo ideal.

Elegir tamaños de búfer incorrectos resultará en una pérdida de rendimiento.

Siempre compare diferentes implementaciones para la tarea simulada. Debe averiguar qué solución o configuración es la mejor.

¡Diferentes configuraciones pueden producir diferentes resultados de rendimiento en diferentes máquinas y/o sistemas operativos!

Mono frente a Dotnet Core:

Como programé mi biblioteca de sockets de manera compatible con FW/Core, pude probarlos en Linux con mono y con compilación nativa central. Lo más interesante es que no pude observar ninguna diferencia de rendimiento notable, ambos fueron rápidos, pero, por supuesto, dejar mono y compilar en el núcleo debería ser el camino a seguir.

Consejo de rendimiento adicional:

Si su tarjeta de red es compatible con RSS (Receive Side Scaling), habilítelo en Windows en la configuración del dispositivo de red en las propiedades avanzadas, y establezca la Cola RSS de 1 a lo más alto que pueda/tan alto como sea mejor para su rendimiento.

Si es compatible con su tarjeta de red, generalmente se establece en 1, esto asigna el evento de red para procesar solo por un núcleo de CPU por el kernel. Si puede incrementar este recuento de colas a números más altos, distribuirá los eventos de red entre más núcleos de CPU y dará como resultado un rendimiento mucho mejor.

En linux también es posible configurar esto, pero de diferentes maneras, es mejor buscar la información del controlador de su distro/lan de linux.

¡Espero que mi experiencia ayude a algunos de ustedes!


Yo tuve el mismo problema. Debería echar un vistazo a:NetCoreServer

Cada subproceso en el grupo de subprocesos .NET clr puede manejar una tarea a la vez. Entonces, para manejar más conexiones/lecturas asíncronas, etc., debe cambiar el tamaño del grupo de subprocesos usando:

ThreadPool.SetMinThreads(Int32, Int32)

El uso de EAP (patrón asíncrono basado en eventos) es el camino a seguir en Windows. Yo también lo usaría en Linux debido a los problemas que mencionaste y daría un salto en el rendimiento.

Lo mejor sería puertos de finalización io en Windows, pero no son portátiles.

PD:cuando se trata de serializar objetos, le recomendamos que utilice protobuf-net . ¡Serializa objetos binarios hasta 10 veces más rápido que el serializador binario .NET y también ahorra un poco de espacio!


Linux
  1. Averiguar si se entregó un mensaje a través de tcp

  2. Efecto de SO_SNDBUF

  3. ¿Conectarse a un socket UNIX ya establecido con node.js?

  4. ¿Cómo uso System.Data en una aplicación de consola .NET Core RC2 (Linux, Debian 8)?

  5. ¿Cuál es la forma más fácil de detectar datos de tráfico TCP en Linux?

Cómo conectar NGINX a PHP-FPM usando UNIX o TCP/IP Socket

Cómo proteger el socket TCP de Docker con TLS

Cómo:Introducción a la programación:variables, tipos y manipulación de datos

¿Es posible exponer el túnel TCP en Linux como dispositivo de caracteres especiales?

Unix socket vs host TCP/IP:puerto

Configuración de TCP de baja latencia en Ubuntu