Comment l'architecture Web à tolérance de pannes est implémentée dans la plate-forme Mail.ru Cloud Solutions



Bonjour, Habr! Je suis Artyom Karamyshev, chef de l'équipe d'administration système chez Mail.Ru Cloud Solutions (MCS) . Au cours de la dernière année, nous avons eu de nombreux lancements de nouveaux produits. Nous voulions que les services API évoluent facilement, soient tolérants aux pannes et prêts pour la croissance rapide de la charge utilisateur. Notre plateforme est implémentée sur OpenStack, et je veux vous dire quels problèmes de tolérance aux pannes de composants nous avons dû fermer afin d'obtenir un système tolérant aux pannes. Je pense que cela sera intéressant pour ceux qui développent également des produits sur OpenStack.

La tolérance de panne globale de la plate-forme consiste en la stabilité de ses composants. Nous allons donc progressivement passer par tous les niveaux auxquels nous avons découvert les risques et les avons fermés.

Une version vidéo de cette histoire, dont la source était un reportage lors de la conférence Uptime day 4 organisée par ITSumma , peut être visionnée sur la chaîne YouTube Uptime Community .

Tolérance aux pannes de l'architecture physique


La partie publique du cloud MCS est désormais basée dans deux centres de données de niveau III, entre eux il y a une fibre noire qui lui est propre, réservée sur la couche physique par différentes routes, avec un débit de 200 Gb / s. Le niveau de niveau III fournit le niveau nécessaire de résilience de l'infrastructure physique.

La fibre noire est réservée aux niveaux physique et logique. Le processus de réservation de canaux a été itératif, des problèmes sont survenus et nous améliorons constamment la communication entre les centres de données.

Par exemple, il n'y a pas si longtemps, lorsqu'elle travaillait dans un puits à côté de l'un des centres de données, une excavatrice a perforé un tuyau, à l'intérieur de ce tuyau, il y avait à la fois un câble optique principal et un câble optique de secours. Notre canal de communication tolérant aux pannes avec le centre de données s'est révélé vulnérable à un moment donné, dans le puits. En conséquence, nous avons perdu une partie de l'infrastructure. Nous avons tiré des conclusions, pris un certain nombre de mesures, notamment la pose d'optiques supplémentaires le long d'un puits voisin.

Dans les centres de données, il existe des points de présence de fournisseurs de communication auxquels nous diffusons nos préfixes via BGP. Pour chaque direction de réseau, la meilleure métrique est sélectionnée, ce qui permet à différents clients de fournir la meilleure qualité de connexion. Si la communication via un fournisseur est déconnectée, nous reconstruisons notre routage via les fournisseurs disponibles.

En cas de défaillance d'un fournisseur, nous passons automatiquement au suivant. En cas de défaillance de l'un des centres de données, nous avons une copie miroir de nos services dans le deuxième centre de données, qui prend tout le fardeau sur eux-mêmes.


Résilience de l'infrastructure physique

Ce que nous utilisons pour la tolérance aux pannes au niveau de l'application


Notre service est construit sur un certain nombre de composants open source.

ExaBGP est un service qui implémente un certain nombre de fonctions utilisant le protocole de routage dynamique basé sur BGP. Nous l'utilisons activement pour annoncer nos adresses IP blanches par lesquelles les utilisateurs ont accès à l'API.

HAProxy est un équilibreur très chargé qui vous permet de configurer des règles très flexibles pour équilibrer le trafic à différents niveaux du modèle OSI. Nous l'utilisons pour équilibrer tous les services: bases de données, courtiers de messages, services API, services Web, nos projets internes - tout est derrière HAProxy.

Application API - une application web écrite en python, avec laquelle l'utilisateur contrôle son infrastructure, son service.

Application de travail (ci-après simplement appelée travailleur) - dans les services OpenStack, il s'agit d'un démon d'infrastructure qui vous permet de traduire des commandes API dans l'infrastructure. Par exemple, un disque est créé dans Worker et une demande de création se trouve dans l'API d'application.

Architecture d'application OpenStack standard


