Comme dans , un problème est survenu avec le service distribué, appelons ce service Elvin. Cette fois, je n'ai pas découvert le problème moi-même, c'est l'équipe côté client qui m'en a informé.
Un jour, je me suis réveillé à cause d'un e-mail mécontent à propos de gros délais avec Elvin, que nous prévoyions de lancer très bientôt. En particulier, le client avait rencontré un délai au 99ème percentile d'environ 50 ms, bien au-dessus de notre budget de latence. C'était surprenant, car j'avais soigneusement testé le service, notamment pour la latence, qui est un sujet de plaintes fréquentes.
Avant de confier Elvin aux tests, j'avais réalisé de nombreuses expériences avec 40 000 requêtes par seconde (QPS), toutes ont montré une latence inférieure à 10 ms. J'étais prêt à affirmer que je n'étais pas d'accord avec leurs résultats. Mais en regardant de nouveau l'e-mail, j'ai remarqué quelque chose de nouveau : je n'avais pas du tout testé les conditions qu'ils avaient mentionnées, leur QPS était bien inférieur au mien. J'avais testé à 40k QPS, tandis qu'ils n'étaient qu'à 1k. J'ai lancé une autre expérience, cette fois avec un QPS plus bas, juste pour les apaiser.
Puisque j'écris à ce sujet sur le blog — vous avez probablement déjà compris : leurs chiffres se sont avérés corrects. J'ai vérifié mon client virtuel encore et encore, avec le même résultat : un faible nombre de requêtes non seulement augmente la latence, mais augmente également le nombre de requêtes avec une latence supérieure à 10 ms. En d'autres termes, alors qu'avec 40k QPS environ 50 requêtes par seconde dépassaient 50 ms, avec 1k QPS, il y avait 100 requêtes par seconde supérieures à 50 ms. Paradoxe !

Réduisons le champ de recherche
Confronté à un problème de latence dans un système distribué avec de nombreux composants, la première étape consiste à établir une liste restreinte de suspects. Creusons un peu plus dans l'architecture d'Elvin :

Un bon point de départ est la liste des transitions d'entrées-sorties effectuées (appels réseau / recherche sur disque, etc.). Essayons de comprendre où se situe la latence. En plus de l'évident E/S avec le client, Elvin fait une étape supplémentaire : il se connecte au stockage des données. Cependant, ce stockage fonctionne dans le même cluster qu'Elvin, donc la latence là devrait être inférieure à celle avec le client. Ainsi, la liste des suspects :
- Appel réseau du client à Elvin.
- Appel réseau d'Elvin au stockage des données.
- Recherche sur disque dans le stockage des données.
- Appel réseau du stockage de données vers Elvin.
- Appel réseau d'Elvin au client.
Essayons de rayer certains éléments.
Le stockage de données n'est pas en cause.
Tout d'abord, j'ai transformé Elvin en serveur ping-ping qui ne traite pas les requêtes. Dès qu'il reçoit une requête, il renvoie une réponse vide. Si le délai diminue, alors l'erreur se trouve dans l'implémentation d'Elvin ou du stockage de données - rien d'extraordinaire. Dans la première expérience, nous obtenons ce graphique :

