GNU/Linux >> Tutoriels Linux >  >> Linux

Programmation TCP Socket hautes performances en .NET C#

Parce que cette question reçoit beaucoup de vues, j'ai décidé de publier une "réponse", mais techniquement ce n'est pas une réponse, mais ma conclusion finale pour l'instant, donc je la marquerai comme réponse.

À propos des approches :

Le async/await les fonctions ont tendance à produire de l'asynchronisme en attente Tasks affecté au TaskScheduler du runtime dotnet, donc avoir des milliers de connexions simultanées, donc des milliers d'opérations de lecture/écriture démarreront des milliers de tâches. Autant que je sache, cela crée des milliers de StateMachines stockées dans la RAM et d'innombrables changements de contexte dans les threads auxquels ils sont affectés, ce qui entraîne une surcharge CPU très élevée. Avec quelques connexions/appels asynchrones, il est mieux équilibré, mais à mesure que le nombre de tâches attendues augmente, il ralentit de manière exponentielle.

Le BeginReceive/EndReceive/BeginSend/EndSend les méthodes socket sont techniquement des méthodes asynchrones sans tâches en attente, mais avec des rappels à la fin de l'appel, ce qui optimise en fait davantage le multithreading, mais la limitation de la conception dotnet de ces méthodes socket est médiocre à mon avis, mais pour des solutions simples (ou un nombre limité de connexions), c'est la voie à suivre.

Le SocketAsyncEventArgs/ReceiveAsync/SendAsync Le type d'implémentation de socket est le meilleur sous Windows pour une raison. Il utilise Windows IOCP en arrière-plan pour obtenir les appels de socket asynchrones les plus rapides et utiliser les E/S superposées et un mode de socket spécial. Cette solution est la "plus simple" et la plus rapide sous Windows. Mais sous mono/linux, ce ne sera jamais aussi rapide, car mono émule l'IOCP de Windows en utilisant linux epoll , qui est en fait beaucoup plus rapide qu'IOCP, mais il doit émuler l'IOCP pour obtenir la compatibilité dotnet, ce qui entraîne une surcharge.

À propos des tailles de tampon :