La plupart des services développés pour OpenStack essaient de suivre un seul paradigme. Un service se compose généralement de 2 parties: l'API et les travailleurs (exécuteurs principaux). En règle générale, une API est une application WSGI python qui s'exécute en tant que processus autonome (démon) ou en utilisant un serveur Web Nginx prêt à l'emploi, Apache. L'API traite la demande de l'utilisateur et transmet d'autres instructions à l'application de travail. La transmission se fait à l'aide d'un courtier de messages, généralement RabbitMQ, les autres sont mal pris en charge. Lorsque les messages parviennent au courtier, ils sont traités par les employés et, si nécessaire, renvoient une réponse.

Ce paradigme implique des points de défaillance communs isolés: RabbitMQ et la base de données. Mais RabbitMQ est isolé au sein d'un service et, en théorie, peut être individuel pour chaque service. Donc, chez MCS, nous partageons ces services autant que possible, pour chaque projet individuel, nous créons une base de données distincte, un RabbitMQ distinct. Cette approche est bonne car en cas d'accident sur certains points vulnérables, pas toutes les ruptures de service, mais seulement une partie.

Le nombre d'applications de travail est illimité, de sorte que l'API peut facilement évoluer horizontalement derrière les équilibreurs afin d'augmenter la productivité et la tolérance aux pannes.

Certains services nécessitent une coordination au sein du service - lorsque des opérations séquentielles complexes se produisent entre les API et les travailleurs. Dans ce cas, un seul centre de coordination est utilisé, un système de cluster tel que Redis, Memcache, etcd, qui permet à un travailleur de dire à l'autre que cette tâche lui est assignée ("s'il vous plaît ne le prenez pas"). Nous utilisons etcd. En règle générale, les travailleurs communiquent activement avec la base de données, y écrivent et lisent des informations. En tant que base de données, nous utilisons mariadb, que nous avons dans le cluster multimaître.

Un tel service mono-utilisateur classique est organisé d'une manière généralement acceptée pour OpenStack. Il peut être considéré comme un système fermé, pour lequel les méthodes de mise à l'échelle et de tolérance aux pannes sont assez évidentes. Par exemple, pour la tolérance aux pannes de l'API, il suffit de placer un équilibreur devant eux. La mise à l'échelle des travailleurs est obtenue en augmentant leur nombre.

Les points faibles de l'ensemble du schéma sont RabbitMQ et MariaDB. Leur architecture mérite un article séparé. Dans cet article, je veux me concentrer sur la tolérance aux pannes de l'API.


Architecture d'application Openstack Équilibrage et résilience des plateformes cloud

Rendre HAProxy Balancer résilient avec ExaBGP


Pour rendre nos API évolutives, rapides et tolérantes aux pannes, nous avons placé un équilibreur devant elles. Nous avons choisi HAProxy. À mon avis, il possède toutes les caractéristiques nécessaires à notre tâche: équilibrage à plusieurs niveaux OSI, interface de gestion, flexibilité et évolutivité, un grand nombre de méthodes d'équilibrage, prise en charge des tables de session.

Le premier problème qui devait être résolu était la tolérance aux pannes de l'équilibreur lui-même. L'installation de l'équilibreur crée également un point d'échec: l'équilibreur se casse - le service tombe. Pour éviter cela, nous avons utilisé HAProxy avec ExaBGP.

ExaBGP vous permet de mettre en œuvre un mécanisme de vérification de l'état d'un service. Nous avons utilisé ce mécanisme pour vérifier la fonctionnalité de HAProxy et en cas de problème, désactiver le service HAProxy de BGP.

Schéma ExaBGP + HAProxy

  1. Nous installons le logiciel nécessaire sur trois serveurs, ExaBGP et HAProxy.
  2. Sur chacun des serveurs, nous créons une interface de bouclage.
  3. Sur les trois serveurs, nous attribuons la même adresse IP blanche à cette interface.
  4. Une adresse IP blanche est annoncée sur Internet via ExaBGP.

La tolérance aux pannes est obtenue en annonçant la même adresse IP à partir des trois serveurs. Du point de vue du réseau, la même adresse est accessible à partir de trois prochains espoirs différents. Le routeur voit trois itinéraires identiques, sélectionne le plus prioritaire d'entre eux en fonction de sa propre métrique (c'est généralement la même option), et le trafic ne va qu'à l'un des serveurs.

