banner

Nouvelles

Mar 07, 2023

Construire et déployer MySQL Raft chez Meta

Chez Meta, nous gérons l'un des plus grands déploiements de MySQL au monde. Le déploiement alimente le graphique social ainsi que de nombreux autres services, tels que la messagerie, les annonces et le flux. Au cours des dernières années, nous avons implémenté MySQL Raft, un moteur de consensus Raft qui a été intégré à MySQL pour construire une machine à états répliquée. Nous avons migré une grande partie de notre déploiement vers MySQL Raft et prévoyons de remplacer entièrement les bases de données semi-synchrones MySQL actuelles par celui-ci. Le projet a apporté des avantages significatifs au déploiement de MySQL chez Meta, notamment une fiabilité accrue, une sécurité démontrable, des améliorations significatives du temps de basculement et une simplicité opérationnelle, le tout avec des performances d'écriture égales ou comparables.

Pour permettre une haute disponibilité, une tolérance aux pannes et une mise à l'échelle des lectures, le magasin de données MySQL de Meta est un déploiement massivement fragmenté et géorépliqué avec des millions de fragments, contenant des pétaoctets de données. Le déploiement comprend des milliers de machines fonctionnant dans plusieurs régions et des centres de données sur plusieurs continents.

Auparavant, notre solution de réplication utilisait le protocole de réplication MySQL semi-synchrone (semisync). Il s'agissait d'un protocole de chemin de données uniquement. Le primaire MySQL utiliserait la réplication semi-synchrone vers deux réplicas de journalisation uniquement (logtailers) dans la région primaire mais en dehors du domaine de défaillance du primaire. Ces deux logtailers agiraient comme des ACKer semi-synchrones (un ACK est un accusé de réception au primaire que la transaction a été écrite localement). Cela permettrait au chemin de données d'avoir des validations à très faible latence (inférieure à la milliseconde) et fournirait une haute disponibilité/durabilité pour les écritures. La réplication asynchrone régulière du primaire vers le réplica de MySQL a été utilisée pour une distribution plus large dans d'autres régions.

