Pourquoi la latence du réseau mesurée change-t-elle si j’utilise un sumil?

J’essaie de déterminer le temps nécessaire à une machine pour recevoir un paquet, le traiter et lui donner une réponse.

Cette machine, que j’appelle “serveur”, exécute un programme très simple, qui reçoit un paquet ( recv(2) ) dans un tampon, copie le contenu reçu ( memcpy(3) ) dans un autre tampon et renvoie le paquet ( send(2) ). Le serveur exécute NetBSD 5.1.2.

Mon client mesure plusieurs fois le temps d’aller-retour ( pkt_count ):

 struct timespec start, end; for(i = 0; i < pkt_count; ++i) { printf("%d ", i+1); clock_gettime(CLOCK_MONOTONIC, &start); send(sock, send_buf, pkt_size, 0); recv(sock, recv_buf, pkt_size, 0); clock_gettime(CLOCK_MONOTONIC, &end); //struct timespec nsleep = {.tv_sec = 0, .tv_nsec = 100000}; //nanosleep(&nsleep, NULL); printf("%.3f ", timespec_diff_usec(&end, &start)); } 

J’ai éliminé les vérifications d’erreur et autres éléments mineurs pour plus de clarté. Le client s’exécute sur un Ubuntu 12.04 64 bits. Les deux programmes fonctionnent en priorité en temps réel, bien que seul le kernel Ubuntu soit en temps réel (-rt). La connexion entre les programmes est TCP. Cela fonctionne bien et me donne une moyenne de 750 microsecondes.

Cependant, si j’active l’appel nanosleep commenté (avec un sumil de 100 μs), mes mesures chutent de 100 μs, ce qui donne une moyenne de 650 μs. Si je dors pendant 200 µs, les mesures chutent à 550 µs, et ainsi de suite. Cela monte jusqu’à un sumil de 600 μs, donnant une moyenne de 150 μs. Ensuite, si j’augmente le sumil à 700 μs, mes mesures vont jusqu’à 800 μs en moyenne. J’ai confirmé les mesures de mon programme avec Wireshark.

Je ne peux pas comprendre ce qui se passe. J’ai déjà défini l’option de socket TCP_NODELAY dans le client et le serveur, sans différence. J’ai utilisé UDP, pas de différence (même comportement). Donc, je suppose que ce comportement n’est pas dû à l’algorithme Nagle. Qu’est ce que ça pourrait être?

[METTRE À JOUR]

Voici une capture d’écran de la sortie du client avec Wireshark. Maintenant, j’ai couru mon serveur sur une autre machine. J’ai utilisé le même système d’exploitation avec la même configuration (comme c’est un système Live dans une clé USB), mais le matériel est différent. Ce comportement ne s’est pas manifesté, tout a fonctionné comme prévu. Mais la question demeure: pourquoi cela se passe-t-il dans le matériel précédent?

Comparaison de sortie

[MISE À JOUR 2: Plus d’info]

Comme je l’ai déjà dit, j’ai testé ma paire de programmes (client / serveur) sur deux serveurs différents. J’ai tracé les deux résultats obtenus.

Comparaison entre deux serveurs

Le premier serveur (le plus étrange) est un ordinateur à carte unique RTD , avec une interface Ethernet 1 Gbit / s. Le second serveur (normal) est un ordinateur à carte unique Diamond doté d’une interface Ethernet 100 Mbps. Les deux exécutent le SAM OS (NetBSD 5.1.2) à partir de la clé SAME.

À partir de ces résultats, je pense que ce comportement est dû soit au conducteur, soit au NIC lui-même, même si je ne peux toujours pas imaginer pourquoi cela se produit …

OK, je suis arrivé à une conclusion.

J’ai essayé mon programme en utilisant Linux, au lieu de NetBSD, sur le serveur. Il a fonctionné comme prévu, c.-à-d., Peu importe combien je dors à ce point du code, le résultat est le même.

Ce fait me dit que le problème pourrait provenir du pilote d’interface de NetBSD. Pour identifier le pilote, je lis la sortie dmesg . C’est la partie pertinente:

 wm0 at pci0 dev 25 function 0: 82801I mobile (AMT) LAN Controller, rev. 3 wm0: interrupting at ioapic0 pin 20 wm0: PCI-Express bus wm0: FLASH wm0: Ethernet address [OMMITED] ukphy0 at wm0 phy 2: Generic IEEE 802.3u media interface ukphy0: OUI 0x000ac2, model 0x000b, rev. 1 ukphy0: 10baseT, 10baseT-FDX, 100baseTX, 100baseTX-FDX, 1000baseT, 1000baseT-FDX, auto 

Donc, comme vous pouvez le voir, mon interface s’appelle wm0 . Selon cela (page 9), je devrais vérifier quel pilote est chargé en consultant le fichier sys/dev/pci/files.pci , ligne 625 ( ici ). Ça montre:

 # Intel i8254x Gigabit Ethernet device wm: ether, ifnet, arp, mii, mii_bitbang attach wm at pci file dev/pci/if_wm.c wm 

Puis, en parcourant le code source du pilote ( dev/pci/if_wm.c , ici ), j’ai trouvé un extrait de code susceptible de modifier le comportement du pilote:

 /* * For N interrupts/sec, set this value to: * 1000000000 / (N * 256). Note that we set the * absolute and packet timer values to this value * divided by 4 to get "simple timer" behavior. */ sc->sc_itr = 1500; /* 2604 ints/sec */ CSR_WRITE(sc, WMREG_ITR, sc->sc_itr); 

Puis j’ai changé cette valeur 1500 à 1 (en essayant d’augmenter le nombre d’interruptions autorisé par seconde) et à 0 (en essayant d’éliminer complètement la limitation des interruptions), mais ces deux valeurs ont produit le même résultat:

  • Sans nanosep: latence de ~ 400 us
  • Avec un nanosep de 100 us: latence de ~ 230 us
  • Avec un nanosep de 200 us: latence de ~ 120 us
  • Avec un nanosep de 260 us: latence de ~ 70 us
  • Avec un nanosep de 270 us: latence de ~ 60 us (latence minimum que je pourrais atteindre)
  • Avec un nanosep de rien au-dessus de 300 nous: ~ 420 us

C’est, au moins mieux comporté que la situation précédente.

Par conséquent, j’ai conclu que le comportement est dû au pilote d’interface du serveur. Je ne suis pas disposé à aller plus loin pour trouver d’autres coupables, car je passe de NetBSD à Linux pour le projet impliquant cet ordinateur à carte unique.

C’est une conjecture (espérons-le éduquée), mais je pense que cela pourrait expliquer ce que vous voyez.

Je ne suis pas sûr du temps réel du kernel Linux. Il pourrait ne pas être totalement préemptif … Alors, avec cet avertissement, continuez:) …