En cas de problèmes avec le fonctionnement HAProxy ou de défaillance du serveur, ExaBGP arrête d'annoncer l'itinéraire et le trafic bascule en douceur vers un autre serveur.

Ainsi, nous avons atteint la tolérance aux pannes de l'équilibreur.


Tolérance aux pannes des équilibreurs d'HAProxy

Le schéma s'est avéré imparfait: nous avons appris à réserver HAProxy, mais nous n'avons pas appris à répartir la charge à l'intérieur des services. Par conséquent, nous avons développé un peu ce schéma: nous sommes passés à l'équilibre entre plusieurs adresses IP blanches.

DNS Based Balancing Plus BGP


Le problème de l'équilibrage de charge avant notre HAProxy n'est pas encore résolu. Néanmoins, cela peut être résolu tout simplement, comme nous l'avons fait à la maison.

Pour équilibrer les trois serveurs, vous aurez besoin de 3 adresses IP blanches et d'un bon vieux DNS. Chacune de ces adresses est définie sur l'interface de bouclage de chaque HAProxy et est annoncée sur Internet.

OpenStack utilise un catalogue de services pour gérer les ressources, qui définit l'API de noeud final d'un service. Dans ce répertoire, nous prescrivons un nom de domaine - public.infra.mail.ru, qui se résout via DNS avec trois adresses IP différentes. Par conséquent, nous obtenons un équilibrage de charge entre les trois adresses via DNS.

Mais puisque lors de l'annonce des adresses IP blanches, nous ne contrôlons pas les priorités de sélection du serveur, jusqu'à présent, cela n'est pas équilibré. En règle générale, un seul serveur sera sélectionné par priorité de l'adresse IP et les deux autres seront inactifs, car aucune métrique n'est spécifiée dans BGP.

Nous avons commencé à proposer des itinéraires via ExaBGP avec différentes métriques. Chaque équilibreur annonce les trois adresses IP blanches, mais l'une d'entre elles, la principale pour cet équilibreur, est annoncée avec une métrique minimale. Ainsi, pendant que les trois équilibreurs fonctionnent, les appels à la première adresse IP tombent sur le premier équilibreur, les appels à la seconde à la seconde, au troisième au troisième.

Que se passe-t-il lorsque l'un des équilibreurs tombe? En cas de défaillance d'un équilibreur par sa base, l'adresse est toujours annoncée par les deux autres, le trafic entre eux est redistribué. Ainsi, nous donnons à l'utilisateur via le DNS plusieurs adresses IP à la fois. En équilibrant sur DNS et différentes métriques, nous obtenons une distribution de charge uniforme sur les trois équilibreurs. Et en même temps, nous ne perdons pas la tolérance aux fautes.


Équilibrage HAProxy basé sur DNS + BGP

Interaction entre ExaBGP et HAProxy


Nous avons donc implémenté la tolérance aux pannes en cas de départ du serveur, en fonction de la fin de l'annonce des itinéraires. Mais HAProxy peut également être déconnecté pour d'autres raisons que la défaillance du serveur: erreurs d'administration, défaillances de service. Nous voulons retirer l'équilibreur cassé sous la charge et dans ces cas, et nous avons besoin d'un autre mécanisme.

Par conséquent, en étendant le schéma précédent, nous avons implémenté un battement de cœur entre ExaBGP et HAProxy. Il s'agit d'une implémentation logicielle de l'interaction entre ExaBGP et HAProxy, lorsque ExaBGP utilise des scripts personnalisés pour vérifier l'état des applications.

Pour ce faire, dans la configuration ExaBGP, vous devez configurer un vérificateur d'intégrité capable de vérifier l'état de HAProxy. Dans notre cas, nous avons configuré le backend de santé dans HAProxy, et du côté d'ExaBGP, nous vérifions avec une simple demande GET. Si l'annonce cesse de se produire, alors HAProxy ne fonctionne probablement pas et il n'est pas nécessaire de l'annoncer.


Bilan de santé HAProxy

HAProxy Peers: synchronisation de session