Les opérations du plan de contrôle (par exemple, les promotions, le basculement et le changement d'adhésion) seraient sous la responsabilité d'un ensemble de démons Python (ci-après appelé automatisation). L'automatisation effectuerait l'orchestration nécessaire pour promouvoir un nouveau serveur MySQL dans un emplacement de basculement en tant que serveur principal. L'automatisation pointerait également le primaire précédent et les réplicas restants à répliquer à partir du nouveau primaire. Les opérations de changement d'adhésion seraient orchestrées par un autre élément d'automatisation appelé scanner de pool MySQL (MPS). Pour ajouter un nouveau membre, MPS fait pointer le nouveau réplica vers le principal et l'ajoute au magasin de découverte de service. Un basculement serait une opération plus complexe dans laquelle les threads de queue des logtailers (ACKers semi-synchrones) seraient arrêtés pour clôturer le précédent principal mort.

Dans le passé, pour garantir la sécurité et éviter la perte de données lors des opérations complexes de promotion et de basculement, plusieurs démons et scripts d'automatisation utilisaient le verrouillage, les étapes d'orchestration, un mécanisme de clôture et SMC, un système de découverte de services. C'était une configuration distribuée, et il était difficile d'accomplir cela de manière atomique. L'automatisation est devenue plus complexe et plus difficile à maintenir au fil du temps, car de plus en plus de cas critiques devaient être corrigés.

Nous avons décidé d'adopter une approche complètement différente. Nous avons amélioré MySQL et en avons fait un véritable système distribué. Réalisant que les opérations du plan de contrôle telles que les promotions et les modifications d'adhésion étaient à l'origine de la plupart des problèmes, nous voulions que les opérations du plan de contrôle et du plan de données fassent partie du même journal répliqué. Pour cela, nous avons utilisé le protocole de consensus bien compris Raft. Cela signifiait également que la source de vérité de l'adhésion et du leadership se déplaçait à l'intérieur du serveur (mysqld). Il s'agissait de la plus grande contribution de l'introduction de Raft, car elle permettait une exactitude prouvable (propriété de sécurité) à travers les promotions et les changements d'adhésion dans le serveur MySQL.

Notre implémentation de Raft pour MySQL est basée sur Apache Kudu. Nous l'avons considérablement amélioré pour les besoins de MySQL et de notre déploiement. Nous avons publié ce fork en tant que projet open source, kuduraft.

Certaines des fonctionnalités clés que nous avons ajoutées à kuduraft sont :

Nous avons également dû apporter des modifications relativement importantes à la réplication MySQL pour s'interfacer avec Raft. Pour cela, nous avons créé un nouveau plugin MySQL à source fermée appelé MyRaft. MySQL s'interfacerait avec MyRaft via les API du plug-in (des API similaires avaient également été utilisées pour la semi-synchronisation), tandis que nous avons créé une API distincte pour MyRaft pour s'interfacer avec le serveur MySQL (rappels).

Un anneau Raft serait composé de plusieurs instances MySQL (quatre dans le diagramme) dans différentes régions. Le temps de communication aller-retour (RTT) entre ces régions serait compris entre 10 et 100 millisecondes. Quelques-uns de ces MySQL (généralement trois) ont été autorisés à devenir des primaires, tandis que les autres n'étaient autorisés qu'à être des répliques en lecture pure (non compatibles avec les primaires). Le déploiement de MySQL chez Meta a également une exigence de longue date pour des commits à latence extrêmement faible. Les services qui utilisent MySQL comme magasin (par exemple, le graphe social) ont besoin ou ont été conçus pour de telles écritures extrêmement rapides.

Pour répondre à cette exigence, la configuration de FlexiRaft n'utiliserait que des validations dans la région (mode dynamique de région unique). Pour permettre cela, chaque région principale capable aurait deux logtailers supplémentaires (témoins ou entités de journalisation uniquement). Le quorum de données pour les écritures serait de 2/3 (2 ACK sur 1 MySQL + 2 logtailers). Raft gérerait et exécuterait toujours un journal répliqué sur toutes les entités (1 MySQL à capacité primaire + 2 logtailers) * 3 régions + (MySQL non à capacité primaire) * 3 régions = 12 entités.

Rôles du radeau : le leader, comme son nom l'indique, est le leader dans un terme du journal répliqué. Un leader dans Raft serait également le principal dans MySQL et celui qui accepte les écritures client. Le suiveur est un membre votant du ring et reçoit passivement des messages (AppendEntries) du leader. Un suiveur serait une réplique du point de vue de MySQL et appliquerait les transactions à son moteur. Cela n'autoriserait pas les écritures directes à partir des connexions utilisateur (read_only=1 est défini). Un apprenant serait un membre sans droit de vote de l'anneau, par exemple, les trois MySQL dans les régions non primaires (ci-dessus). Ce serait une réplique du point de vue de MySQL.

Pour la réplication, MySQL a toujours utilisé le format de journal binaire. Ce format est au cœur de la réplication de MySQL, et nous avons décidé de le conserver. Du point de vue de Raft, le journal binaire est devenu le journal répliqué. Cela a été fait via l'amélioration de l'abstraction de journal de kuduraft. Les transactions MySQL seraient codées comme une série d'événements (par exemple, l'événement Update Rows) avec un début et une fin pour chaque transaction. Le journal binaire aurait également des en-têtes appropriés et se terminerait généralement par un événement de fin (événement Rotation).

Nous avons dû modifier la façon dont MySQL gère ses journaux en interne. Sur un primaire, Raft écrirait dans un binlog. Ce n'est pas différent de ce qui se passe dans MySQL standard. Dans une réplique, Raft écrirait également dans un binlog au lieu d'un journal de relais séparé dans MySQL standard. Cela a créé de la simplicité pour Raft car il n'y avait qu'un seul espace de noms de fichiers journaux dont Raft serait préoccupé. Si un suiveur était promu leader, il pourrait revenir en toute transparence dans son historique de journaux pour envoyer des transactions aux membres en retard. Les threads d'application de la réplique récupéraient les transactions du binlog, puis les appliquaient au moteur. Au cours de ce processus, un nouveau fichier journal, le journal d'application, serait créé. Ce journal d'application jouerait un rôle important dans la reprise sur incident des répliques, mais il s'agit sinon d'un fichier journal non répliqué.

Donc, en résumé :

Dans MySQL standard :

Dans MySQL Raft :

