
Bonjour Habr! Je m'appelle Artem Karamyshev, chef de l'équipe d'administration système. . Nous avons lancé de nombreux nouveaux produits au cours de la dernière année. Nous voulions nous assurer que les services API étaient facilement évolutifs, tolérants aux pannes et prêts à faire face à une croissance rapide de la charge d'utilisateurs. Notre plate-forme est implémentée sur OpenStack, et je souhaite vous dire quels problèmes de tolérance aux pannes de composants nous avons dû résoudre pour 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 globale aux pannes d’une plateforme réside dans la résilience de ses composants. Nous allons donc progressivement parcourir tous les niveaux où nous avons identifié les risques et les avons clôturés.
Version vidéo de cette histoire, dont la source principale était un rapport de la conférence Uptime day 4, organisée par , tu peux voir .
Résilience de l'architecture physique
La partie publique du cloud MCS est désormais basée dans deux centres de données Tier III, entre eux se trouve sa propre fibre noire, réservée au niveau physique par différentes routes, avec un débit de 200 Gbit/s. Le niveau III fournit le niveau nécessaire de tolérance aux pannes pour l'infrastructure physique.
La fibre noire est réservée aux niveaux physique et logique. Le processus de réservation de canaux était 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, alors qu'elle travaillait dans un puits à proximité d'un des centres de données, une excavatrice a cassé un tuyau et à l'intérieur de ce tuyau se trouvaient à 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 avéré vulnérable à un moment donné, dans le puits. En conséquence, nous avons perdu une partie de l’infrastructure. Nous avons tiré des conclusions et pris un certain nombre de mesures, notamment l'installation d'optiques supplémentaires dans le puits adjacent.
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 du réseau, la meilleure métrique est sélectionnée, ce qui permet de fournir aux différents clients la meilleure qualité de connexion. Si la communication via un fournisseur tombe en panne, nous reconstruisons notre routage via les fournisseurs disponibles.
Si un fournisseur tombe en panne, nous passons automatiquement au suivant. En cas de panne de l'un des centres de données, nous disposons d'une copie miroir de nos services dans le deuxième centre de données, qui prend en charge la totalité de la charge.

Résilience des infrastructures physiques
Ce que nous utilisons pour la tolérance aux pannes au niveau des applications
Notre service est construit sur un certain nombre de composants open source.
ExaBGP est un service qui implémente un certain nombre de fonctions à l'aide du protocole de routage dynamique basé sur BGP. Nous l'utilisons activement pour annoncer nos adresses IP sur liste blanche via lesquelles les utilisateurs accèdent à l'API.
HAProxy est un équilibreur de charge élevé qui vous permet de configurer des règles d'équilibrage du trafic très flexibles à 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 se trouve derrière HAProxy.
Application API — une application web écrite en python, avec laquelle l'utilisateur gère son infrastructure et son service.
Demande de travailleur (ci-après simplement travailleur) - dans les services OpenStack, il s'agit d'un démon d'infrastructure qui vous permet de diffuser des commandes API vers l'infrastructure. Par exemple, la création du disque se produit dans le travailleur et la demande de création se produit dans l'API de l'application.
Architecture d'applications OpenStack standard
La plupart des services développés pour OpenStack tentent de suivre un paradigme unique. Un service se compose généralement de 2 parties : l'API et les travailleurs (exécuteurs backend). En règle générale, une API est une application WSGI en python, qui est lancée soit en tant que processus indépendant (démon), soit à l'aide d'un serveur Web Nginx ou Apache prêt à l'emploi. L'API traite la demande de l'utilisateur et transmet des instructions supplémentaires à l'application de travail pour exécution. Le transfert s'effectue à l'aide d'un courtier de messages, généralement RabbitMQ, les autres étant mal supportés. Lorsque les messages parviennent au courtier, ils sont traités par les travailleurs 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 seul service et, en théorie, peut être individuel pour chaque service. Ainsi, chez MCS, nous séparons 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, ce n'est pas tout le service qui tombe en panne, mais seulement une partie de celui-ci.
Le nombre d'applications de travail est illimité, de sorte que l'API peut facilement évoluer horizontalement derrière les équilibreurs afin d'augmenter les performances 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 à un autre que cette tâche lui est assignée (« s'il vous plaît, ne la 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. Nous utilisons mariadb comme base de données, située dans un cluster multimaître.
Ce service unique 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 des API, il suffit de mettre un équilibreur devant elles. La mise à l’échelle des travailleurs est obtenue en augmentant leur nombre.
Le point faible de l’ensemble du système est RabbitMQ et MariaDB. Leur architecture mérite un article séparé. Dans cet article, je souhaite me concentrer sur la tolérance aux pannes des API.