Il existe d'innombrables façons de gérer les données sur les sockets. La lecture est simple, les données arrivent, vous en connaissez la longueur, il vous suffit de copier des octets du tampon de socket vers votre application et de les traiter. L'envoi de données est un peu différent.

  • Vous pouvez transmettre vos données complètes au socket et il les coupera en morceaux, copiera les mandrins dans le tampon du socket jusqu'à ce qu'il n'y ait plus rien à envoyer et la méthode d'envoi du socket reviendra lorsque toutes les données seront envoyées (ou lorsqu'une erreur se produit).
  • Vous pouvez prendre vos données, les couper en morceaux et appeler la méthode d'envoi de socket avec un morceau, et quand elle revient, envoyer le morceau suivant jusqu'à ce qu'il n'y en ait plus.

Dans tous les cas, vous devez considérer la taille de tampon de socket que vous devez choisir. Si vous envoyez une grande quantité de données, plus le tampon est grand, moins il faut envoyer de morceaux, donc moins d'appels dans votre boucle (ou dans la boucle interne du socket) doivent être appelés, moins de copie de mémoire, moins de surcharge. Mais l'allocation de mémoires tampons de socket et de données de programme volumineuses entraînera une utilisation importante de la mémoire, en particulier si vous avez des milliers de connexions, et l'allocation (et la libération) de mémoire importante plusieurs fois est toujours coûteuse.

Côté envoi, une taille de mémoire tampon de socket de 1-2-4-8 Ko est idéale dans la plupart des cas, mais si vous vous préparez à envoyer régulièrement des fichiers volumineux (plus de quelques Mo), une taille de mémoire tampon de 16-32-64 Ko est la solution. Plus de 64 Ko, il n'y a généralement pas de raison d'y aller.

Mais cela n'a d'avantage que si le côté récepteur dispose également de tampons de réception relativement importants.

Habituellement, via les connexions Internet (pas le réseau local), il est inutile d'obtenir plus de 32 Ko, même 16 Ko est idéal.

Passer en dessous de 4 à 8 Ko peut entraîner une augmentation exponentielle du nombre d'appels dans la boucle de lecture/écriture, entraînant une charge importante du processeur et un ralentissement du traitement des données dans l'application.

Ne dépassez 4 Ko que si vous savez que vos messages seront généralement inférieurs à 4 Ko, ou très rarement supérieurs à 4 Ko.

Ma conclusion :

En ce qui concerne mes expériences, les classes/méthodes/solutions de socket intégrées dans dotnet sont correctes, mais pas efficaces du tout. Mes simples programmes de test Linux C utilisant des sockets non bloquants pourraient surpasser la solution la plus rapide et "haute performance" des sockets dotnet (SocketAsyncEventArgs ).

Cela ne signifie pas qu'il est impossible d'avoir une programmation de socket rapide dans dotnet, mais sous Windows, j'ai dû faire ma propre implémentation de Windows IOCP en communiquant directement avec le noyau Windows via InteropServices/Marshaling, appelant directement les méthodes Winsock2 , en utilisant beaucoup de codes non sécurisés pour transmettre les structures de contexte de mes connexions en tant que pointeurs entre mes classes/appels, en créant mon propre ThreadPool, en créant des threads de gestionnaire d'événements IO, en créant mon propre TaskScheduler pour limiter le nombre d'appels asynchrones simultanés pour éviter inutilement beaucoup changements de contexte.

C'était beaucoup de travail avec beaucoup de recherches, d'expériences et de tests. Si vous voulez le faire par vous-même, faites-le uniquement si vous pensez vraiment que cela en vaut la peine. Mélanger du code non sécurisé / non géré avec du code géré est une douleur dans le cul, mais la fin en vaut la peine, car avec cette solution, je pouvais atteindre avec mon propre serveur http environ 36 000 requêtes http / s sur un réseau local de 1 Gbit, sous Windows 7, avec un i7 4790.

C'est une performance tellement élevée que je n'aurais jamais pu atteindre avec les sockets dotnet intégrés.

Lors de l'exécution de mon serveur dotnet sur un i9 7900X sous Windows 10, connecté à un NAS Intel Atom 4c / 8t sous Linux, via 10gbit lan, je peux utiliser toute la bande passante (donc copier les données avec 1 Go / s) peu importe si je n'ai que 1 ou 10000 connexions simultanées.

Ma bibliothèque de sockets détecte également si le code s'exécute sous Linux, puis au lieu de Windows IOCP (évidemment), il utilise des appels de noyau Linux via InteropServices/Marshalling pour créer, utiliser des sockets et gérer les événements de socket directement avec Linux epoll, réussi à maximiser les performances des machines de test.

Conseil de conception :

Il s'est avéré qu'il est difficile de concevoir une bibliothèque réseau à partir de zéro, en particulier une bibliothèque qui est probablement très universelle à toutes fins utiles. Vous devez le concevoir pour avoir de nombreux paramètres, ou en particulier pour la tâche dont vous avez besoin. Cela signifie trouver les tailles de tampon de socket appropriées, le nombre de threads de traitement d'E/S, le nombre de threads de travail, le nombre de tâches asynchrones autorisées, tout cela doit être réglé sur la machine sur laquelle l'application s'exécute et sur le nombre de connexions et le type de données que vous souhaitez transférer via le réseau. C'est pourquoi les sockets intégrés ne sont pas très performants, car ils doivent être universels, et ils ne vous permettent pas de définir ces paramètres.

Dans mon cas, l'affectation de plus de 2 threads dédiés au traitement des événements d'E/S aggrave en fait les performances globales, car l'utilisation de seulement 2 files d'attente RSS et provoque plus de changements de contexte que ce qui est idéal.

Le choix de mauvaises tailles de tampon entraînera une perte de performances.

Comparez toujours différentes implémentations pour la tâche simulée. Vous devez déterminer quelle solution ou quel paramètre est le meilleur.

Différents paramètres peuvent produire des résultats de performances différents sur différentes machines et/ou systèmes d'exploitation !

Mono vs Dotnet Core :

Depuis que j'ai programmé ma bibliothèque de sockets d'une manière compatible FW/Core, j'ai pu les tester sous linux avec mono et avec une compilation native core. Plus intéressant encore, je n'ai pas pu observer de différences de performances remarquables, les deux étaient rapides, mais bien sûr, laisser mono et compiler dans le noyau devrait être la voie à suivre.

Conseil de performance bonus :

Si votre carte réseau est capable de RSS (Receive Side Scaling), activez-la dans Windows dans les paramètres du périphérique réseau dans les propriétés avancées, et définissez la file d'attente RSS de 1 à aussi haut que possible/aussi haut est le meilleur pour vos performances.

S'il est pris en charge par votre carte réseau, il est généralement défini sur 1, ce qui affecte l'événement réseau à traiter uniquement par un cœur de processeur par le noyau. Si vous pouvez incrémenter ce nombre de files d'attente à des nombres plus élevés, cela répartira les événements réseau entre plusieurs cœurs de processeur et se traduira par de bien meilleures performances.

Sous Linux, il est également possible de le configurer, mais de différentes manières, il est préférable de rechercher les informations de votre pilote de distribution/lan linux.

J'espère que mon expérience aidera certains d'entre vous !


J'ai eu le même problème. Vous devriez jeter un œil à :NetCoreServer

Chaque thread du pool de threads .NET clr peut gérer une tâche à la fois. Donc, pour gérer plus de connexions/lectures asynchrones, etc., vous devez modifier la taille du pool de threads en utilisant :

ThreadPool.SetMinThreads(Int32, Int32)

L'utilisation d'EAP (modèle asynchrone basé sur des événements) est la voie à suivre sous Windows. Je l'utiliserais aussi sous Linux à cause des problèmes que vous avez mentionnés et je franchirais le pas des performances.

Le mieux serait les ports de complétion io sous Windows, mais ils ne sont pas portables.

PS :lorsqu'il s'agit de sérialiser des objets, nous vous encourageons fortement à utiliser protobuf-net . Il sérialise les objets binaires jusqu'à 10 fois plus rapidement que le sérialiseur binaire .NET et économise également un peu d'espace !


Linux
  1. Guide pratique :programmation de sockets en Python

  2. Quelle est la différence entre AF_INET et PF_INET dans la programmation de socket ?

  3. Effet de SO_SNDBUF

  4. requête arp et réponse à l'aide de la programmation par socket c

  5. Connexion à un socket UNIX déjà établi avec node.js ?

Comment connecter NGINX à PHP-FPM à l'aide d'UNIX ou d'un socket TCP/IP

Comment sécuriser le socket TCP de Docker avec TLS

Comment :Introduction à la programmation - Variables, types et manipulation de données

Créer une connexion de port série virtuel sur TCP

Est-il possible d'exposer le tunnel TCP sous Linux en tant que périphérique de caractères spécial ?

Pourquoi < ou > sont-ils nécessaires pour utiliser /dev/tcp