La transaction serait d'abord préparée dans le moteur. Cela se produirait dans le fil de la connexion de l'utilisateur. L'acte de préparation de la transaction impliquerait des interactions avec le moteur de stockage (par exemple, InnoDB ou MyRocks) et générerait une charge utile binlog en mémoire pour la transaction. Au moment de la validation, l'écriture passerait par le flux de groupe commit/ordered_commit. Les GTID seraient attribués, puis Raft attribuerait un OpId (term: index) à la transaction. À ce stade, Raft compresserait la transaction, la stockerait dans son LogCache et écrirait la transaction dans un fichier binlog. Il commencerait à expédier de manière asynchrone la transaction à d'autres abonnés pour obtenir des ACK et parvenir à un consensus.

Le thread utilisateur, qui est en "commit" de la transaction, serait bloqué, attendant le consensus de Raft. Lorsque Raft obtiendrait deux votes sur trois dans la région, un engagement consensuel serait atteint. Raft enverrait également la transaction à tous les membres hors région mais ignorerait leurs votes à cause d'un algorithme appelé FlexiRaft (décrit ci-dessous). Lors de la validation par consensus, le thread utilisateur serait débloqué et la transaction se poursuivrait et s'engagerait dans le moteur. Après la validation du moteur, la requête d'écriture se terminerait et reviendrait au client. Peu de temps après, Raft enverrait également de manière asynchrone un marqueur de validation (OpId de la validation actuelle) aux abonnés en aval afin qu'ils puissent également appliquer les transactions à leur base de données.

Des modifications ont dû être apportées à la récupération après crash pour qu'elle fonctionne de manière transparente avec Raft. Les plantages peuvent survenir à tout moment de la durée de vie d'une transaction et, par conséquent, le protocole doit assurer la cohérence des membres. Voici quelques idées clés sur la façon dont nous l'avons fait fonctionner.

Le basculement et les opérations de maintenance régulières peuvent déclencher des changements de direction dans Raft. Après l'élection d'un leader, le plugin MyRaft essaierait de faire passer le MySQL qui l'accompagne en mode primaire. Pour cela, le plugin orchestrerait un ensemble d'étapes. Ces rappels de Raft → MySQL interrompraient les transactions en cours, annuleraient les GTID en cours d'utilisation, feraient passer le journal côté moteur de apply-log à binlog et définiraient éventuellement les paramètres read_only appropriés. Ce mécanisme est complexe et actuellement non open source.

Étant donné que le document Raft et Apache Kudu ne prenaient en charge qu'un seul quorum global, cela ne fonctionnerait pas bien à Meta, où les anneaux étaient grands mais le quorum de chemin de données devait être petit.

Pour contourner ce problème, nous avons innové sur FlexiRaft, en empruntant des idées à Flexible Paxos.

À un niveau élevé, FlexiRaft permet à Raft d'avoir un quorum de validation de données différent (petit) mais de prendre un coup correspondant sur le quorum d'élection du leader (grand). En suivant les garanties prouvables d'intersection de quorum, FlexiRaft garantit que les règles de log les plus longues de Raft et l'intersection de quorum appropriée garantiront une sécurité prouvable.