Architecture d'applications Openstack. Équilibrage et tolérance aux pannes de la plateforme cloud
Rendre l'équilibreur HAProxy tolérant aux pannes à l'aide d'ExaBGP
Pour rendre nos API évolutives, rapides et tolérantes aux pannes, nous leur avons mis un équilibreur de charge. Nous avons choisi HAProxy. Il possède à mon avis 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, support des tables de session.
Le premier problème à résoudre était la tolérance aux pannes de l’équilibreur lui-même. La simple installation d'un équilibreur crée également un point de défaillance : l'équilibreur tombe en panne et le service plante. Pour éviter que cela ne se produise, nous avons utilisé HAProxy en conjonction avec ExaBGP.
ExaBGP permet d'implémenter 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
- Nous installons les logiciels nécessaires, ExaBGP et HAProxy, sur trois serveurs.
- Nous créons une interface de bouclage sur chaque serveur.
- Sur les trois serveurs, nous attribuons la même adresse IP blanche à cette interface.
- 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 sur les trois serveurs. D'un point de vue réseau, la même adresse est accessible à partir de trois prochains sauts différents. Le routeur voit trois routes identiques, sélectionne la priorité la plus élevée en fonction de sa propre métrique (il s'agit généralement de la même option) et le trafic ne va qu'à l'un des serveurs.
En cas de problèmes de fonctionnement de HAProxy ou de panne de serveur, ExaBGP cesse d'annoncer l'itinéraire et le trafic passe en douceur vers un autre serveur.
Ainsi, nous avons atteint la tolérance aux pannes de l’équilibreur.

Tolérance aux pannes des équilibreurs HAProxy
Le schéma s'est avéré imparfait : nous avons appris à réserver HAProxy, mais n'avons pas appris à répartir la charge au sein des services. Nous avons donc un peu élargi ce schéma : nous sommes passés à l'équilibrage entre plusieurs adresses IP blanches.
Équilibrage basé sur DNS plus BGP
La question de l'équilibrage de charge pour notre HAProxy reste en suspens. Cependant, cela peut être résolu tout simplement, comme nous l’avons fait ici.
Pour équilibrer trois serveurs vous aurez besoin de 3 adresses IP blanches et du bon vieux DNS. Chacune de ces adresses est déterminée sur l'interface de bouclage de chaque HAProxy et publiée sur Internet.
Dans OpenStack, pour gérer les ressources, un répertoire de services est utilisé, qui spécifie l'API de point de terminaison d'un service particulier. Dans ce répertoire, nous enregistrons un nom de domaine - public.infra.mail.ru, qui est résolu via DNS par trois adresses IP différentes. En conséquence, nous obtenons une répartition de la charge entre trois adresses via DNS.
Mais comme lors de l'annonce des adresses IP blanches, nous ne contrôlons pas les priorités de sélection des serveurs, cela n'est pas encore équilibré. En règle générale, un seul serveur sera sélectionné en fonction de l'ancienneté de l'adresse IP, et les deux autres seront inactifs car aucune métrique n'est spécifiée dans BGP.
Nous avons commencé à envoyer des routes via ExaBGP avec différentes métriques. Chaque équilibreur annonce les trois adresses IP blanches, mais l'une d'entre elles, la principale de cet équilibreur, est annoncée avec la métrique minimale. Ainsi, pendant que les trois équilibreurs fonctionnent, les appels vers la première adresse IP vont vers le premier équilibreur, les appels vers le deuxième vers le deuxième et les appels vers la troisième vers le troisième.
Que se passe-t-il lorsque l'un des équilibreurs tombe ? Si un équilibreur tombe en panne, son adresse principale est toujours annoncée par les deux autres et le trafic est redistribué entre eux. Ainsi, nous donnons à l'utilisateur plusieurs adresses IP à la fois via DNS. En équilibrant par DNS et différentes métriques, nous obtenons une répartition uniforme de la charge sur les trois équilibreurs. Et en même temps, nous ne perdons pas la tolérance aux pannes.

