Parfois, plus c'est moins. Lorsque la réduction de la charge entraîne une augmentation de la latence

Comme dans la plupart des messages, il y a un problème avec un service distribué, appelons ce service Alvin. Cette fois, je n’ai pas découvert le problème moi-même, m’ont informé les gars du côté client.

Un jour, je me suis réveillé avec un e-mail mécontent en raison de longs retards avec Alvin, que nous prévoyions de lancer dans un avenir proche. Plus précisément, le client a connu une latence du 99e centile de l’ordre de 50 ms, bien au-dessus de notre budget de latence. C'était surprenant car j'ai testé le service de manière approfondie, notamment sur la latence, qui est une plainte courante.

Avant de tester Alvin, j'ai effectué de nombreuses expériences avec 40 10 requêtes par seconde (QPS), toutes montrant une latence inférieure à 40 ms. J'étais prêt à déclarer que je n'étais pas d'accord avec leurs résultats. Mais en regardant à nouveau la lettre, j'ai remarqué quelque chose de nouveau : je n'avais pas exactement testé les conditions mentionnées, leur QPS était bien inférieur au mien. J'ai testé à 1 XNUMX QPS, mais ils seulement à XNUMX XNUMX. J'ai mené une autre expérience, cette fois avec un QPS inférieur, juste pour les apaiser.

Depuis que je blogue à ce sujet, vous avez probablement déjà compris que leurs chiffres étaient exacts. J'ai testé mon client virtuel encore et encore, avec le même résultat : un faible nombre de requêtes augmente non seulement la latence, mais augmente le nombre de requêtes avec une latence supérieure à 10 ms. En d’autres termes, si à 40 50 QPS, environ 50 requêtes par seconde dépassaient 1 ms, alors à 100 50 QPS, il y avait XNUMX requêtes supérieures à XNUMX ms par seconde. Paradoxe!

Parfois, plus c'est moins. Lorsque la réduction de la charge entraîne une augmentation de la latence

Affiner la recherche

Face à un problème de latence dans un système distribué comportant de nombreux composants, la première étape consiste à créer une courte liste de suspects. Approfondissons un peu l'architecture d'Alvin :

Parfois, plus c'est moins. Lorsque la réduction de la charge entraîne une augmentation de la latence

Un bon point de départ est une liste des transitions d'E/S terminées (appels réseau/recherches de disque, etc.). Essayons de comprendre où se situe le retard. Outre les E/S évidentes avec le client, Alvin franchit une étape supplémentaire : il accède au magasin de données. Cependant, ce stockage fonctionne dans le même cluster qu'Alvin, la latence devrait donc y être moindre qu'avec le client. Ainsi, la liste des suspects :

  1. Appel réseau du client à Alvin.
  2. Appel réseau d'Alvin au magasin de données.
  3. Recherchez sur le disque dans le magasin de données.
  4. Appel réseau de l'entrepôt de données à Alvin.
  5. Appel réseau d'Alvin à un client.

Essayons de rayer quelques points.

Le stockage des données n'a rien à voir avec ça

La première chose que j'ai faite a été de convertir Alvin en un serveur ping-ping qui ne traite pas les requêtes. Lorsqu'il reçoit une requête, il renvoie une réponse vide. Si la latence diminue, alors un bug dans l’implémentation d’Alvin ou de l’entrepôt de données n’est pas inhabituel. Dans la première expérience, nous obtenons le graphique suivant :

Parfois, plus c'est moins. Lorsque la réduction de la charge entraîne une augmentation de la latence

Comme vous pouvez le constater, il n'y a aucune amélioration lors de l'utilisation du serveur ping-ping. Cela signifie que l’entrepôt de données n’augmente pas la latence et que la liste des suspects est réduite de moitié :

  1. Appel réseau du client à Alvin.
  2. Appel réseau d'Alvin à un client.

Super! La liste se réduit rapidement. Je pensais avoir presque compris la raison.

gRPC

Il est maintenant temps de vous présenter un nouveau joueur : gRPC. Il s'agit d'une bibliothèque open source de Google pour la communication en cours RPC. Bien que gRPC bien optimisé et largement utilisé, c'était la première fois que je l'utilisais sur un système de cette taille et je m'attendais à ce que mon implémentation soit pour le moins sous-optimale.