FlexiRaft prend en charge le mode dynamique à région unique. Dans ce mode, les membres sont regroupés par leur géo-région. Le quorum actuel de Raft dépend de qui est le leader actuel (d'où le nom de "dynamique de région unique"). Le quorum de données est la majorité des électeurs dans la région du chef. Lors des promotions, si les mandats sont continus, le Candidat recoupera la région du dernier leader connu. FlexiRaft garantirait également que le quorum de la région du candidat soit également atteint, sinon le message No-Op suivant pourrait rester bloqué. Si, dans de rares cas, les termes ne sont pas continus, Flexi Raft essaierait de comprendre un ensemble croissant de régions qui doivent être intersectées pour des raisons de sécurité ou, dans le pire des cas, reviendrait au cas d'intersection de la région N de Paxos flexible. . Grâce aux pré-élections et aux simulations d'élections, les incidences d'intervalles de mandats sont rares.

Afin de sérialiser les événements de promotion et de changement d'adhésion dans le binlog, nous avons détourné l'événement Rotate Event and Metadata du format de journal binaire MySQL. Ces événements porteraient l'équivalent des messages No-Op et des opérations d'ajout/de suppression de membre de Raft. Apache Kudu n'a pas pris en charge le consensus conjoint, c'est pourquoi nous n'autorisons que les modifications d'adhésion une par une (vous pouvez modifier l'adhésion d'une seule entité en un tour pour suivre les règles d'intersection de quorum implicite).

Avec la mise en œuvre de MySQL Raft, nous avons atteint une séparation très nette des préoccupations pour le déploiement de MySQL. Le serveur MySQL serait responsable de la sécurité via la machine à états répliquée de Raft. La garantie de non-perte de données serait manifestement inscrite dans le serveur lui-même. L'automatisation (scripts Python, démons) lancerait les opérations du plan de contrôle et surveillerait la santé de la flotte. Il remplacerait également les membres ou ferait des promotions via Raft pendant la maintenance ou lorsqu'une panne d'hôte était détectée. De temps en temps, l'automatisation pourrait également modifier le placement régional de la topologie MySQL. Changer l'automatisation pour s'adapter à Raft a été une entreprise colossale, s'étalant sur plusieurs années de développement et d'efforts de déploiement.

Lors d'événements de maintenance prolongés, l'automatisation définirait des informations d'interdiction de leadership sur Raft. Raft interdirait à ces entités interdites de devenir chef ou les évacuerait rapidement en cas d'élection par inadvertance. L'automatisation favoriserait également l'éloignement de ces régions vers d'autres régions.

Le déploiement de Raft dans la flotte a été un énorme apprentissage pour l'équipe. Nous avons initialement développé Raft sur MySQL 5.6 et avons dû migrer vers MySQL 8.0.

L'un des principaux apprentissages a été que si l'exactitude était plus facile à raisonner avec Raft, le protocole Raft en lui-même n'aide pas beaucoup dans le souci de disponibilité. Étant donné que notre quorum de données MySQL était très petit (deux membres sur trois dans la région), deux mauvaises entités dans la région pourraient à peu près briser le quorum et réduire la disponibilité. La flotte MySQL subit chaque jour une bonne quantité de roulement (en raison de la maintenance, des pannes d'hôte, des opérations de rééquilibrage), donc initier et faire des changements d'adhésion rapidement et correctement était une exigence clé pour une disponibilité constante. Une grande partie de l'effort de déploiement s'est concentrée sur le remplacement rapide de logtailer et de MySQL afin que les quorums Raft soient sains.

Nous avons dû améliorer kuduraft pour le rendre plus robuste pour la disponibilité. Ces améliorations ne faisaient pas partie du protocole de base mais peuvent être considérées comme des ajouts techniques à celui-ci. Kuduraft prend en charge les pré-élections, mais les pré-élections ne sont effectuées que lors d'un basculement. Lors d'un transfert gracieux de leadership, le candidat désigné passe directement à une véritable élection, annulant le mandat. Cela conduit à des leaders bloqués (kuduraft ne fait pas de descente automatique). Pour résoudre ce problème, nous avons ajouté une fonctionnalité d'élections fictives, qui était similaire aux pré-élections, mais qui ne se produisait que lors d'un transfert gracieux de leadership. Comme il s'agissait d'une opération asynchrone, cela n'a pas augmenté les temps d'arrêt des promotions. Une simulation d'élection éliminerait les cas où une véritable élection réussirait partiellement et resterait bloquée.

Gestion des échecs byzantins : la liste des membres de Raft est considérée comme bénie par Raft lui-même. Mais lors de l'approvisionnement de nouveaux membres, ou à cause de courses dans l'automatisation, il pourrait y avoir des cas bizarres où deux anneaux de radeau différents se croisent. Ces nœuds d'adhésion zombies ont dû être éliminés et ne devraient pas pouvoir communiquer entre eux. Nous avons implémenté une fonctionnalité pour bloquer les RPC de ces membres zombies vers le ring. C'était, à certains égards, une manipulation d'un acteur byzantin. Nous avons amélioré la mise en œuvre de Raft après avoir remarqué ces rares incidents survenus lors de notre déploiement.

Lors du lancement de MySQL Raft, l'un des objectifs était de réduire la complexité opérationnelle des appels, afin que les ingénieurs puissent identifier les causes profondes et atténuer les problèmes. Nous avons construit plusieurs tableaux de bord, outils CLI et tables de plongée pour surveiller Raft. Nous avons ajouté une copieuse journalisation à MySQL, en particulier dans le domaine des promotions et des changements d'adhésion. Nous avons créé des CLI pour les rapports de quorum et de vote sur un anneau, ce qui nous aide à identifier rapidement quand et pourquoi un anneau est indisponible (quorum brisé). L'investissement dans l'infrastructure d'outillage et d'automatisation allait de pair et aurait pu représenter un investissement plus important que les changements de serveur. Cet investissement a été très rentable et a réduit les difficultés opérationnelles et d'intégration.

Bien que ce ne soit pas souhaitable, les quorums sont brisés de temps en temps, ce qui entraîne une perte de disponibilité. Le cas typique est lorsque l'automatisation ne détecte pas les instances/logtailers malsains dans l'anneau et ne les remplace pas rapidement. Cela peut se produire en raison d'une mauvaise détection, d'une surcharge de la file d'attente de travail ou d'un manque de capacité d'hôte disponible. Les échecs corrélés, lorsque plusieurs entités du quorum tombent en panne en même temps, sont moins courants. Cela ne se produit pas souvent, car les déploiements tentent d'isoler les domaines de défaillance dans les entités critiques du quorum grâce à des décisions de placement appropriées. Pour faire court : à grande échelle, des événements inattendus se produisent, malgré les mesures de protection existantes. Des outils doivent être disponibles pour atténuer de telles situations en production. Nous avons construit Quorum Fixer en prévision de cela.

Quorum Fixer est un outil de correction manuel créé en Python qui bloque les écritures sur l'anneau. Il effectue des vérifications hors bande pour déterminer l'entité de journal la plus longue. Cela modifie de force les attentes de quorum pour l'élection d'un leader à l'intérieur de Raft, de sorte que l'entité choisie devienne un leader. Après une promotion réussie, nous réinitialisons l'attente de quorum et l'anneau redevient généralement sain.

C'était une décision consciente de ne pas exécuter cet outil automatiquement, car nous voulons identifier la cause première et identifier tous les cas de perte de quorum et corriger les bogues en cours de route (ne pas les faire corriger silencieusement par l'automatisation).

