Transition de Tinder vers Kubernetes

Noter. trad.: Les employés du service Tinder de renommée mondiale ont récemment partagé quelques détails techniques sur la migration de leur infrastructure vers Kubernetes. Le processus a duré près de deux ans et a abouti au lancement d'une plateforme à très grande échelle sur K8, composée de 200 services hébergés sur 48 XNUMX conteneurs. Quelles difficultés intéressantes les ingénieurs de Tinder ont-ils rencontrés et à quels résultats sont-ils parvenus ? Lisez cette traduction.

Transition de Tinder vers Kubernetes

Pourquoi?

Il y a presque deux ans, Tinder a décidé de migrer sa plateforme vers Kubernetes. Kubernetes permettrait à l'équipe Tinder de conteneuriser et de passer en production avec un minimum d'effort grâce à un déploiement immuable (déploiement immuable). Dans ce cas, l’assemblage des applications, leur déploiement et l’infrastructure elle-même seraient définis de manière unique par le code.

Nous recherchions également une solution au problème d’évolutivité et de stabilité. Lorsque la mise à l'échelle devenait critique, nous devions souvent attendre plusieurs minutes pour que de nouvelles instances EC2 démarrent. L'idée de lancer des conteneurs et de commencer à desservir le trafic en quelques secondes au lieu de quelques minutes nous est devenue très attractive.

Le processus s'est avéré difficile. Lors de notre migration début 2019, le cluster Kubernetes a atteint une masse critique et nous avons commencé à rencontrer divers problèmes dus au volume de trafic, à la taille du cluster et au DNS. En cours de route, nous avons résolu de nombreux problèmes intéressants liés à la migration de 200 services et à la maintenance d'un cluster Kubernetes composé de 1000 15000 nœuds, 48000 XNUMX pods et XNUMX XNUMX conteneurs en cours d'exécution.

Comment?

Depuis janvier 2018, nous avons traversé différentes étapes de migration. Nous avons commencé par conteneuriser tous nos services et les déployer dans des environnements cloud de test Kubernetes. Dès octobre, nous avons commencé à migrer méthodiquement tous les services existants vers Kubernetes. En mars de l’année suivante, nous avons terminé la migration et désormais la plateforme Tinder fonctionne exclusivement sur Kubernetes.

Créer des images pour Kubernetes

Nous disposons de plus de 30 référentiels de code source pour les microservices exécutés sur un cluster Kubernetes. Le code de ces référentiels est écrit dans différents langages (par exemple, Node.js, Java, Scala, Go) avec plusieurs environnements d'exécution pour le même langage.

Le système de build est conçu pour fournir un « contexte de build » entièrement personnalisable pour chaque microservice. Il se compose généralement d'un Dockerfile et d'une liste de commandes shell. Leur contenu est entièrement personnalisable, et en même temps, tous ces contextes de build sont écrits selon un format standardisé. La standardisation des contextes de build permet à un seul système de build de gérer tous les microservices.

Transition de Tinder vers Kubernetes
Figure 1-1. Processus de construction standardisé via le conteneur Builder

Pour obtenir une cohérence maximale entre les environnements d'exécution (environnements d'exécution) le même processus de construction est utilisé pendant le développement et les tests. Nous avons été confrontés à un défi très intéressant : nous devions développer un moyen d'assurer la cohérence de l'environnement de build sur l'ensemble de la plateforme. Pour y parvenir, tous les processus d’assemblage sont effectués dans un conteneur spécial. Constructeur.

Sa mise en œuvre de conteneurs nécessitait des techniques Docker avancées. Builder hérite de l'ID utilisateur local et des secrets (tels que la clé SSH, les informations d'identification AWS, etc.) requis pour accéder aux référentiels privés Tinder. Il monte des répertoires locaux contenant des sources pour stocker naturellement les artefacts de construction. Cette approche améliore les performances car elle élimine le besoin de copier les artefacts de build entre le conteneur Builder et l'hôte. Les artefacts de build stockés peuvent être réutilisés sans configuration supplémentaire.

Pour certains services, nous avons dû créer un autre conteneur pour mapper l'environnement de compilation à l'environnement d'exécution (par exemple, la bibliothèque bcrypt Node.js génère des artefacts binaires spécifiques à la plate-forme lors de l'installation). Pendant le processus de compilation, les exigences peuvent varier d'un service à l'autre et le Dockerfile final est compilé à la volée.

Architecture et migration des clusters Kubernetes

Gestion de la taille des clusters