La prochaine chose à faire était de synchroniser les sessions. Lorsque vous travaillez avec des équilibreurs distribués, il est difficile d'organiser le stockage des informations sur les sessions client. Mais HAProxy est l'un des rares équilibreurs qui peut le faire en raison de la fonctionnalité Peers - la possibilité de transférer des tables de session entre différents processus HAProxy.

Il existe différentes méthodes d'équilibrage: simples, telles que la répétition alternée et avancées, lorsqu'une session client est mémorisée et chaque fois qu'elle arrive sur le même serveur qu'auparavant. Nous voulions mettre en œuvre la deuxième option.

HAProxy utilise des stick-tables pour enregistrer les sessions client pour ce mécanisme. Ils enregistrent l'adresse IP source du client, l'adresse cible sélectionnée (backend) et certaines informations de service. En règle générale, les tables Stick sont utilisées pour enregistrer la paire source-IP + destination-IP, ce qui est particulièrement utile pour les applications qui ne peuvent pas transmettre le contexte de session de l'utilisateur lors du basculement vers un autre équilibreur, par exemple, en mode d'équilibrage RoundRobin.

Si la stick-table apprend à se déplacer entre différents processus HAProxy (entre lesquels l'équilibrage se produit), nos équilibreurs pourront travailler avec un seul pool de stick-tables. Cela permettra de basculer de manière transparente sur le réseau client lorsque l'un des équilibreurs tombe, le travail avec les sessions client continuera sur les mêmes backends que ceux précédemment sélectionnés.

Pour un fonctionnement correct, l'adresse IP source de l'équilibreur à partir duquel la session est établie doit être résolue. Dans notre cas, il s'agit d'une adresse dynamique sur l'interface de bouclage.

Le bon fonctionnement des pairs n'est atteint que dans certaines conditions. Autrement dit, les délais d'expiration TCP doivent être suffisamment importants ou le commutateur doit être suffisamment rapide pour que la session TCP n'ait pas le temps de se rompre. Cependant, cela permet une commutation transparente.

Chez IaaS, nous avons un service basé sur la même technologie. Il s'agit d'un Load Balancer en tant que service pour OpenStack appelé Octavia. Il est basé sur la base de deux processus HAProxy, il comprenait à l'origine le support des pairs. Ils ont fait leurs preuves dans ce service.

L'image montre schématiquement le mouvement des pairs-tables entre trois instances HAProxy, une configuration est suggérée, comment cela peut être configuré:


HAProxy Peers (synchronisation de session)

Si vous implémentez le même schéma, son travail doit être soigneusement testé. Pas le fait que cela fonctionnera de la même manière dans 100% des cas. Mais au moins, vous ne perdrez pas les tables de bâton lorsque vous devez vous souvenir de l'IP source du client.

Limiter le nombre de demandes simultanées du même client


Tous les services qui sont dans le domaine public, y compris nos API, peuvent faire l'objet d'avalanches de demandes. Les raisons peuvent être complètement différentes, des erreurs des utilisateurs aux attaques ciblées. Nous sommes périodiquement DDoS aux adresses IP. Les clients font souvent des erreurs dans leurs scripts, ils font de nous des mini-DDoS.

D'une manière ou d'une autre, une protection supplémentaire doit être fournie. La solution évidente est de limiter le nombre de requêtes API et de ne pas perdre de temps CPU à traiter les requêtes malveillantes.

Pour mettre en œuvre de telles restrictions, nous utilisons des limites de taux, organisées sur la base de HAProxy, en utilisant les mêmes stick-tables. Les limites sont configurées assez simplement et vous permettent de limiter l'utilisateur par le nombre de requêtes à l'API. L'algorithme se souvient de l'adresse IP source à partir de laquelle les demandes sont effectuées et limite le nombre de demandes simultanées d'un utilisateur. Bien sûr, nous avons calculé le profil de charge API moyen pour chaque service et fixé la limite ≈ 10 fois cette valeur. Jusqu'à présent, nous continuons de suivre de près la situation, nous gardons le doigt sur le pouls.

À quoi cela ressemble-t-il dans la pratique? Nous avons des clients qui utilisent constamment nos API de mise à l'échelle automatique. Ils créent environ deux ou trois cents machines virtuelles plus près du matin et les suppriment plus près du soir. Pour OpenStack, créez une machine virtuelle, également avec des services PaaS, au moins 1 000 demandes d'API, car l'interaction entre les services s'effectue également via l'API.

Un tel lancement de tâche entraîne une charge assez importante. Nous avons estimé cette charge, collecté les pics quotidiens, les multiplié par dix, ce qui est devenu notre limite de taux. Nous gardons le doigt sur le pouls. Nous voyons souvent des robots, des scanners, qui essaient de nous regarder, avons-nous des scripts CGA qui peuvent être exécutés, nous les coupons activement.

Comment mettre à jour la base de code discrètement pour les utilisateurs


Nous implémentons également la tolérance aux pannes au niveau des processus de déploiement de code. Il y a des plantages lors des déploiements, mais leur impact sur la disponibilité des services peut être minimisé.

Nous mettons constamment à jour nos services et devons assurer le processus de mise à jour de la base de code sans effet pour les utilisateurs. Nous avons réussi à résoudre ce problème en utilisant les capacités de gestion HAProxy et la mise en œuvre de Graceful Shutdown dans nos services.

Pour résoudre ce problème, il était nécessaire de fournir un contrôle de l'équilibreur et l'arrêt «correct» des services:

  • Dans le cas de HAProxy, le contrôle se fait via le fichier stats, qui est essentiellement un socket et est défini dans la configuration HAProxy. Vous pouvez lui envoyer des commandes via stdio. Mais notre principal outil de contrôle de configuration est ansible, il a donc un module intégré pour gérer HAProxy. Que nous utilisons activement.
  • La plupart de nos services API et moteur prennent en charge des technologies d'arrêt gracieuses: à l'arrêt, ils attendent la fin de la tâche en cours, que ce soit une requête http ou une sorte de tâche utilitaire. La même chose se produit avec le travailleur. Il connaît toutes les tâches qu'il accomplit et se termine lorsqu'il a tout accompli avec succès.

Grâce à ces deux points, l'algorithme sûr de notre déploiement est le suivant.

  1. Le développeur construit un nouveau package de code (nous avons RPM), teste dans l'environnement de développement, teste à l'étape et le laisse dans le référentiel de l'étape.
  2. Le développeur place la tâche sur le déploiement avec la description la plus détaillée des «artefacts»: la version du nouveau package, une description de la nouvelle fonctionnalité et d'autres détails sur le déploiement, si nécessaire.
  3. L'administrateur système démarre la mise à niveau. Lance le playbook Ansible, qui à son tour effectue les opérations suivantes:
    • Il prend un package du référentiel d'étape, met à jour la version du package dans le référentiel de produit avec lui.
    • Fait une liste de backends du service mis à jour.
    • Désactive le premier service mis à jour dans HAProxy et attend la fin de ses processus. Grâce à un arrêt progressif, nous sommes convaincus que toutes les demandes actuelles des clients se termineront avec succès.
    • Une fois l'API, les travailleurs et HAProxy complètement arrêtés, le code est mis à jour.
    • Ansible lance ses services.
    • Pour chaque service, il tire certains «stylos» qui effectuent des tests unitaires pour un certain nombre de tests clés prédéfinis. Une vérification de base du nouveau code se produit.
    • Si aucune erreur n'a été trouvée à l'étape précédente, le backend est activé.
    • Accédez au backend suivant.
  4. Après la mise à jour de tous les backends, des tests fonctionnels sont lancés. S'ils ne suffisent pas, le développeur examine toutes les nouvelles fonctionnalités qu'il a faites.

Sur ce déploiement est terminé.


Cycle de mise à jour du service

Ce schéma ne fonctionnerait pas si nous n'avions pas une seule règle. Nous prenons en charge les anciennes et les nouvelles versions en combat. À l'avance, au stade du développement logiciel, il est prévu que même s'il y a des changements dans la base de données des services, ils ne casseront pas le code précédent. En conséquence, la base de code est progressivement mise à jour.

Conclusion


Partageant mes propres réflexions sur l'architecture WEB tolérante aux pannes, je veux encore une fois noter ses points clés:

  • tolérance aux pannes physiques;
  • tolérance aux pannes du réseau (équilibreurs, BGP);
  • .

uptime!

Source: https://habr.com/ru/post/fr474180/


All Articles