Recherche

Kubernetes : les réflexions de Publicis Sapient sur le load balancing

Confronté à une mauvaise répartition de charge sur une application Kubernetes, Publicis Sapient a exploré trois options de load balancing.

Publié par Clément Bohic le | Mis à jour le
Lecture
5 min
  • Imprimer
Kubernetes : les réflexions de Publicis Sapient sur le load balancing
© Maksim Kabakou - Adobe Stock

Soit deux microservices "Panier" et "Produit". Pourquoi le downscaling du premier perturbe-t-il l'équilibrage de la charge vers le second ?

Chez Publicis Sapient, on s'est retrouvé confronté à la question dans un environnement AKS. Un spécialiste plate-forme a saisi l'occasion pour apporter une grille de lecture de trois options de load balancing sur Kubernetes.

Un problème dans les bibliothèques clientes

"Panier", composé de 60 pods, interagissait avec "Produit" (40 pods), lequel communiquait ensuite avec un service externe ("Prix"). Celui-ci connut des erreurs 5xx à répétition. Pour atténuer l'impact, "Panier" avait été redimensionné à 10 pods. Cela avait réduit la charge sur "Produit" et "Prix". Mais avec une conséquence inattendue : la distribution inégales des requêtes dont "Produit" était destinataire. Elles apparaissaient complètement aléatoires, tant en nombre qu'en intervalle et en durée. L'autoscaler a réagi en créant des pods supplémentaires... restés toutefois largement inutilisés. Le problème a persisté jusqu'au redimensionnement de "Panier" à 60 pods.

L'équipe du projet avait déterminé que la situation n'était pas le fait de Kubernetes. En particulier au vu de l'absence d'affinité de session, explicitée dans la description du service "Produit".
Le problème se trouvait en fait dans le framework Java Micronaut, utilisé par "Panier" pour les communications HTTP(S). La bibliothèque cliente favorise les connexions persistantes (keep-alive). Elle n'est pas la seule du genre. Le problème s'est avéré reproductible, entre autres, avec des applications Node.js.

Un "effet d'échelle" sur le load balancing

Cette tendance à réutiliser les connexions TCP permet d'optimiser les performances. Mais lorsque ces dernières expirent, le système peut se retrouver perturbé. En tout cas si une application s'appuie sur un client qui se connecte toujours au même pod. L'algorithme d'iptables - mode par défaut de kube-proxy - ne choisit en effet pas forcément la même règle NAT à chaque traitement d'une nouvelle connexion. Solutions envisageables : configurer des timeouts appropriés, exploiter l'affinité de session... ou concevoir des applications sans état résistantes aux erreurs.

Dans ce contexte, le load balancing est tributaire d'une forme d'effet d'échelle. Lorsque les clients sont plus nombreux que les serveurs, la charge est généralement bien distribuée. Dans le scénario inverse, chaque client ne tente de créer qu'un nombre limité de connexions, chacune "adhérant" à un pod spécifique en raison du système de suivi d'iptables. Si la connexion ne prend pas fin et que les clients ne génèrent pas davantage de requêtes, iptables lui-même ne crée pas de nouvelles connexions... et laisse donc orphelins des pods orphelins en back-end. Parmi les solutions envisageables outre un dimensionnement adapté à la charge : permettre aux clients de créer davantage de connexions concurrentes, recourir une fois encore à l'affinité de session (avec prudence au risque d'accroître le déséquilibre de charge)... ou utiliser un service mesh.

1 - Load balancing côté client

L'application connaît les adresses (IP + port) de tous les pods en back-end. Elle utilise un algorithme de load balacing et établit des connexions directes.

Cette approche offre une latence réduite (contournement de kube-proxy) et la possibilité d'explorer divers algorithmes de load balancing. Elle favorise aussi l'observabilité tout en permettant de réutiliser les connexions.
Le code de l'application s'en trouve cepenant complexifié. Et cette dernière devient étroitement liée à l'environnement (des changements sur l'API ou sur l'infra réseau peuvent exiger des modifications de code). Il peut par ailleurs être difficile de maintenir à jour la liste des pods. On l'évitera donc si on souhaite de la simplicité, si la répartition de charge de Kubernetes suffit ou si on souhaite exploiter pleinement la stack réseau de l'orchestrateur.

2 - Pattern side-car

On déploie un proxy léger (Envoy, HAProxy, Nginx...) sous forme de conteneur dans le pod de l'application.

La logique de load balancing se trouve ainsi découplée du code de l'app. Le side-car est en outre utilisable peu importe le langage de programmation. La gestion des stratégies de répartition de charge est centralisée et l'observabilité, améliorée.
Cette approche augmente toutefois la consommation de ressources, ajoute un saut de réseau et crée un point de défaillance unique (si le side-car tombe en panne, l'application ne peut plus communiquer avec le monde extérieur).

On pourra envisager cette option lorsqu'il s'agit de gérer un grand nombre de microservices ou qu'il y a besoin d'appliquer au trafic des stratégies de sécurité strictes. À l'inverse, on l'évitera pour les applications simples aux exigences de load balancing minimales, ainsi qu'à celles qui nécessitent une faible latence.

3 - Service mesh

On s'appuie sur le pattern side-car, en créant un maillage entre chaque pod d'application doté de son proxy.

Cette approche présente une certaine exhaustivité fonctionnelle (A/B testing, circuit breaking...), une sécurité améliorée (mTLS, stratégies d'authentification et d'autorisation...) et permet une collecte détaillée de métriques/logs. La logique de routage et de load balancing est, de surcroît, indépendante de l'application.
Le service mesh consomme néanmoins des ressources, suppose une courbe d'apprentissage et peut se révéler difficile à intégrer, surtout avec des applications pas conçues pour. Il porte également un risque de verrouillage (fonctionnalités et API différentes entre service meshes).

On envisagera un service mesh pour gérer des microservices à grande échelle qui exploitent une diversité de protocoles, ont besoin d'une sécurité robuste (authentification, autorisation, chiffrement), de résilience et d'une observabilité avancée. En fonction de l'expertise et des ressources informatiques dont on dispose, on l'évitera éventuellement. Tout comme en cas de présence d'applications monolithiques et/ou de systèmes hérités.

Illustration © Maksim Kabakou - Adobe Stock

Sur le même thème

Voir tous les articles Cloud

Livres Blancs #workspace

Voir tous les livres blancs

Vos prochains événements

Voir tous les événements

Voir tous les événements

S'abonner
au magazine
Se connecter
Retour haut de page
OSZAR »