Nous avons décidé d'utiliser kube-aws pour le déploiement automatisé de clusters sur les instances Amazon EC2. Au tout début, tout fonctionnait dans un pool commun de nœuds. Nous avons rapidement réalisé la nécessité de séparer les charges de travail par taille et par type d'instance pour utiliser les ressources plus efficacement. La logique était que l'exécution de plusieurs pods multithread chargés s'avérait plus prévisible en termes de performances que leur coexistence avec un grand nombre de pods monothread.

Au final, nous avons opté pour :

  • m5.4xlarge — pour la surveillance (Prometheus);
  • c5.4xlarge - pour la charge de travail Node.js (charge de travail monothread) ;
  • c5.2xlarge - pour Java et Go (charge de travail multithread) ;
  • c5.4xlarge — pour le panneau de commande (3 nœuds).

Migration

L'une des étapes préparatoires à la migration de l'ancienne infrastructure vers Kubernetes consistait à rediriger la communication directe existante entre les services vers les nouveaux équilibreurs de charge (Elastic Load Balancers (ELB). Ils ont été créés sur un sous-réseau spécifique d'un cloud privé virtuel (VPC). Ce sous-réseau était connecté à un VPC Kubernetes. Cela nous a permis de migrer les modules progressivement, sans tenir compte de l'ordre spécifique des dépendances des services.

Ces points de terminaison ont été créés à l’aide d’ensembles pondérés d’enregistrements DNS dont les CNAME pointaient vers chaque nouvel ELB. Pour basculer, nous avons ajouté une nouvelle entrée pointant vers le nouvel ELB du service Kubernetes avec un poids de 0. Nous avons ensuite fixé le Time To Live (TTL) de l'entrée à 0. Après cela, l'ancien et le nouveau poids ont été s'est lentement ajusté et finalement 100 % de la charge a été envoyée vers un nouveau serveur. Une fois la commutation terminée, la valeur TTL est revenue à un niveau plus adéquat.

Les modules Java dont nous disposions pouvaient gérer un DNS TTL faible, mais pas les applications Node. L'un des ingénieurs a réécrit une partie du code du pool de connexions et l'a intégré dans un gestionnaire qui mettait à jour les pools toutes les 60 secondes. L’approche choisie a très bien fonctionné et sans aucune dégradation notable des performances.

Les leçons

Les limites de la structure réseau

Au petit matin du 8 janvier 2019, la plateforme Tinder s'est écrasée de manière inattendue. En réponse à une augmentation indépendante de la latence de la plateforme plus tôt dans la matinée, le nombre de pods et de nœuds dans le cluster a augmenté. Cela a entraîné l'épuisement du cache ARP sur tous nos nœuds.

Il existe trois options Linux liées au cache ARP :

Transition de Tinder vers Kubernetes
(source)

gc_thresh3 - c'est une limite stricte. L'apparition d'entrées de « débordement de table voisine » dans le journal signifiait que même après un garbage collection (GC) synchrone, il n'y avait pas assez d'espace dans le cache ARP pour stocker l'entrée voisine. Dans ce cas, le noyau a simplement supprimé complètement le paquet.

Nous utilisons Flanelle en tant que structure réseau dans Kubernetes. Les paquets sont transmis via VXLAN. VXLAN est un tunnel L2 élevé au-dessus d'un réseau L3. La technologie utilise l'encapsulation MAC-in-UDP (MAC Address-in-User Datagram Protocol) et permet l'expansion des segments réseau de couche 2. Le protocole de transport sur le réseau physique du centre de données est IP plus UDP.

Transition de Tinder vers Kubernetes
Figure 2–1. Diagramme de flanelle (source)

Transition de Tinder vers Kubernetes
Figure 2-2. Forfait VXLAN (source)

Chaque nœud de travail Kubernetes alloue un espace d'adressage virtuel avec un masque /24 à partir d'un bloc /9 plus grand. Pour chaque nœud, c'est moyen une entrée dans la table de routage, une entrée dans la table ARP (sur l'interface flannel.1) et une entrée dans la table de commutation (FDB). Ils sont ajoutés la première fois qu'un nœud de travail est démarré ou chaque fois qu'un nouveau nœud est découvert.

De plus, la communication nœud-pod (ou pod-pod) passe finalement par l'interface eth0 (comme le montre le diagramme de flanelle ci-dessus). Cela entraîne une entrée supplémentaire dans la table ARP pour chaque hôte source et destination correspondant.

Dans notre environnement, ce type de communication est très courant. Pour les objets de service dans Kubernetes, un ELB est créé et Kubernetes enregistre chaque nœud auprès de l'ELB. L'ELB ne sait rien des pods et le nœud sélectionné peut ne pas être la destination finale du paquet. Le fait est que lorsqu'un nœud reçoit un paquet de l'ELB, il le considère en tenant compte des règles iptables pour un service spécifique et sélectionne au hasard un pod sur un autre nœud.

