Article du 10e jour du calendrier de l'avent Linux.
Dans le domaine de l'exploitation et de la recherche et développement, je pense qu'il existe de nombreuses opportunités pour mesurer la vitesse de communication entre ordinateurs avec des outils de référence ou des applications propres pour des expériences logicielles ou des tests et sélection d'appareils. En revanche, dans les réseaux haut débit récents tels que 10 Gbps et 40 Gbps, ces résultats de mesure varient considérablement en fonction de l'implémentation de la partie API de communication de l'application, des paramètres du noyau ou des options de compilation, alors définissez-les correctement pour une mesure précise. / Besoin de comprendre. Cet article vise à vous donner un aperçu de la façon dont le noyau et les applications fonctionnent autour de votre réseau et les points clés de celui-ci.
Tout d'abord, jetons un coup d'œil à la manière dont les programmes serveur actuels qui utilisent TCP sont créés. Le programme serveur attend essentiellement une demande du client, traite la demande, renvoie la réponse au client et le programme client génère la demande, envoie la demande et répond à partir du serveur. Le processus consiste à attendre, et bien que l'ordre soit différent, la structure du programme est presque la même. Le client et le serveur doivent gérer les demandes (réponses dans le cas des clients) sur plusieurs connexions TCP qui fonctionnent de manière asynchrone, utilisez donc un cadre de surveillance des événements fourni par le système d'exploitation tel qu'epoll. L'image du programme utilisant epoll_ * ressemble à ce qui suit. Voir, par exemple, ici pour le code complet.
int lfd, epfd, newfd, nevts = 1000;
struct epoll_event ev;
struct epoll_event evts[1000]; /*Recevez jusqu'à 1000 demandes à la fois*/
struct sockaddr_in sin = {.sin_port = htons(50000), .sin_addr = INADDR_ANY};
char buf[10000]; /*Recevoir 10 Ko/Envoyer le tampon*/
/*Attendez une nouvelle connexion(listen)Prise pour*/
lfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
bind(lfd, &sin, sizeof(sin));
listen(listen_fd);
/*Descripteur de fichier d'événement pour l'attente d'événements multi-socket*/
epfd = epoll_create1(EPOLL_CLOEXE);
/*Enregistrer le socket d'écoute dans le descripteur de fichier d'événement*/
bzero(&ev, sizeof(ev));
ev.events = POLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev);
for (;;) {
int i, nfds;
/*Bloquez jusqu'à 1000 ms et attendez un événement*/
nfds = epoll_wait(epfd, evts, nevts, 1000);
for (i = 0; i < nfds; i++) {
int fd = evts[i].data.fd;
if (fd == lfd) { /*Nouvelle connexion TCP*/
bzero(&ev, sizeof(ev));
newfd = accept(fd);
ev.events = POLLIN;
ev.data.fd = newfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, newfd, &ev);
} else { /*Demandes de connexions TCP existantes*/
read(fd, buf);
/*Traiter la requête lue dans buf et préparer la réponse à buf*/
write(fd, buf); /*Envoyer une réponse*/
}
}
}
Dans ce programme, un descripteur de fichier epfd pour la surveillance des événements, un socket lfd pour écouter une nouvelle connexion et un socket pour chaque connexion TCP pour laquelle une connexion a été établie apparaîtront. epoll_wait () récupère les événements du noyau et les traite un par un (pour les boucles qui incrémentent la variable i). Les nouvelles demandes de connexion sont notifiées au socket d'écoute (lfd), et les demandes sur les connexions existantes sont notifiées aux sockets des connexions existantes. Pour le premier événement, l'appel système accept () crée une socket correspondant à la nouvelle connexion et l'enregistre dans la liste des descripteurs gérés par epoll (epoll_ctl ()). Pour ce dernier événement, read () lit la requête du descripteur cible, fait quelque chose, puis écrit () la réponse. Pour utiliser plusieurs cœurs de processeur, exécutez cette boucle d'événements epoll_wait () dans un thread distinct sur chaque cœur.
Les applications serveur et les bibliothèques telles que nginx, memcached, Redis et libuv ont presque le même modèle de programmation, donc lorsque vous exécutez un programme qui gère plusieurs connexions TCP, gardez à l'esprit un aperçu de ce comportement. Je pense que c'est bien.
Ensuite, examinons rapidement le fonctionnement de la pile réseau sous Linux. Lorsqu'un paquet arrive à la carte réseau, la carte réseau transmet le paquet en mémoire et interrompt le processeur. Cela arrêtera temporairement le thread en cours d'exécution sur ce processeur et exécutera à la place quelque chose comme une fonction appelée gestionnaire d'interruption. Strictement parlant, il est divisé en interruptions matérielles et en interruptions logicielles, mais ici ces processus sont collectivement appelés un gestionnaire d'interruption. Le gestionnaire d'interruption, par exemple, déplace le paquet vers la pile réseau (IP ou TCP) et traite l'en-tête. Au cours de ce processus, il est déterminé si le paquet doit être notifié à l'application (paquet ACK pour SYN / ACK représentant une nouvelle connexion, paquet contenant des données). Si l'application doit être notifiée pour le traitement des paquets et que l'application s'est enregistrée et a attendu le descripteur dans epoll_wait () (c'est-à-dire, dans le cas du code ci-dessus), la file d'attente epoll (exemple de code ci-dessus) Enregistrez le descripteur et le contenu de l'événement (POLLIN pour la réception) dans (correspondant à epfd). Les événements enregistrés de cette manière sont notifiés collectivement lorsque epoll_wait (), qui est bloqué par l'application, est renvoyé.
Jusqu'à présent, nous avons brièvement vu le processus depuis le moment où le noyau reçoit un paquet jusqu'au moment où l'application reçoit les données, mais de nombreux paramètres sont impliqués. Cette section utilise des expériences pour expliquer les effets de ces paramètres sur les performances du réseau.
Le serveur est Intel Xeon Silver 4110 (2,1 Ghz), le client est Xeon E5-2690v4 (2,6 Ghz) et il est connecté par Intel X540 10GbE NIC. Les détails seront décrits plus tard, mais sauf indication contraire comme configuration de base, turbo boost et hyper threading, mode veille du processeur, netfilter sont tous désactivés, le nombre de files d'attente NIC est de un et dans epoll. Le temps de blocage est mis à zéro. En tant que logiciel de référence, le serveur fonctionne de la même manière que le code ci-dessus Programme serveur expérimental, le client est Utilisez le populaire outil d'analyse comparative HTTP wrk. wrk continuera à envoyer des requêtes au serveur et à recevoir des réponses sur le nombre spécifié de connexions TCP pendant la durée spécifiée. La connexion TCP reste tendue et la taille à l'exclusion des en-têtes HTTP GET et OK TCP / IP / Ethernet échangés est respectivement de 44 octets et 151 octets. Comme les deux sont de petites données et tiennent dans un seul paquet, les fonctions de déchargement NIC telles que TSO, LRO et le déchargement de somme de contrôle n'ont aucun effet et sont désactivées.
Dans la discussion précédente, nous avons mentionné que la carte réseau interrompt le processeur, mais sur les réseaux à haut débit, le taux de réception des paquets peut être de millions voire de dizaines de millions de paquets par seconde. Etant donné que le traitement des interruptions est interrompu pour l'application en cours, si le débit de réception des paquets est trop élevé, la plupart du temps CPU sera utilisé pour le traitement des interruptions, ou le traitement de l'application ou du noyau sera fréquent. Il y a un problème d'interruption. Ce problème est communément appelé livelock. Puisqu'il faut des dizaines à des centaines de ns pour traiter un paquet reçu, on calcule qu'un cycle CPU de plusieurs Ghz n'est utilisé que pour traiter l'interruption entière.
Par conséquent, la carte réseau a un mécanisme pour réduire la fréquence d'interruption du processeur. Par défaut, il est souvent défini sur environ une fois par 1us. Cependant, augmenter simplement cette valeur ne signifie pas que le nombre d'interruptions peut être réduit, et même si un nouveau paquet arrive, l'interruption ne sera pas générée pendant un certain temps, il y a donc un problème que le retard à faible charge augmente. De plus, en raison du mécanisme appelé NAPI, le noyau désactive l'interruption de la carte réseau par lui-même en fonction de la quantité de paquets reçus non traités, donc même si cette valeur est augmentée, le débit n'augmente même pas et la réception de nouveaux paquets est retardée. Il peut également s'agir d'une situation telle que.
Ici, mesurons le temps nécessaire pour émettre une requête via HTTP et recevoir une réponse en raison de la différence de taux d'interruption. Tout d'abord, établissez une seule connexion TCP au serveur, puis répétez l'envoi de HTTP GET et la réception de HTTP OK. Vous trouverez ci-dessous les commandes et les résultats, avec le délai d'interruption côté serveur défini sur zéro. «-d 3» représente l'heure de l'expérience, et «-c 1» et «-t 1» représentent respectivement une connexion et un thread.
root@client:~# wrk -d 3 -c 1 -t 1 http://192.168.11.3:60000/
Running 3s test @ http://192.168.11.3:60000/
1 threads and 1 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 24.56us 2.11us 268.00us 97.76%
Req/Sec 38.43k 231.73 38.85k 64.52%
118515 requests in 3.10s, 17.07MB read
Requests/sec: 38238.90
Transfer/sec: 5.51MB
Voici le résultat lorsque le délai d'interruption côté serveur est défini sur 1us.
root@client:~# wrk -d 3 -c 1 -t 1 http://192.168.11.3:60000/
Running 3s test @ http://192.168.11.3:60000/
1 threads and 1 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 29.16us 2.59us 314.00us 99.03%
Req/Sec 32.53k 193.15 32.82k 74.19%
100352 requests in 3.10s, 14.45MB read
Requests/sec: 32379.67
Transfer/sec: 4.66MB
Comme vous pouvez le voir, le résultat est assez différent.
Ensuite, essayez d'utiliser plusieurs connexions TCP parallèles. Avec la commande suivante, le client établit 100 connexions TCP, envoie une demande sur chaque connexion avec 100 threads et reçoit une réponse.
Ce qui suit est le cas sans délai d'interruption
root@client:~# wrk -d 3 -c 100 -t 100 http://192.168.11.3:60000/
Running 3s test @ http://192.168.11.3:60000/
100 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 245.83us 41.77us 1.22ms 84.60%
Req/Sec 4.05k 152.00 5.45k 88.90%
1248585 requests in 3.10s, 179.80MB read
Requests/sec: 402774.94
Transfer/sec: 58.00MB
Voici le délai d'interruption 1us.
root@client:~# wrk -d 3 -c 100 -t 100 http://192.168.11.3:60000/
Running 3s test @ http://192.168.11.3:60000/
100 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 247.22us 41.20us 1.16ms 84.70%
Req/Sec 4.03k 137.84 5.43k 90.80%
1241477 requests in 3.10s, 178.78MB read
Requests/sec: 400575.95
Transfer/sec: 57.68MB
Le délai d'interruption a peu d'effet, mais cela est dû à la capacité du NIC à désactiver l'interruption elle-même. Étant donné que de nombreux paquets arrivent en même temps (jusqu'à 100), le noyau désactive temporairement les interruptions du NIC même si la fréquence d'interruption est réglée à un niveau élevé.
Un autre point à noter ici est que le retard est d'environ un ordre de grandeur plus grand que lorsqu'il n'y a pas de connexion parallèle. En effet, plusieurs événements (demandes entrantes) sont traités un par un dans la boucle d'événements de l'application. Imaginez que le programme serveur ci-dessus reçoive jusqu'à 100 événements en même temps (jusqu'à 100 nfds renvoyés par epoll_wait ()).
Le taux d'interruption de la carte réseau est différent pour le pilote ixgbe (carte réseau Intel X520 et X540 10GbE) et le pilote i40e (carte réseau Intel X710 / XXV710 / XL710 10/25/40 GbE).
root@server:~# ethtool -C enp23s0f0 rx-usecs 0 #Aucun délai d'interruption
Il peut être défini comme suit.
Lors de la mesure du retard du réseau, vérifiez les paramètres d'interruption de la carte réseau.
Cette fonction augmente l'horloge d'un autre cœur de processeur à forte charge lorsque la charge de certains cœurs de processeur est faible. C'est en fait une fonctionnalité très ennuyeuse et devrait être désactivée pour les mesures de performances. La raison en est que lors d'une expérience d'évolutivité sur le nombre de cœurs de processeur, le débit évolue linéairement de 1 à 3 cœurs sur 6 cœurs, mais arrête soudainement la mise à l'échelle. se produire. Ce n'était pas parce que le programme ou le noyau était mauvais, bien sûr, mais parce que si le nombre de cœurs utilisés était de 1 à 3, les cœurs restants étaient inactifs et les cœurs actifs étaient synchronisés par turbo boost. Il y a beaucoup de.
Pour désactiver le turbo boost, vous pouvez le désactiver dans les paramètres du BIOS ou effectuer les opérations suivantes:
root@server:~# sh -c "echo 1 >> /sys/devices/system/cpu/intel_pstate/no_turbo"
Si vous rencontrez des problèmes d'évolutivité multicœur, vous devez également revoir vos paramètres de turbo boost.
Éteignons-le sans réfléchir
Pour économiser l'énergie, les processeurs modernes passent en plusieurs étapes de veille en fonction de la charge, et les performances du logiciel peuvent devenir instables en raison du mouvement entre les états de veille. En tant que phénomène apparemment mystérieux causé par cela, par exemple, dans le code tel que le programme serveur ci-dessus, si plusieurs connexions parallèles sont gérées plutôt qu'une connexion unique, la charge augmentera modérément et le processeur ne se mettra pas en veille. Il arrive que le délai observé par le client se raccourcisse. Par exemple, dans la figure 2 de cet article, le délai pour 5 connexions est légèrement plus petit que pour 1 connexion, ce qui est involontaire. J'ai oublié de désactiver le mode veille pour ce processeur.
Pour éviter que le processeur ne se mette en veille, il est judicieux de définir intel_idle.max_cstate = 0 processor.max_cstate = 1 dans les paramètres de démarrage du noyau (spécifiés dans des fichiers tels que grub.cfg et pxelinux.cfg / default). Voici un extrait de mon pxelinux.cfg / default
APPEND ip=::::::dhcp console=ttyS0,57600n8 intel_idle.max_cstate=0 processor.max_cstate=1
Les cartes réseau modernes ont plusieurs files d'attente de tampon de paquets, chacune pouvant interrompre un processeur distinct. Par conséquent, le nombre de files d'attente dans le NIC a un effet important sur la gestion des interruptions dans le noyau et doit être examiné attentivement.
root@c307:~# wrk -d 3 -c 1 -t 1 http://192.168.11.3:60000/
Running 3s test @ http://192.168.11.3:60000/
1 threads and 1 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 25.22us 2.03us 259.00us 97.68%
Req/Sec 37.50k 271.83 38.40k 74.19%
115595 requests in 3.10s, 16.65MB read
Requests/sec: 37299.11
Transfer/sec: 5.37MB
Ce qui précède est la même expérience que celle dans laquelle le délai d'interruption a été mis à zéro dans la section [NIC interruption delay](#NIC interruption delay) précédemment, mais le nombre de files d'attente sur le serveur NIC a été défini sur 8 et le nombre de threads et de cœurs. Est réglé sur 8. En tant qu'image, pensez à la boucle d'événements epoll_wait du programme serveur ci-dessus s'exécutant dans un thread séparé sur chaque cœur. La raison pour laquelle le débit ne s'améliore pas est qu'une seule connexion est utilisée dans cette expérience, et le NIC détermine la file d'attente / CPU à interrompre par la valeur de hachage du port ou de l'adresse de la connexion, donc tout le traitement est le même CPU. Parce que cela se fait dans un fil. De plus, le délai est légèrement augmenté par rapport à l'expérience [Interrupt Delay Section](#NIC interruption delay) utilisant 1 thread et 1 file d'attente (24,56-> 25,22). Cela montre que l'activation de plusieurs files d'attente entraîne une légère surcharge. Par conséquent, en fonction de l'expérience, il peut être préférable de réduire le nombre de files d'attente à un.
Comme vous pouvez le voir ci-dessous, avec 100 connexions, les paquets appartenant à différentes connexions sont répartis sur plusieurs files d'attente, ce qui entraîne un débit mis à l'échelle, sinon parfait.
root@client:~# wrk -d 3 -c 100 -t 100 http://192.168.11.3:60000/
Running 3s test @ http://192.168.11.3:60000/
100 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 65.68us 120.17us 17.26ms 99.53%
Req/Sec 15.52k 1.56k 28.00k 81.43%
4780372 requests in 3.10s, 688.40MB read
Requests/sec: 1542345.05
Transfer/sec: 222.11MB
Le nombre de files d'attente NIC est
ethtool -L enp23s0f0 combined 8 #Nombre de files d'attente NIC à 8
Il peut être défini comme suit.
En résumé, la file d'attente de la carte réseau devrait essentiellement être identique au nombre de cœurs utilisés par l'application, mais dans certains cas, cela peut être une surcharge, alors changez-la si nécessaire. Et bien sûr, sachez que votre application doit également être programmée ou configurée pour s'exécuter sur plusieurs cœurs.
Le noyau Linux fournit un mécanisme (netfilter) pour accrocher des paquets à divers endroits de la pile réseau. Ces hooks sont activés dynamiquement selon iptables etc., mais ce mécanisme lui-même affecte souvent les performances indépendamment du fait que les hooks individuels soient activés ou désactivés. Si vous souhaitez mesurer les performances avec précision, désactivez CONFIG_NETFILTER et CONFIG_RETPOLINE dans la configuration de votre noyau, sauf si vous en avez besoin.
Comme vu dans le programme serveur ci-dessus, les applications bloquent généralement (sommeil) avec epoll_wait () et attendent les événements. Vous pouvez passer une valeur à epoll_wait () qui spécifie le temps de blocage en unités de ms. Cependant, comme mentionné ci-dessus, le gestionnaire d'interruption est exécuté en empruntant le contexte du thread en cours d'exécution, donc s'il n'y a pas de thread en cours d'exécution lorsque le CPU reçoit une interruption de la carte réseau, le thread en veille est d'abord sélectionné. Vous devrez le réveiller. L'opération qui réveille ce fil implique une surcharge considérable. Ce qui suit est le résultat de l'utilisation de epoll_wait () pour bloquer 1000 ms (le délai d'interruption de la carte réseau est nul).
root@client:~# wrk -d 3 -c 1 -t 1 http://192.168.11.3:60000/
Running 3s test @ http://192.168.11.3:60000/
1 threads and 1 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 33.56us 3.28us 282.00us 95.50%
Req/Sec 28.42k 192.11 28.76k 54.84%
87598 requests in 3.10s, 12.61MB read
Requests/sec: 28257.46
Transfer/sec: 4.07MB
Dans l'expérience de la section [NIC interruption delay](#NIC interruption delay), il était de 24,56us, donc nous pouvons voir que le délai a augmenté de près de 10us. Pour vous assurer qu'il y a un thread en cours d'exécution chaque fois que le CPU est interrompu, vous pouvez passer zéro temps de blocage à epoll_wait ().
Dans cet article, nous avons présenté divers paramètres qui affectent les performances du réseau. Nous espérons qu'il sera utile pour les expériences des administrateurs de serveur, des développeurs d'applications et de ceux qui (planifient) des recherches sur les piles réseau et le système d'exploitation (de bibliothèque).
Recommended Posts