Chez Achievers, après avoir développé un framework pour les tests de charge afin d'aider nos équipes d'ingénierie à comprendre les performances de notre plateforme quand elle est soumise à une charge imprévue, l'équipe a pu améliorer les performances générales de la plateforme, ce qui a permis un débit de trafic 4 fois plus important et une scalabilité plus efficace des clusters. Ce blog examine les objectifs définis à partir des résultats de la base de référence initiale et comment nous avons résolu les goulots d’étranglement au cours de la phase de test. Après avoir obtenu un bon rapport sur l'état actuel de la plateforme, il était temps de dépasser la base de référence.

Dépassement de la base de référence

Examinons rapidement les métriques que nous avons monitorées et les rapports générés.

  • Débit : le nombre de demandes par secondes pouvant être traitées par le système.
  • Erreurs : le nombre de demandes qui ont échoué au cours de la période de test.
  • Latence : le temps nécessaire au traitement d'une demande à partir du moment où elle est envoyée.
  • Scalabilité : la capacité de la plateforme à traiter des charges de plus en plus importantes sans dégradation significative des performances.

Après avoir mis en évidence les métriques ci-dessus, nous avons défini un objectif pour chacune d'elles. Le but était de mesurer si nous pouvions effectivement réussir à améliorer notre système.

graphique

Objectifs de résultats : mesurer la réussite des tests de charge

Pour mesurer les résultats des tests et faire le suivi des attentes, nous avons créé des dashboards d'observabilité intégrés à New Relic. Les dashboards ont préparé l'équipe à rapidement identifier les goulots d’étranglement et les problèmes qui se produisent pendant la période de test.

Après avoir revu les dashboards, il était évident que les goulots d’étranglement bloquaient les performances de notre plateforme.

Optimisation du débit

Équilibrage de la charge avec Istio

gRPC garde les sessions TCP ouvertes le plus longtemps possible pour optimiser le débit et minimiser les frais, mais les sessions longues complexifient l'équilibrage de charge. Cela est particulièrement un problème dans les environnements Kubernetes ajustés automatiquement. Lorsque la charge augmente, de nouveaux pods sont ajoutés, mais le client reste connecté aux anciens pods gRPC, ce qui entraîne une distribution inégale de la charge.

Vous trouverez ci-dessous un bon exemple de trafic gRPC allant vers le déploiement, mais non non équilibré.

graphique

Dashboard New Relic montrant un déséquilibre des demandes par seconde (RPS)

Vous pouvez voir que nous obtenons des demandes sporadiques pour chaque pod, avec un pod qui n'a aucun trafic. Istio nous aide à distribuer les demandes en partageant les informations de connexion entre les proxies Envoy. Les proxies Envoy partagent le nombre de demandes reçues et Istio peut déterminer quel est le pod le moins occupé qui peut mieux répondre à la demande.

envoy

La configuration de l'équilibrage de charge sur votre DestinationRule, pour qu'elle utilise LEAST_REQUEST au lieu du routage en tourniquet par défaut, a aidé à résoudre le problème d'un trafic déséquilibré.

code

DestinationRule avec LEAST_REQUEST

Après le déploiement de la nouvelle configuration loadBalancer, les résultats montrent une distribution équitable des demandes entre les pods et une forte augmentation du débit sur tous les services.

graphique

Résultat : une distribution équilibrée équitablement des demandes par pod

Erreurs

Côté client

Tous les goulots d’étranglement se trouvent sur le serveur. Nous avons remarqué quelques goulots d’étranglement sur le client testant la charge qui a généré la charge, ce qui a résulté en un taux d’erreur élevé. Bien que nous ayons suffisamment d'UC/de mémoire pour exécuter nos tests, certaines limites ont été révélées sur notre réseau.

Les machines virtuelles Google Cloud sans adresse IP externe envoient le trafic sortant via un Cloud NAT. Lors de l'exécution d'un test de charge depuis un autre projet GCP, le trafic de sortie a envoyé un pic d'erreurs OUT_OF_RESOURCES pendant la période de test.

graphique

Fort taux d’erreur sur les connexions de sortie pendant le test