La transition de semi-synchrone à MySQL Raft sur un déploiement massif est difficile. Pour cela, nous avons créé un outil (en Python) appelé enable-raft. Enable-raft orchestre la transition de semi-synchrone à Raft en chargeant le plugin et en définissant les configurations appropriées (mysql sys-vars) sur chacune des entités. Ce processus implique un petit temps d'arrêt pour l'anneau. L'outil a été rendu robuste dans le temps et peut déployer Raft à grande échelle très rapidement. Nous l'avons utilisé pour déployer en toute sécurité Raft.

Inutile de dire que faire un changement dans le pipeline de réplication de base de MySQL est un projet très difficile. Étant donné que la sécurité des données est en jeu, les tests étaient la clé de la confiance. Nous avons considérablement tiré parti des tests fantômes et de l'injection de pannes au cours du projet. Nous injecterions des milliers de basculements et d'élections sur des anneaux de test avant chaque déploiement de gestionnaire de packages RPM. Nous déclencherions des remplacements et des modifications d'adhésion sur les actifs de test pour déclencher les chemins de code critiques.

Des tests de longue durée avec des vérifications de l'exactitude des données étaient également essentiels. Nous avons une automatisation qui s'exécute la nuit sur les fragments, garantissant la cohérence des primaires et des répliques. Nous sommes alertés de toute incompatibilité de ce type et nous la déboguons.

La performance de la latence du chemin d'écriture pour Raft était équivalente à la semi-synchronisation. La machinerie semi-synchrone est légèrement plus simple et devrait donc être plus légère, mais nous avons optimisé Raft pour obtenir les mêmes latences que la semi-synchronisation. Nous avons optimisé kuduraft pour ne plus ajouter de CPU à la flotte malgré l'ajout de nombreuses autres responsabilités qui étaient auparavant en dehors du binaire du serveur.