Selon le planificateur, une tâche aura peut-être ce qu’on appelle un “quanta”, qui est juste une quantité de temps pendant laquelle une tâche de même priorité sera programmée. Si le kernel n’est pas totalement préemptif, Cela peut également être le point où une tâche de priorité plus élevée peut être exécutée. Cela dépend des détails du programmateur dont je ne connais pas assez.

Entre votre premier et votre deuxième rendez-vous, votre tâche peut être préemptée. Cela signifie simplement qu’il est “suspendu” et qu’une autre tâche utilise le processeur pendant un certain temps.

La boucle sans le sumil pourrait aller quelque chose comme ça

 clock_gettime(CLOCK_MONOTONIC, &start); send(sock, send_buf, pkt_size, 0); recv(sock, recv_buf, pkt_size, 0); clock_gettime(CLOCK_MONOTONIC, &end); printf("%.3f ", timespec_diff_usec(&end, &start)); clock_gettime(CLOCK_MONOTONIC, &start); <----- PREMPTION .. your tasks quanta has run out and the scheduler kicks in ... another task runs for a little while <----- PREMPTION again and your back on the CPU send(sock, send_buf, pkt_size, 0); recv(sock, recv_buf, pkt_size, 0); clock_gettime(CLOCK_MONOTONIC, &end); // Because you got pre-empted, your time measurement is artifically long printf("%.3f ", timespec_diff_usec(&end, &start)); clock_gettime(CLOCK_MONOTONIC, &start); <----- PREMPTION .. your tasks quanta has run out and the scheduler kicks in ... another task runs for a little while <----- PREMPTION again and your back on the CPU and so on.... 