disponibilité gRPC dans la pile a soulevé une nouvelle question : c'est peut-être mon implémentation ou moi-même gRPC causant un problème de latence ? Ajout d'un nouveau suspect à la liste :

  1. Le client appelle la bibliothèque gRPC
  2. bibliothèque gRPC effectue un appel réseau à la bibliothèque sur le client gRPC sur le serveur
  3. bibliothèque gRPC contacte Alvin (pas d'opération en cas de serveur ping-pong)

Pour vous donner une idée de ce à quoi ressemble le code, mon implémentation client/Alvin n'est pas très différente de celle client-serveur exemples asynchrones.

Remarque : La liste ci-dessus est un peu simplifiée car gRPC permet d'utiliser votre propre modèle de thread (modèle ?), dans lequel la pile d'exécution est entrelacée gRPC et la mise en œuvre par les utilisateurs. Par souci de simplicité, nous nous en tiendrons à ce modèle.

Le profilage va tout régler

Après avoir barré les magasins de données, je pensais avoir presque terminé : « Maintenant, c'est facile ! Appliquons le profil et découvrons où se produit le retard. je grand fan du profilage de précision, car les processeurs sont très rapides et ne constituent généralement pas un goulot d'étranglement. La plupart des retards se produisent lorsque le processeur doit arrêter le traitement pour faire autre chose. C'est exactement ce que fait le profilage précis du processeur : il enregistre tout avec précision changements de contexte et indique clairement où se produisent les retards.

J'ai pris quatre profils : avec un QPS élevé (faible latence) et avec un serveur ping-pong avec un faible QPS (latence élevée), aussi bien côté client que côté serveur. Et juste au cas où, j'ai également pris un exemple de profil de processeur. Lorsque je compare des profils, je recherche généralement une pile d’appels anormale. Par exemple, du mauvais côté, 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 était presque le même. À ma grande horreur, il n’y avait rien d’important là-bas.

Débogage supplémentaire

J'étais désespéré. Je ne savais pas quels autres outils je pouvais utiliser, et mon plan suivant consistait essentiellement à répéter les expériences avec différentes variantes plutôt que de diagnostiquer clairement le problème.

Et qu'est-ce qui se passerait si

Dès le début, j'étais préoccupé par la latence spécifique de 50 ms. C'est un très grand moment. J'ai décidé de supprimer des morceaux du code jusqu'à ce que je puisse déterminer exactement quelle partie était à l'origine de cette erreur. Puis vint une expérience qui a fonctionné.

Comme d’habitude, avec le recul, il semble que tout était évident. J'ai placé le client sur la même machine qu'Alvin - et j'ai envoyé une demande à localhost. Et l’augmentation de la latence a disparu !

Parfois, plus c'est moins. Lorsque la réduction de la charge entraîne une augmentation de la latence

Quelque chose n'allait pas avec le réseau.

Acquérir les compétences d’un ingénieur réseau

Je dois l'admettre : ma connaissance des technologies de réseau est terrible, d'autant plus que je travaille avec elles quotidiennement. 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 tracert semblait être un bon début pour déboguer les problèmes de transport réseau.

Tout d'abord, j'ai lancé PsPing au port TCP d'Alvin. J'ai utilisé les paramètres par défaut – rien de spécial. Sur plus d'un millier de pings, aucun n'a dépassé les 10 ms, à l'exception du premier pour l'échauffement. Ceci est contraire à l'augmentation observée de la latence de 50 ms au 99e centile : là, pour 100 requêtes, nous aurions dû voir environ une requête avec une latence de 50 ms.

Puis j'ai essayé tracert: Il peut y avoir un problème au niveau de l'un des nœuds le long de la route entre Alvin et le client. Mais le traceur est lui aussi revenu les mains vides.

Ce n'était donc pas mon code, l'implémentation de gRPC ou le réseau qui était à l'origine du retard. Je commençais à craindre de ne jamais comprendre cela.

Maintenant, sur quel système d'exploitation sommes-nous

gRPC largement utilisé sous Linux, mais exotique sous Windows. J'ai décidé de tenter une expérience qui a fonctionné : j'ai créé une machine virtuelle Linux, compilé Alvin pour Linux et l'ai déployé.

Parfois, plus c'est moins. Lorsque la réduction de la charge entraîne une augmentation de la latence

Et voici ce qui s'est passé : le serveur de ping-pong Linux n'a pas eu les mêmes délais qu'un hôte Windows similaire, même si la source de données n'était pas différente. Il s'avère que le problème réside dans 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 ce que c'est vraiment gRPC Le drapeau Windows est manquant. J'ai trouvé une bibliothèque RPC interne qui, j'étais sûr, fonctionnerait bien pour tous les indicateurs définis Winsock. Ensuite, j'ai ajouté tous ces drapeaux à gRPC et déployé Alvin sur Windows, dans un serveur de ping-pong Windows corrigé !

Parfois, plus c'est moins. Lorsque la réduction de la charge entraîne une augmentation de la latence

Presque Terminé : j'ai commencé à supprimer les indicateurs ajoutés un par un jusqu'à ce que la régression revienne afin de pouvoir en identifier la cause. C'était infâme TCP_NODELAY, le commutateur d'algorithme de Nagle.

L'algorithme de Nagle tente de réduire le nombre de paquets envoyés sur un 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 le système d'exploitation retardera certains messages, provoquant des retards en cas de faible QPS. U gRPC cet indicateur a été défini dans l'implémentation Linux pour les sockets TCP, mais pas dans Windows. je suis ce fixe.

Conclusion

La latence plus élevée à faible QPS était due à l'optimisation du système d'exploitation. Rétrospectivement, le profilage n'a pas détecté de latence car il a été effectué en mode noyau plutôt qu'en mode noyau. mode utilisateur. Je ne sais pas si l'algorithme de Nagle peut être observé via les captures ETW, mais ce serait intéressant.

Quant à l'expérience localhost, elle n'a probablement pas touché au code réseau réel et l'algorithme de Nagle n'a pas fonctionné, donc les problèmes de latence ont disparu lorsque le client a atteint Alvin via localhost.

La prochaine fois que vous constaterez une augmentation de la latence à mesure que le nombre de requêtes par seconde diminue, l'algorithme de Nagle devrait figurer sur votre liste de suspects !

Source: habr.com

Ajouter un commentaire