Raft a apporté des améliorations d'ordre de grandeur aux promotions et aux délais de basculement. Les promotions gracieuses, qui constituent l'essentiel des changements de leadership dans la flotte, se sont considérablement améliorées et nous pouvons généralement terminer une promotion en 300 millisecondes. Dans les configurations semi-synchrones, étant donné que le magasin de découverte de services serait la source de vérité, les clients remarquant la fin de la promotion seraient beaucoup plus longs, ce qui entraînerait des temps d'arrêt plus élevés pour l'utilisateur final sur une partition.

Raft effectue généralement un basculement en 2 secondes. En effet, nous battons notre cœur pour la santé de Raft toutes les 500 millisecondes et commençons une élection lorsque trois battements de cœur successifs échouent. Dans le monde semi-synchronisé, cette étape était lourde d'orchestration et prendrait 20 à 40 secondes. Raft a ainsi multiplié par 10 les temps d'arrêt pour les cas de basculement.

Raft a aidé à résoudre les problèmes de gestion opérationnelle de MySQL chez Meta en offrant une sécurité et une simplicité prouvées. Nos objectifs d'avoir une gestion autonome de la cohérence de MySQL et d'avoir des outils pour les rares cas de perte de disponibilité sont pour la plupart atteints. Raft ouvre maintenant d'importantes opportunités à l'avenir, car nous pouvons nous concentrer sur l'amélioration de l'offre aux services qui utilisent MySQL. L'une des demandes de nos propriétaires de services est d'avoir une cohérence configurable. La cohérence configurable permettra aux propriétaires, au moment de l'intégration, de sélectionner si le service a besoin de quorums X-region ou de quorums qui demandent des copies dans certaines zones géographiques spécifiques (par exemple, l'Europe et les États-Unis). FlexiRaft prend en charge de manière transparente ces quorums configurables, et nous prévoyons de commencer à déployer cette prise en charge à l'avenir. De tels quorums conduiront en conséquence à des latences de validation plus élevées, mais les cas d'utilisation doivent pouvoir trouver un compromis entre cohérence et latence (par exemple, le théorème PACELC).

Grâce à la fonctionnalité de proxy (capacité d'envoyer des messages en utilisant une topologie de distribution multi-sauts), Raft peut également économiser de la bande passante réseau outre-Atlantique. Nous prévoyons d'utiliser Raft pour répliquer des États-Unis vers l'Europe une seule fois, puis d'utiliser la fonction de proxy de Raft pour distribuer en Europe. Cela augmentera la latence, mais elle sera nominale étant donné que la majeure partie de la latence se trouve dans le transfert transatlantique et que le saut supplémentaire est beaucoup plus court.

Certaines des idées les plus spéculatives dans les déploiements de bases de données de Meta et l'espace de consensus distribué concernent l'exploration de protocoles sans leader, comme Epaxos. Nos déploiements et services actuels ont fonctionné avec les hypothèses qui accompagnent les protocoles leaders puissants, mais nous commençons à voir un petit nombre d'exigences où les services bénéficieraient d'une latence d'écriture plus uniforme dans le WAN. Une autre idée que nous envisageons est de séparer le journal de la machine d'état (la base de données) en une configuration de journal désagrégée. Cela permettra à l'équipe de gérer les préoccupations du journal et de la réplication séparément des préoccupations du stockage de la base de données et du moteur d'exécution SQL.

La construction et le déploiement de MySQL Raft à l'échelle Meta nécessitaient un travail d'équipe important et un soutien à la gestion. Nous tenons à remercier les personnes suivantes pour leur rôle dans la réussite de ce projet. Shrikanth Shankar, Tobias Asplund, Jim Carrig, Affan Dar et David Nagle pour avoir soutenu les membres de l'équipe pendant ce voyage. Nous tenons également à remercier les responsables de programme compétents de ce projet, Dan O et Karthik Chidambaram, qui nous ont permis de rester sur la bonne voie.

L'effort d'ingénierie a impliqué des contributions clés de plusieurs membres actuels et passés de l'équipe, notamment Vinaykumar Bhat, Xi Wang, Bartholomew Pelc, Chi Li, Yash Botadra, Alan Liang, Michael Percy, Yoshinori Matsunobu, Ritwik Yadav, Luqun Lou, Pushap Goyal et Anatoly Karp Igor. Pozgaj.

PARTAGER