Je suis en train de développer une application sous Linux qui devra supporter environ 250 connexions et transmettre de gros fichiers sur des sockets TCP dans la gamme de taille 100MB+. Le but est de régler le débit plutôt que la latence. Je veux garder des connexions ethernet 2x1Gbit saturées à tout moment. Celles-ci seront liées à un canal.
Les entrées-sorties sur disque sont généralement plus lentes que sur le réseau. 250 clients, ce n'est rien pour les processeurs modernes.
Et la taille des fichiers n'est pas importante. La vraie question est de savoir si la quantité totale de données s'adapte à la RAM ou non - et si la RAM peut être étendue pour que les données s'y adaptent. Si les données rentrent dans la RAM, alors ne vous embêtez pas à sur-optimiser : un serveur bête à un seul fil avec sendfile()
ferait l'affaire.
Le SSD doit être envisagé pour le stockage, surtout si la lecture des données est la priorité.
On s'attend à ce que l'application soit occupée en permanence et qu'elle envoie des données aussi rapidement que possible. Les connexions resteront actives la plupart du temps et, contrairement à HTTP, elles ne seront pas interrompues aussi souvent.
"Aussi vite que possible" est une recette pour un désastre. Je suis responsable d'au moins un tel désastre multithread qui ne peut tout simplement pas évoluer en raison de la quantité de recherches sur disque qu'il entraîne.
En général, on peut vouloir avoir quelques (par exemple, 4) fils de lecture de disque par stockage qui appelleraient read()
o sendfile()
pour les très gros blocs afin que le système d'exploitation ait la possibilité d'optimiser les entrées-sorties. Peu de threads sont nécessaires car on veut être optimiste sur le fait que certaines données peuvent être servies à partir du cache IO du système d'exploitation en parallèle.
N'oubliez pas de définir également un tampon d'envoi de socket important. Dans votre cas, il est également utile de vérifier la possibilité d'écrire sur la socket : si le client ne peut pas recevoir aussi vite que vous pouvez lire/envoyer, il est inutile de lire. Le canal réseau de votre serveur est peut-être gros, mais les cartes réseau/disques des clients ne le sont pas.
J'ai examiné les différentes options telles que epoll, sendfile api etc. pour les hautes performances et aio (qui semble trop immature et risqué IMHO).
Pratiquement tous les serveurs FTP utilisent désormais sendfile()
. Oracle utilise AIO et Linux est leur principale plate-forme.
J'ai également examiné l'api boost asio qui utilise epoll en dessous. Je l'ai déjà utilisé mais pas pour une application de haute performance comme celle-ci.
IIRC c'est seulement pour les douilles. IMO : tout utilitaire qui facilite la manipulation des sockets est bon.
J'ai plus de 4 cœurs de processeur disponibles, je peux donc en faire usage.
Le TCP est accéléré par les NIC et l'IO des disques est en grande partie réalisée par les contrôleurs eux-mêmes. Idéalement, votre application devrait être inactive et attendre l'entrée en communication du disque.
Cependant, j'ai lu que le boost asio n'est pas très bon avec des fils multiples à cause d'un certain verrouillage dans la conception du réacteur. Est-ce que cela risque d'être un problème pour moi ?
Vérifiez le libevent comme alternative. Le nombre limité de fils dont vous aurez probablement besoin uniquement pour sendfile()
. Et le nombre doit être limité, car sinon vous détruisez le débit avec les recherches sur le disque.
Si je dispose de plusieurs cœurs de processeur, dois-je créer autant de threads ou de processus bifurqués et les configurer pour qu'ils fonctionnent sur chaque cœur de processeur ?
Non. Les disques sont les plus affectés par les recherches. (Ai-je répété cela suffisamment de fois ?) Et si vous aviez de nombreux fils de lecture autonomes, vous perdriez la possibilité de contrôler les entrées-sorties qui sont envoyées au disque.
Considérez le pire des cas. Tous les read()s
doit aller sur le disque == plus de threads, plus de recherches sur le disque.
Considérez le meilleur cas. Tous les read()s
sont servis à partir du cache == aucune entrée/sortie. Vous travaillez alors à la vitesse de la RAM et n'avez probablement pas besoin de threads (la RAM est plus rapide que le réseau).
Qu'en est-il du verrouillage, etc. J'aimerais avoir des suggestions de conception. Je pense que mon principal goulot d'étranglement sera l'entrée/sortie du disque, mais néanmoins...
C'est une question dont la réponse est très très longue et qui n'a pas sa place ici (et je n'ai pas le temps de l'écrire). Et cela dépend aussi largement de la quantité de données que vous allez servir, du type de stockage que vous utilisez et de la manière dont vous accédez au stockage.
Si nous prenons le SSD comme stockage, alors n'importe quelle conception stupide (comme démarrer un fil pour chaque client) fonctionnerait bien. Si vous avez de vrais supports rotatifs en arrière-plan, vous devez alors découper et mettre en file d'attente les demandes d'E/S des clients, en essayant d'une part d'éviter d'affamer les clients et d'autre part de planifier les E/S de manière à provoquer le moins de recherches possible.
Personnellement, j'aurais commencé par un simple design monofilaire avec poll() (ou boost.asio ou libevent) dans la boucle principale. Si les données sont mises en cache, il n'y a aucun intérêt à démarrer un nouveau thread. Si les données doivent être récupérées sur le disque, la conception monofilaire permet d'éviter les recherches. Remplir le tampon de la socket avec les données lues et passer en mode POLLOUT pour savoir quand le client a consommé les données et est prêt à recevoir le prochain morceau. Cela signifie que j'aurais au moins trois types de sockets dans la boucle principale : socket d'écoute, socket client dont j'attends la requête, socket client dont j'attends qu'il redevienne accessible en écriture.
Je veux un bon design dès le départ, sans avoir à le retravailler par la suite.
Ah... de beaux rêves......