Au moment de la panne, le cluster comptait 605 nœuds. Pour les raisons évoquées ci-dessus, cela était suffisant pour surmonter l'importance gc_thresh3, qui est la valeur par défaut. Lorsque cela se produit, non seulement les paquets commencent à être supprimés, mais tout l'espace d'adressage virtuel Flannel avec un masque /24 disparaît de la table ARP. La communication nœud-pod et les requêtes DNS sont interrompues (le DNS est hébergé dans un cluster ; lisez plus loin dans cet article pour plus de détails).

Pour résoudre ce problème, vous devez augmenter les valeurs gc_thresh1, gc_thresh2 и gc_thresh3 et redémarrez Flannel pour réenregistrer les réseaux manquants.

Mise à l'échelle DNS inattendue

Au cours du processus de migration, nous avons activement utilisé DNS pour gérer le trafic et transférer progressivement les services de l'ancienne infrastructure vers Kubernetes. Nous définissons des valeurs TTL relativement faibles pour les RecordSets associés dans Route53. Lorsque l'ancienne infrastructure fonctionnait sur des instances EC2, notre configuration de résolveur pointait vers Amazon DNS. Nous avons pris cela pour acquis et l'impact du faible TTL sur nos services et sur les services Amazon (tels que DynamoDB) est passé largement inaperçu.

Lors de la migration des services vers Kubernetes, nous avons constaté que DNS traitait 250 1000 requêtes par seconde. En conséquence, les applications ont commencé à connaître des délais d’attente constants et importants pour les requêtes DNS. Cela s'est produit malgré des efforts incroyables pour optimiser et basculer le fournisseur DNS vers CoreDNS (qui, en charge maximale, atteignait 120 XNUMX pods fonctionnant sur XNUMX cœurs).

En recherchant d'autres causes et solutions possibles, nous avons découvert статью, décrivant les conditions de concurrence affectant le cadre de filtrage de paquets netfilter sous Linux. Les délais d'attente que nous avons observés, couplés à un compteur croissant insert_failed dans l’interface Flannel étaient cohérents avec les conclusions de l’article.

Le problème se produit au stade de la traduction des adresses réseau source et destination (SNAT et DNAT) et de l'entrée ultérieure dans le tableau. conntrack. L'une des solutions de contournement discutées en interne et suggérées par la communauté consistait à déplacer le DNS vers le nœud de travail lui-même. Dans ce cas:

  • SNAT n'est pas nécessaire car le trafic reste à l'intérieur du nœud. Il n'est pas nécessaire de l'acheminer via l'interface eth0.
  • DNAT n'est pas nécessaire puisque l'adresse IP de destination est locale au nœud, et non un pod sélectionné au hasard selon les règles iptables.

Nous avons décidé de nous en tenir à cette approche. CoreDNS a été déployé en tant que DaemonSet dans Kubernetes et nous avons implémenté un serveur DNS de nœud local dans résolution.conf chaque pod en définissant un drapeau --cluster-dns les équipes Kubelet . Cette solution s'est avérée efficace pour les délais d'attente DNS.

Cependant, nous avons quand même constaté des pertes de paquets et une augmentation du compteur insert_failed dans l'interface Flanelle. Cela a continué après la mise en œuvre de la solution de contournement, car nous avons pu éliminer SNAT et/ou DNAT pour le trafic DNS uniquement. Les conditions de course ont été préservées pour les autres types de trafic. Heureusement, la plupart de nos paquets sont TCP, et si un problème survient, ils sont simplement retransmis. Nous essayons toujours de trouver une solution adaptée à tous les types de trafic.

Utiliser Envoy pour un meilleur équilibrage de charge

Lors de la migration des services backend vers Kubernetes, nous avons commencé à souffrir d'une charge déséquilibrée entre les pods. Nous avons constaté que HTTP Keepalive provoquait le blocage des connexions ELB sur les premiers pods prêts de chaque déploiement déployé. Ainsi, l’essentiel du trafic passait par un faible pourcentage de pods disponibles. La première solution que nous avons testée consistait à définir MaxSurge à 100 % sur les nouveaux déploiements pour les pires scénarios. L’effet s’est avéré insignifiant et peu prometteur en termes de déploiements à plus grande échelle.

Une autre solution que nous avons utilisée consistait à augmenter artificiellement les demandes de ressources pour les services critiques. Dans ce cas, les pods placés à proximité auraient plus de marge de manœuvre par rapport aux autres pods lourds. Cela ne fonctionnerait pas non plus à long terme car ce serait un gaspillage de ressources. De plus, nos applications Node étaient monothread et, par conséquent, ne pouvaient utiliser qu'un seul cœur. La seule véritable solution consistait à utiliser un meilleur équilibrage de charge.