L'équipe a ajouté des adresses IP supplémentaires et a fait passer les « ports autorisés vers une adresse de destination » vers Google Cloud NAT. Vous pouvez voir dans l'image ci-dessous que nous touchons une limite horizontale avant de résoudre le problème à la fin du graphique.

graphique

Les lignes horizontales montrent les ressources épuisées — la droite de l'image montre le problème résolu

Une solution à long terme consisterait à passer complètement à IPV6 pour supprimer les limites de connexion de sortie. Achievers prévoit d'être sur IPV6 pour tout notre réseau en 2024.

Latence

Configuration Istio

Étant donné que tout le trafic des clusters passe par le proxy Istio Envoy, nous pensions que nous aurions les moyens d'améliorer la latence. Au cours de l'enquête, nous n'avons pas vu de longues transactions en attente dans Envoy, mais nous pensions que c'était un bon moment pour voir si nous pouvions améliorer Istio en ajustant certains aspects de la configuration. La simultanéité Istio est le nombre de fils de workers exécutés sur le sidecar du proxy Envoy. Si elle n'est pas définie, la valeur par défaut est 2. Si elle est définie sur 0, tous les cores de la machine sont utilisés sur la base des demandes et des limites de l'UC ; ces valeurs sont utilisées pour les contrôler, les limites prévalant sur les demandes.

Résultats de la simultanéité :

4 workers => 4434,11/s + p(95)=114,67 ms

6 workers => 4230,42/s + p(95)=98,3 ms

2 workers => 4519,10/s + p(95)=100,75 ms

Dans notre cas, la simultanéité Istio ne semble avoir aucun impact sur les performances. Cet exercice nous a permis de voir si cette solution serait avantageuse pour nous même si au bout du compte il n'y a pas eu de gains significatifs.

Améliorations du code

Un code qui est partagé peut souvent introduire des problèmes sur tous les services. Dans le cas, par exemple, d'un bogue dans notre connecteur de base de données qui cause une latence légèrement plus importante des connexions de sorties.

tableau

Les appels supplémentaires à la base de données ont causé une latence inutile — les lignes rouges montrent une violation des SLO (mesurée en millisecondes).

En utilisant le tracing de New Relic, nous avons pu facilement repérer un modèle. Dans l'image ci-dessus, vous pouvez voir que les résultats de la latence étaient élevés au niveau des points de terminaison dont les appels de base de données étaient lents. Des verrous de bases de données étaient utilisés pour passer au schéma nécessaire avant d'exécuter la demande. Le maintien de ces verrous prend plus de temps. Le problème a été résolu en supprimant un appel de changement des schémas et en ajoutant à la place le nom du schéma à la requête.

Après l'optimisation de la bibliothèque, une amélioration globale du temps de réponse moyen pour le service a été constatée.

tableau

Après l'amélioration des appels de base de données, le temps de réponse moyen a nettement baissé (mesuré en millisecondes).

Un autre problème de code a été révélé sur un ancien point de terminaison qui existait dans notre monolithe. Les performances du point de terminaison étaient vraiment médiocres et exigeaient souvent jusqu'à 10 secondes avant de compléter une demande.

point de terminaison

Un point de terminaison prenait 10 secondes pour compléter une demande.

Après la mise à niveau du point de terminaison et l'optimisation des requêtes de base de données, nous avons réussi à le réduire à quelques millisecondes.

graphique

Résultat après les améliorations apportées au code pour réduire la latence.

Mise en cache

l

Mémoire insuffisante sur un pod Kubernetes

De grandes améliorations au niveau de la mise en cache ont pu être implémentées en déplaçant des images vers le CDN pour améliorer les temps de réponse. Quelques goulots d’étranglement étaient situés au niveau des services qui dépendent lourdement du cache Redis. Bien que Redis ne devrait pas constituer une dépendance pour les services, il améliore nettement les performances pour les applications.

En examinant les graphiques de ressources dans New Relic, nous avons découvert que les ressources de Redis étaient épuisées dans deux ou trois namespaces critiques. Les deux lignes violettes dépassent la limite de Kubernetes, ce qui révèle l'épuisement de la mémoire et des erreurs de mémoire insuffisante (OOM).

l