Comme nous le voyons, en utilisant le serveur ping-ping, aucune amélioration n'est notée. Cela signifie que le stockage de données n'augmente pas le délai, et la liste des suspects est réduite de moitié :
- Appel réseau du client à Elvin.
- Appel réseau d'Elvin au client.
Super ! La liste se réduit rapidement. Je pensais avoir presque trouvé la cause.
gRPC
Il est maintenant temps de vous présenter un nouveau joueur : . C'est une bibliothèque open-source de Google pour la communication inter-processus. . Bien que gRPC bien optimisée et largement utilisée, c'est la première fois que je l'utilise dans un système d'une telle ampleur, et je m'attendais à ce que mon implémentation soit sous-optimale - pour le dire poliment.
Présence gRPC dans la pile a soulevé une nouvelle question : est-ce que c'est mon implémentation ou la bibliothèque elle-même gRPC causerait le problème de délai ? Ajoutons à la liste un nouveau suspect :
- Le client appelle la bibliothèque.
gRPC - Bibliothèque
gRPCsur le client effectue l'appel réseau de la bibliothèque.gRPCsur le serveur - Bibliothèque
gRPCse connecte à Elvin (pas d'opération dans le cas du serveur ping-pong).
Pour que vous compreniez à quoi ressemble le code, mon implémentation client/Elvin ne diffère pas beaucoup des exemples client-serveur. .
Remarque : la liste ci-dessus est légèrement simplifiée, car
gRPCpermet d'utiliser son propre modèle de flux (template ?) dans lequel s'entrelacent la pile d'exécutiongRPCet l'implémentation utilisateur. Par simplicité, restons sur ce modèle.
Le profilage résoudra tout.
Avoir exclu les stockages de données, je pensais que j'avais presque fini : « Maintenant, c'est facile ! Appliquons le profil et découvrons où se situe le délai ». Je parce que le CPU est très rapide et est souvent pas le goulot d'étranglement. La plupart des délais se produisent lorsque le processeur doit arrêter le traitement pour faire autre chose. Le profilage précis du CPU est fait exactement pour ça : il enregistre avec précision tous les et permet de comprendre où se produisent les délais.
J'ai pris quatre profils : un pour un QPS élevé (faible latence) et un avec un serveur ping-pong à faible QPS (grande latence), à la fois côté client et côté serveur. Et juste au cas où, j'ai également pris un échantillon de profil de processeur. Lors de la comparaison des profils, je recherche généralement une pile d'appels anormale. Par exemple, du côté mauvais avec une latence élevée, il y a beaucoup plus de changements de contexte (10 fois ou plus). Mais dans mon cas, le nombre de changements de contexte coïncidait pratiquement. À ma grande horreur, il n'y avait rien de substantiel.
Débogage supplémentaire
J'étais désespéré. Je ne savais pas quels autres outils utiliser, et mon prochain plan consistait essentiellement à répéter des expériences avec différentes variations, plutôt qu'à diagnostiquer clairement le problème.
Et si
Depuis le début, une latence spécifique de 50 ms me préoccupait. C'est un temps très élevé. J'ai décidé de couper des morceaux de code jusqu'à ce que je puisse déterminer exactement quelle partie provoquait cette erreur. Ensuite, une expérience a suivi, qui a fonctionné.
Comme d'habitude, avec le recul, tout semblait évident. J'ai mis le client sur une machine avec Elvin - et j'ai envoyé une requête à localhost. Et l'augmentation de latence a disparu !

Il y avait quelque chose qui clochait avec le réseau.
Développer des compétences d'ingénieur réseau
Je dois admettre : mes connaissances en technologies réseau sont terribles, surtout considérant que je travaille avec elles tous les jours. Mais le réseau était le principal suspect, et je devais apprendre à le déboguer.
Heureusement, Internet aime ceux qui veulent apprendre. La combinaison de ping et de tracert semblait être un bon début pour déboguer les problèmes de transport réseau.
Tout d'abord, j'ai lancé sur le port TCP d'Elvin. J'ai utilisé les paramètres par défaut - rien de spécial. Sur plus de mille pings, aucun n'a dépassé 10 ms, sauf le premier pour le préchauffage. Cela contredit l'augmentation observée de la latence de 50 ms au 99e percentile : là, pour chaque 100 requêtes, nous devrions voir environ une requête avec une latence de 50 ms.
Puis j'ai essayé : peut-être que le problème se situe sur l'un des nœuds du chemin entre Elvin et le client. Mais le traceur est également revenu les mains vides.
Ainsi, la cause de la latence n'était ni mon code, ni l'implémentation gRPC, ni le réseau. J'ai déjà commencé à m'inquiéter de ne jamais comprendre cela.
Alors, sur quel OS sommes-nous ?
gRPC Il est largement utilisé sur Linux, mais c'est exotique pour Windows. J'ai décidé de mener une expérience qui a fonctionné : j'ai créé une machine virtuelle Linux, compilé Alvin pour Linux et l'ai déployé.

Et voici ce qui s'est passé : dans le serveur ping-pong de Linux, il n'y avait pas de latences comme sur l'équivalent Windows, bien que la source des données soit restée identique. Le problème semble venir de l'implémentation de gRPC pour Windows.
L'algorithme de Nagle
Tout ce temps, je pensais qu'il me manquait un drapeau gRPC. Maintenant, je comprends qu'en fait, c'est dans gRPC le drapeau Windows qui manque. J'ai trouvé une bibliothèque RPC interne, en étant sûr qu'elle fonctionne bien pour tous les drapeaux installés. . J'ai ensuite ajouté tous ces drapeaux à gRPC et déployé Alvin sur Windows, dans le serveur ping-pong corrigé sous Windows !

Presque terminé : j'ai commencé à supprimer les drapeaux ajoutés un par un, jusqu'à ce que la régression revienne, ce qui m'a permis de déterminer précisément sa cause. C'était le tristement célèbre , le commutateur de l'algorithme de Nagle.
essaye de réduire le nombre de paquets envoyés sur le réseau en retardant la transmission des messages jusqu'à ce que la taille du paquet dépasse un certain nombre d'octets. Bien que cela puisse être agréable pour l'utilisateur moyen, cela est destructeur pour les serveurs en temps réel, car l'OS retarde certains messages, provoquant des retards à faible QPS. Un gRPC avait ce drapeau activé dans l'implémentation Linux pour les sockets TCP, mais pas pour Windows. Je l'ai .
Conclusion
La grande latence à faible QPS était causée par une optimisation de l'OS. En y repensant, le profilage n'a pas détecté de latence, car il a été effectué en mode noyau, et non en . Je ne sais pas si l'on peut observer l'algorithme de Nagle à travers des captures ETW, mais ce serait intéressant.
Concernant l'expérience localhost, elle ne concernait probablement pas le code réseau réel, et l'algorithme de Nagle ne s'est pas activé, donc les problèmes de latence ont disparu lorsque le client a accédé à Alvin via localhost.
La prochaine fois que vous verrez une augmentation de la latence avec une diminution du nombre de requêtes par seconde, l'algorithme de Nagle doit être sur votre liste des suspects !
Source : habr.com