Équilibrage de HAProxy basé sur DNS + BGP
Interaction entre ExaBGP et HAProxy
Nous avons donc implémenté une tolérance aux pannes en cas de départ du serveur, basée sur l'arrêt de l'annonce des routes. Mais HAProxy peut s'arrêter pour d'autres raisons qu'une panne de serveur : erreurs d'administration, pannes au sein du service. Nous voulons également retirer l'équilibreur cassé sous la charge dans ces cas-là, et nous avons besoin d'un mécanisme différent.
Par conséquent, en élargissant le schéma précédent, nous avons implémenté le rythme cardiaque entre ExaBGP et HAProxy. Il s'agit d'une implémentation logicielle de l'interaction entre ExaBGP et HAProxy, lorsqu'ExaBGP utilise des scripts personnalisés pour vérifier l'état des applications.
Pour ce faire, vous devez configurer un vérificateur de santé dans la configuration ExaBGP, qui peut vérifier l'état de HAProxy. Dans notre cas, nous avons configuré le backend de santé dans HAProxy, et du côté ExaBGP, nous vérifions avec une simple requête GET. Si l’annonce cesse de se produire, alors HAProxy ne fonctionne probablement pas et il n’est pas nécessaire d’en faire la publicité.

Bilan de santé HAProxy
HAProxy Peers : synchronisation de session
La prochaine chose à faire était de synchroniser les sessions. Lorsque vous travaillez via 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 capables de le faire grâce à 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 : les plus simples comme , et étendu, lorsque la session du client est mémorisée, et à chaque fois il se retrouve sur le même serveur qu'auparavant. Nous voulions mettre en œuvre la deuxième option.
HAProxy utilise des tables stick pour enregistrer les sessions client de ce mécanisme. Ils enregistrent l'adresse IP d'origine 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 stocker une paire source-IP + destination-IP, ce qui est particulièrement utile pour les applications qui ne peuvent pas transférer le contexte de session utilisateur lors du passage à un autre équilibreur, par exemple en mode d'équilibrage RoundRobin.
Si une table bâton apprend à se déplacer entre différents processus HAProxy (entre lesquels l'équilibrage se produit), nos équilibreurs seront capables de travailler avec un seul pool de tables bâton. Cela permettra de changer de manière transparente le réseau du client en cas de panne de l'un des équilibreurs ; le travail avec les sessions client se poursuivra sur les mêmes backends que ceux sélectionnés précédemment.
Pour un bon fonctionnement, le problème de l'adresse IP source de l'équilibreur à partir duquel la session a été établie doit être résolu. Dans notre cas, il s'agit d'une adresse dynamique sur l'interface de bouclage.
Un travail correct des pairs n'est obtenu que sous certaines conditions. Autrement dit, les délais d'attente TCP doivent être suffisamment longs ou la commutation doit être suffisamment rapide pour que la session TCP n'ait pas le temps de se terminer. Cependant, cela permet une commutation transparente.
Dans IaaS, nous avons un service construit en utilisant la même technologie. Ce , qui s'appelle Octavie. Il est basé sur deux processus HAProxy et inclut initialement la prise en charge des pairs. Ils ont fait leurs preuves dans ce service.
L'image montre schématiquement le mouvement des tables homologues entre trois instances HAProxy, une configuration est proposée sur la façon dont cela peut être configuré :

Homologues HAProxy (synchronisation de session)
Si vous mettez en œuvre le même schéma, son fonctionnement doit être soigneusement testé. Ce n’est pas un fait que cela fonctionnera de la même manière 100 % du temps. Mais au moins, vous ne perdrez pas les tables stick lorsque vous devrez vous souvenir de l'adresse IP source du client.
Limiter le nombre de requêtes simultanées d'un même client
Tous les services accessibles au public, y compris nos API, peuvent faire l'objet d'avalanches de demandes. Les raisons peuvent être complètement différentes, depuis les erreurs des utilisateurs jusqu'aux attaques ciblées. Nous sommes périodiquement victimes de DDoS par adresses IP. Les clients font souvent des erreurs dans leurs scripts et nous envoient 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 des 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 tables de bâtons. La configuration des limites est assez simple et permet de limiter l'utilisateur par le nombre de requêtes à l'API. L'algorithme mémorise l'adresse IP source à partir de laquelle les demandes sont effectuées et limite le nombre de demandes simultanées d'un utilisateur. Bien entendu, nous avons calculé le profil de charge API moyen pour chaque service et fixé une limite de ≈ 10 fois cette valeur. Nous continuons de suivre de près la situation et de rester à l’écoute.
A quoi cela ressemble-t-il en pratique ? Nous avons des clients qui utilisent nos API d’autoscaling en permanence. Ils créent environ deux à trois cents machines virtuelles le matin et les suppriment le soir. Pour OpenStack, la création d'une machine virtuelle, également avec des services PaaS, nécessite au moins 1000 XNUMX requêtes API, puisque l'interaction entre les services se fait également via l'API.
Un tel transfert de tâches entraîne une charge assez importante. Nous avons évalué cette charge, collecté les pics quotidiens, les avons décuplés, et c'est devenu notre limite de débit. Nous restons à l’écoute. Nous voyons souvent des robots et des scanners qui essaient de nous observer pour voir si nous avons des scripts CGA pouvant être exécutés, nous les supprimons activement.
Comment mettre à jour votre base de code sans que les utilisateurs ne s'en aperçoivent
Nous mettons également en œuvre la tolérance aux pannes au niveau des processus de déploiement de code. Des problèmes peuvent survenir lors des déploiements, mais leur impact sur la disponibilité du service peut être minimisé.
Nous mettons constamment à jour nos services et devons nous assurer que la base de code est mise à jour sans affecter les utilisateurs. Nous avons réussi à résoudre ce problème en utilisant les capacités de gestion de HAProxy et la mise en œuvre de Graceful Shutdown dans nos services.
Pour résoudre ce problème, il fallait assurer le contrôle de l'équilibreur et l'arrêt « correct » des services :
- Dans le cas de HAProxy, le contrôle est effectué via un fichier de statistiques, qui est essentiellement un socket et est défini dans la configuration de HAProxy. Vous pouvez lui envoyer des commandes via stdio. Mais notre principal outil de contrôle de configuration est ansible, il dispose donc d'un module intégré pour gérer HAProxy. Que nous utilisons activement.
- La plupart de nos services API et Engine prennent en charge les technologies d'arrêt progressif : lors de l'arrêt, ils attendent que la tâche en cours soit terminée, qu'il s'agisse d'une requête http ou d'une tâche de service. 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écurisé de notre déploiement ressemble à ceci.
- Le développeur assemble un nouveau package de code (pour nous, il s'agit de RPM), le teste dans l'environnement de développement, le teste dans la scène et le laisse dans le référentiel de scène.
- Le développeur définit la tâche de 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.
- L'administrateur système commence la mise à jour. Lance le playbook Ansible, qui effectue à son tour les opérations suivantes :
- Prend un package du référentiel de scène et l'utilise pour mettre à jour la version du package dans le référentiel de produit.
- Compile une liste de backends du service mis à jour.
- Arrête le premier service à mettre à jour dans HAProxy et attend la fin de son exécution. Grâce à un arrêt progressif, nous sommes convaincus que toutes les demandes actuelles des clients seront traitées avec succès.
- Une fois l'API et les travailleurs complètement arrêtés et HAProxy désactivé, le code est mis à jour.
- Ansible exécute des services.
- Pour chaque service, certaines « poignées » sont tirées, qui effectuent des tests unitaires sur un certain nombre de tests clés prédéfinis. Une vérification de base du nouveau code a lieu.
- Si aucune erreur n'a été trouvée à l'étape précédente, le backend est activé.
- Passons au backend suivant.
- Une fois tous les backends mis à jour, les tests fonctionnels sont lancés. S'ils manquent, le développeur examine toute nouvelle fonctionnalité qu'il a créée.
Ceci termine le déploiement.

Cycle de mise à jour du service
Ce système ne fonctionnerait pas s’il n’y avait pas une seule règle. Nous prenons en charge les anciennes et les nouvelles versions en combat. Au stade du développement du logiciel, il est prévu à l'avance que même s'il y a des modifications dans la base de données du service, elles ne briseront pas le code précédent. En conséquence, la base de code est progressivement mise à jour.
Conclusion
Partageant mes propres réflexions sur une architecture WEB tolérante aux pannes, je voudrais une fois de plus souligner ses points clés :
- tolérance aux pannes physiques ;
- tolérance aux pannes du réseau (équilibreurs, BGP) ;
- tolérance aux pannes des logiciels utilisés et développés.
Disponibilité stable à tous !
Source: habr.com