Lorsque vous mettez en veille la nanoseconde, il est fort probable que le planificateur puisse s'exécuter avant que les quanta de la tâche en cours n'expirent (la même chose s'applique à recv (), qui bloque). Alors peut-être que ce que vous obtenez est quelque chose comme ça

 clock_gettime(CLOCK_MONOTONIC, &start); send(sock, send_buf, pkt_size, 0); recv(sock, recv_buf, pkt_size, 0); clock_gettime(CLOCK_MONOTONIC, &end); struct timespec nsleep = {.tv_sec = 0, .tv_nsec = 100000}; nanosleep(&nsleep, NULL); <----- PREMPTION .. nanosleep allows the scheduler to kick in because this is a pre-emption point ... another task runs for a little while <----- PREMPTION again and your back on the CPU // Now it so happens that because your task got prempted where it did, the time // measurement has not been artifically increased. Your task then can fiish the rest of // it's quanta printf("%.3f ", timespec_diff_usec(&end, &start)); clock_gettime(CLOCK_MONOTONIC, &start); ... and so on 

Une sorte d'entrelacement se produira alors où parfois vous êtes entre les deux gettime () et parfois en dehors à cause du nanosleep. En fonction de x, vous pouvez atteindre un endroit où vous vous trouvez (par hasard) pour que votre sharepoint préemption, en moyenne, soit en dehors de votre bloc de mesure du temps.

En tout cas, ça vaut la peine, j'espère que ça aide à expliquer les choses 🙂

Une petite note sur les "nanosecondes" pour finir avec ...

Je pense qu'il faut être prudent avec le sumil "nanosecondes". La raison pour laquelle je dis cela est que je pense qu’il est peu probable qu’un ordinateur moyen puisse réellement le faire à moins d’utiliser un matériel spécial.

Normalement, un OS aura un système "tick", généré à environ 5 ms. Ceci est une interruption générée par exemple par un RTC (Real Time Clock - juste un peu de matériel). En utilisant ce "tick", le système génère sa représentation temporelle interne. Ainsi, l'OS moyen n'aura qu'une résolution temporelle de quelques millisecondes. La raison pour laquelle cette coche n’est pas plus rapide est qu’il faut trouver un équilibre entre garder un temps très précis et ne pas saturer le système avec des interruptions de timer.

Je ne suis pas sûr que je sois un peu obsolète avec votre PC moderne moyen ... Je pense que certains d'entre eux ont des timers plus élevées, mais pas encore dans la gamme des nanosecondes et ils pourraient même avoir du mal à 100uS.

Donc, en résumé, gardez à l'esprit que la meilleure résolution temporelle que vous êtes susceptible d'obtenir est normalement dans la plage des millisecondes.

EDIT: Revisitez cela et pensais que j'appendais ce qui suit ... n'explique pas ce que vous voyez, mais pourrait fournir une autre voie à explorer ...

Comme mentionné, la précision de la synchronisation du nanosleep est peu probable que les millisecondes. De plus, votre tâche peut être préemptée, ce qui entraînera également des problèmes de synchronisation. Il y a également le problème que le temps nécessaire à un paquet pour monter la stack de protocole peut varier, ainsi que le délai du réseau.

Une chose que vous pouvez essayer est, si votre carte réseau prend en charge, IEEE1588 (aka PTP). Si votre carte réseau le prend en charge, il peut horodater les paquets d'événements PTP au fur et à mesure qu'ils quittent et entrent dans le PHY. Cela vous donnera une estimation possible du délai du réseau. Cela élimine tous les problèmes que vous pourriez avoir avec la préemption logicielle, etc., etc. Je sais que le squat Linux PTP me fait peur, mais vous pouvez essayer http://linuxptp.sourceforge.net

Je pense que «quanta» est la meilleure théorie pour l’explication. Sur Linux, c’est la fréquence de changement de contexte. Le kernel donne à traiter le temps quanta. Mais le processus est préempté dans deux situations:

  1. Procédure du système d’appel de processus
  2. le temps quanta est terminé
  3. l’interruption matérielle est en cours (à partir du réseau, hdd, usb, clock, etc …)

Le temps de quanta non utilisé est affecté à un autre processus prêt à l’emploi, en utilisant les priorités / rt, etc.

En fait, la fréquence de changement de contexte est configurée à 10000 fois par seconde, cela donne environ 100us pour les quanta. mais le changement de contenu prend du temps, cpu dépend, voir ceci: http://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html pourquoi la fréquence swith de contenu est-elle si élevée mais c’est la discussion pour le forum du kernel Linux.

problème partiellement similaire, vous pouvez trouver ici: https://serverfault.com/questions/14199/how-many-context-switches-is-normal-as-a-function-of-cpu-cores-or-autre

Si la quantité de données envoyées par l’application est importante et assez rapide, cela pourrait remplir les tampons du kernel, ce qui entraînerait un délai à chaque envoi (). Comme le sumil est en dehors de la section mesurée, il prendrait alors le temps qui serait autrement bloqué lors de l’appel send ().

Une façon de vérifier ce cas serait d’exécuter avec un nombre relativement faible d’itérations, puis un nombre modéré d’itérations. Si le problème survient avec un petit nombre d’itérations (disons 20) avec des tailles de paquet réduites (disons <1k), alors il s'agit probablement d'un diagnostic incorrect.

Gardez à l’esprit que votre processus et le kernel peuvent facilement surcharger la carte réseau et la vitesse filaire de l’Ethernet (ou d’un autre type de média) si vous envoyez des données en boucle comme celle-ci.

J’ai du mal à lire les captures d’écran. Si Wirehark affiche un taux de transmission constant sur le câble, cela suggère que ce sont les diagnostics corrects. Bien sûr, faire le calcul – diviser la vitesse de fil par la taille du paquet (+ en-tête) – devrait donner une idée de la vitesse maximale à laquelle les paquets peuvent être envoyés.

En ce qui concerne les 700 microsecondes conduisant à un retard accru, cela est plus difficile à déterminer. Je n’ai aucune idée à ce sujet.

J’ai un conseil sur la façon de créer une mesure de performance plus précise. Utilisez l’instruction RDTSC (ou même mieux la fonction insortingnsèque __rdtsc ()). Cela implique la lecture d’un compteur CPU sans quitter ring3 (pas d’appel système). Les fonctions gettime impliquent presque toujours un appel système qui ralentit les choses.

Votre code est un peu délicat car il implique 2 appels système (send / recv), mais en général, il est préférable d’appeler sleep (0) avant la première mesure pour s’assurer que la mesure très courte ne reçoit pas de changement de contexte. Bien entendu, le code de mesure du temps (et Sleep ()) doit être désactivé / activé via des macros dans des fonctions sensibles aux performances.

Certains systèmes d’exploitation peuvent être amenés à augmenter la priorité de votre processus en faisant en sorte que votre processus libère la fenêtre de temps d’exécution (par exemple sleep (0)). À la prochaine planification, le système d’exploitation (pas tous) augmentera la priorité de votre processus car il n’a pas fini d’exécuter son quota de temps d’exécution.