Dashboard New Relic montrant l'usage élevé de la mémoire sur les masters Redis pendant les tests de charge

Une rapide demande de tirage pour pousser les demandes Redis Kubernetes a rapidement résolu l'épuisement de la mémoire. Globalement, cet exercice était excellent pour tester l'impact sur les performances des services fortement mis en cache lorsque les ressources Redis arrivent à saturation.

Scalabilité

Les nœuds sont les machines virtuelles sur lesquelles un cluster Kubernetes exécute les workloads. Bien que les machines virtuelles puissent être éphémères et aller et venir, vous devez garantir les bonnes performances et la scalabilité de votre cluster. Une fois que vous êtes passé par des améliorations de base telles que l'ajustement de l'UC du nœud, la mémoire et le disque, vous devriez examiner quelques-unes des autres modifications apportées ci-dessous, qui garantissent que votre infrastructure évolue efficacement.

Limitations de l'adresse IP

Les clusters sont créés avec des plages IP et souvent, ces valeurs ne sont jamais modifiées. Au début du test, nous nous sommes vite rendu compte que notre cluster pouvait assez rapidement atteindre la limite du nombre de workers qu'il contenait. Kubernetes prend le réseau du pod et le répartit sur le réseau des workers, ce qui limite le nombre de workers dans vos nodepools. En fonction de la valeur max_pods_per_node, la création de nœuds est liée au nombre d'adresses dans la plage d'adresse du pod.

Coût annuel médian des pannes

Exemple : la scalabilité s'est aplanie au cours d'un test de charge sur un cluster Kubernetes

Par exemple, si vous définissez le nombre maximal de pods par défaut sur 110 et la plage d'adresse IP secondaire des pods sur /21, Kubernetes attribue une plage CIDR /24 aux nœuds sur le cluster. Ceci autorise un maximum de 2(24-21) = 23 = 8 nœuds sur le cluster.

La limite des adresses IP peut résulter en une incapacité pour les nœuds du cluster d'évoluer et à l'étranglement des workloads ou pire, le plantage en raison de l'épuisement des ressources. Nous pouvons facilement résoudre ce problème, car nous avons simplement besoin d'ajuster certaines des plages IP attribuées aux pods et aux workers. Google a également présenté des « plages IP secondaires » qui peuvent aider les équipes qui doivent recréer leurs clusters pour résoudre ce problème.

TopologySpreadConstraints

Les déploiements Kubernetes fortement utilisés peuvent devoir être distribués entre différents workers pour empêcher l'épuisement des ressources. L'utilisation des pods  topologySpreadConstraints est répartie entre les domaines d'échec des clusters tels que les régions, zones, hôtes et d'autres topologies définies par l'utilisateur.

Nous utilisons souvent cette fonctionnalité sur nos services critiques pour assurer la scalabilité efficace des clusters Kubernetes. Lorsque Unsatisfiable est défini sur DoNotSchedule, le cluster peut être obligé d'évoluer si plus de deux pods sont alloués à chaque worker.

spread

Si nous laissons le cluster évoluer en fonction des services critiques, nous nous assurons que nous avons suffisamment d'UC et de mémoire pendant un pic de trafic. L'ajustement de cette valeur avec les valeurs de scalabilité moyennes a permis au cluster d'évoluer 10 fois plus efficacement.

Conclusion

Après avoir résolu les gros goulots d’étranglement, il était temps de revoir nos objectifs initiaux. Les résultats étaient prêts !

graphique

L'équipe a pu atteindre nos objectifs dans tous les cas et elle a même souvent dépassé nos attentes. Nous étions soulagés de voir que nous n'avions pas eu d'impact sur les SLO que nous avions définis pour les erreurs ou la latence à la fin de notre test initial. Nous avons multiplié par 4 le débit et cette amélioration nous a permis de traiter en toute confiance le trafic de basculement dans une seule région pendant une panne. Globalement, la scalabilité du cluster a également été améliorée en introduisant des ressources de tailles appropriées, parallèlement au traitement des demandes d'équilibrage de la charge gRPC par Istio.

Bien que des améliorations soient toujours possibles, la plateforme Achievers dispose maintenant d'un framework flexible permettant de continuellement tester la charge et mesurer les performances.