Nous avons longtemps voulu apprécier pleinement Envoyé. La situation actuelle nous a permis de le déployer de manière très limitée et d’obtenir des résultats immédiats. Envoy est un proxy de couche XNUMX open source hautes performances conçu pour les grandes applications SOA. Il peut mettre en œuvre des techniques avancées d’équilibrage de charge, notamment des tentatives automatiques, des disjoncteurs et une limitation globale du débit. (Noter. trad.: Vous pouvez en savoir plus à ce sujet dans cet article à propos d'Istio, qui est basé sur Envoy.)

Nous avons proposé la configuration suivante : disposer d'un side-car Envoy pour chaque pod et d'une seule route, et connecter le cluster au conteneur localement via le port. Pour minimiser les cascades potentielles et maintenir un petit rayon d'action, nous avons utilisé une flotte de modules de proxy frontal Envoy, un par zone de disponibilité (AZ) pour chaque service. Ils se sont appuyés sur un simple moteur de découverte de services écrit par l'un de nos ingénieurs qui renvoyait simplement une liste de pods dans chaque AZ pour un service donné.

Les Service Front-Envoys ont ensuite utilisé ce mécanisme de découverte de services avec un cluster et une route en amont. Nous avons défini des délais d'attente adéquats, augmenté tous les paramètres des disjoncteurs et ajouté une configuration minimale de nouvelles tentatives pour faciliter les pannes uniques et garantir des déploiements fluides. Nous avons placé un TCP ELB devant chacun de ces envoyés du front de service. Même si le keepalive de notre couche proxy principale était bloqué sur certains pods Envoy, ils étaient toujours capables de mieux gérer la charge et étaient configurés pour équilibrer via least_request dans le backend.

Pour le déploiement, nous avons utilisé le hook preStop sur les pods d'application et les pods side-car. Le hook a déclenché une erreur lors de la vérification de l'état du point de terminaison d'administration situé sur le conteneur side-car et s'est mis en veille pendant un certain temps pour permettre la fin des connexions actives.

L’une des raisons pour lesquelles nous avons pu agir si rapidement est due aux mesures détaillées que nous avons pu facilement intégrer dans une installation Prometheus typique. Cela nous a permis de voir exactement ce qui se passait pendant que nous ajustions les paramètres de configuration et redistribuions le trafic.

Les résultats ont été immédiats et évidents. Nous avons commencé avec les services les plus déséquilibrés, et pour le moment il opère devant les 12 services les plus importants du cluster. Cette année, nous prévoyons une transition vers un service maillé complet avec une découverte de services, une coupure de circuit, une détection des valeurs aberrantes, une limitation de débit et un traçage plus avancés.

Transition de Tinder vers Kubernetes
Figure 3–1. Convergence CPU d'un service lors de la transition vers Envoy

Transition de Tinder vers Kubernetes

Transition de Tinder vers Kubernetes

Résultat final

Grâce à cette expérience et à des recherches supplémentaires, nous avons constitué une équipe d'infrastructure solide dotée de solides compétences dans la conception, le déploiement et l'exploitation de grands clusters Kubernetes. Tous les ingénieurs de Tinder possèdent désormais les connaissances et l'expérience nécessaires pour empaqueter des conteneurs et déployer des applications sur Kubernetes.

Lorsque le besoin de capacité supplémentaire s'est fait sentir sur l'ancienne infrastructure, nous avons dû attendre plusieurs minutes avant le lancement de nouvelles instances EC2. Désormais, les conteneurs commencent à fonctionner et à traiter le trafic en quelques secondes au lieu de quelques minutes. La planification de plusieurs conteneurs sur une seule instance EC2 permet également d'améliorer la concentration horizontale. En conséquence, nous prévoyons une réduction significative des coûts EC2019 en 2 par rapport à l’année dernière.

La migration a duré près de deux ans, mais nous l'avons achevée en mars 2019. Actuellement, la plateforme Tinder fonctionne exclusivement sur un cluster Kubernetes composé de 200 services, 1000 15 nœuds, 000 48 pods et 000 XNUMX conteneurs en cours d’exécution. L’infrastructure n’est plus le seul domaine des équipes opérationnelles. Tous nos ingénieurs partagent cette responsabilité et contrôlent le processus de création et de déploiement de leurs applications en utilisant uniquement du code.

PS du traducteur

Lisez également une série d’articles sur notre blog :

Source: habr.com

Ajouter un commentaire