NewSQL = NoSQL+ACID

NewSQL = NoSQL+ACID
Jusqu'Ă  rĂ©cemment, Odnoklassniki avait environ 50 To de donnĂ©es traitĂ©es en temps rĂ©el stockĂ©es dans SQL Server. Pour un tel volume, assurer un accĂšs rapide, fiable et rĂ©silient aux pannes via un datacenter en utilisant une base de donnĂ©es SQL est pratiquement impossible. Dans de tels cas, l'une des solutions NoSQL est gĂ©nĂ©ralement utilisĂ©e, mais tout ne peut pas ĂȘtre transfĂ©rĂ© vers NoSQL : certaines entitĂ©s nĂ©cessitent des garanties de transactions ACID.

Cela nous a amenĂ©s Ă  utiliser un stockage NewSQL, c'est-Ă -dire une base de donnĂ©es offrant la rĂ©silience, la scalabilitĂ© et la rapiditĂ© des systĂšmes NoSQL, tout en conservant les garanties ACID familiĂšres des systĂšmes classiques. Les systĂšmes industriels en fonctionnement de cette nouvelle classe sont peu nombreux, c'est pourquoi nous en avons rĂ©alisĂ© un nous-mĂȘmes et l'avons mis en production.

Comment cela fonctionne et quel en a Ă©tĂ© le rĂ©sultat — lis la suite.

Aujourd'hui, l'audience mensuelle d'Odnoklassniki dĂ©passe 70 millions de visiteurs uniques. Nous sommes dans le top cinq des plus grands rĂ©seaux sociaux du monde, et parmi les vingt sites sur lesquels les utilisateurs passent le plus de temps. L'infrastructure d'OK gĂšre des charges trĂšs Ă©levĂ©es : plus d'un million de requĂȘtes HTTP/seconde en front-end. Des parties du parc de serveurs, comptant plus de 8000 unitĂ©s, sont situĂ©es Ă  proximitĂ© les unes des autres — dans quatre centres de donnĂ©es Ă  Moscou, ce qui permet d'assurer une latence rĂ©seau infĂ©rieure Ă  1 ms entre eux.

Nous utilisons Cassandra depuis 2010, à partir de la version 0.6. Aujourd'hui, plusieurs dizaines de clusters sont en exploitation. Le cluster le plus rapide traite plus de 4 millions d'opérations par seconde, tandis que le plus important stocke 260 To.

Cependant, tout cela reprĂ©sente des clusters NoSQL ordinaires, utilisĂ©s pour stocker des donnĂ©es faiblement cohĂ©rentes. Nous souhaitions remplacer le stockage principal cohĂ©rent, Microsoft SQL Server, utilisĂ© depuis le lancement d'Odnoklassniki. Le stockage Ă©tait composĂ© de plus de 300 machines SQL Server Standard Edition, contenant 50 To de donnĂ©es — des entitĂ©s commerciales. Ces donnĂ©es sont modifiĂ©es dans le cadre de transactions ACID et nĂ©cessitent une haute cohĂ©rence..

Pour la rĂ©partition des donnĂ©es sur les nƓuds SQL Server, nous avons utilisĂ© tant du partitionnement vertical qu'horizontal. partitionnement (sharding). Historiquement, nous avons utilisĂ© un schĂ©ma de sharding des donnĂ©es simple : chaque entitĂ© se voit attribuer un jeton — une fonction de l'ID de l'entitĂ©. Les entitĂ©s partageant le mĂȘme jeton Ă©taient placĂ©es sur un seul serveur SQL. La relation de type master-detail Ă©tait mise en Ɠuvre de maniĂšre Ă  ce que les jetons de l'enregistrement principal et de l'enregistrement dĂ©rivĂ© coĂŻncident toujours et soient prĂ©sents sur un seul serveur. Dans un rĂ©seau social, presque tous les enregistrements sont gĂ©nĂ©rĂ©s au nom d'un utilisateur — ce qui signifie que toutes les donnĂ©es de l'utilisateur, au sein d'un mĂȘme sous-systĂšme fonctionnel, sont stockĂ©es sur un seul serveur. Ainsi, dans une transaction commerciale, presque toutes les tables appartenaient Ă  un seul serveur SQL, ce qui permettait d'assurer la cohĂ©rence des donnĂ©es grĂące Ă  des transactions ACID locales, sans nĂ©cessitĂ© d'utiliser des transactions ACID distribuĂ©es lentes et peu fiables. . GrĂące au sharding et pour amĂ©liorer les performances SQL :

Nous n'utilisons pas de contraintes de clé étrangÚre, car, lors du sharding, l'ID de l'entité peut se trouver sur un autre serveur.

  • Nous n'utilisons pas de procĂ©dures stockĂ©es ou de dĂ©clencheurs en raison de la charge supplĂ©mentaire sur le CPU du SGBD.
  • Nous n'utilisons pas de JOINs en raison de tout ce qui prĂ©cĂšde et de la multitude de lectures alĂ©atoires depuis le disque.
  • En dehors des transactions, pour rĂ©duire les blocages, nous utilisons le niveau d'isolation Read Uncommitted.
  • Nous effectuons uniquement des transactions courtes (en moyenne moins de 100 ms).
  • Nous n'utilisons pas de mises Ă  jour et de suppressions multi-lignes en raison du grand nombre de blocages — nous mettons Ă  jour un seul enregistrement Ă  la fois.
  • Les requĂȘtes sont toujours exĂ©cutĂ©es uniquement par index — une requĂȘte avec un plan de lecture complĂšte de la table pour nous signifie une surcharge de la base de donnĂ©es et son Ă©chec.
  • Ces Ă©tapes ont permis de tirer presque le maximum de performance des serveurs SQL. Cependant, les problĂšmes augmentaient de plus en plus. Examinons-les.

ProblĂšmes avec SQL

Étant donnĂ© que nous utilisions un sharding fait maison, l'ajout de nouveaux shards Ă©tait effectuĂ© manuellement par les administrateurs. Pendant tout ce temps, les rĂ©pliques de donnĂ©es Ă©volutives ne traitaient pas de requĂȘtes.

  • À mesure que le nombre d'enregistrements dans une table augmente, la vitesse d'insertion et de modification diminue, et l'ajout d'index Ă  une table existante provoque une chute exponentielle de la vitesse, la crĂ©ation et la recrĂ©ation d'index se fait avec des temps d'arrĂȘt.
  • La prĂ©sence en production d'un petit nombre de Windows pour SQL Server complique la gestion de l'infrastructure.
  • Mais le principal problĂšme —

Mais le principal problùme est —

Résilience

Un serveur SQL classique présente une mauvaise résilience. Supposons que vous n'ayez qu'un seul serveur de base de données, et qu'il tombe en panne une fois tous les trois ans. Pendant ce temps, le site ne fonctionne pas pendant 20 minutes, ce qui est acceptable. Si vous avez 64 serveurs, le site est hors service une fois toutes les trois semaines. Et si vous avez 200 serveurs, le site ne fonctionne pas chaque semaine. C'est un problÚme.

Que peut-on faire pour amĂ©liorer la rĂ©silience des serveurs SQL ? WikipĂ©dia nous suggĂšre de construire un cluster Ă  haute disponibilitĂ©: oĂč, en cas de dĂ©faillance de l'un des composants, il y a un duplicata.

Cela nĂ©cessite un parc de matĂ©riel coĂ»teux : de nombreuses redondances, de la fibre optique, des systĂšmes de stockage partagĂ©s, et de plus, l'activation de la redondance fonctionne de maniĂšre peu fiable : environ 10 % des activations se terminent par l'Ă©chec de la nƓud de secours, entraĂźnant le nƓud principal.

Mais le principal inconvĂ©nient d'un tel cluster Ă  haute disponibilitĂ© est l'absence totale de disponibilitĂ© en cas de dĂ©faillance du centre de donnĂ©es oĂč il se trouve. Odnoklassniki dispose de quatre centres de donnĂ©es, et nous devons assurer le fonctionnement en cas de sinistre total dans l'un d'eux.

Pour cela, on pourrait appliquer une réplication Multi-Master intégrée à SQL Server. Cette solution est beaucoup plus coûteuse en raison des frais logiciels et souffre de problÚmes de réplication bien connus - des délais de transactions imprévisibles lors de la réplication synchrone et des délais dans l'application des réplications (et, par conséquent, des modifications perdues) en mode asynchrone. La nécessité de résoudre manuellement les conflits rend cette option complÚtement inapplicable pour nous.

Tous ces problÚmes nécessitaient une solution radicale, et nous avons commencé à les analyser en détail. Ici, nous devons nous familiariser avec ce que fait principalement SQL Server : les transactions.

Une transaction simple

Prenons la transaction la plus simple du point de vue d'un programmeur SQL appliqué : l'ajout d'une photo à un album. Les albums et les photos sont stockés dans différentes tables. Un album a un compteur de photos publiques. Ainsi, cette transaction se décompose en plusieurs étapes :

  1. Nous verrouillons l'album par clé.
  2. Nous créons un enregistrement dans la table des photos.
  3. Si la photo est publique, nous incrémentons le compteur de photos publiques dans l'album, mettons à jour l'enregistrement, et validons la transaction.

Ou sous forme de pseudocode :

TX.start("Albums", id);
Album album = albums.lock(id);
Photo photo = photos.create(
);

if (photo.status == PUBLIC) {
    album.incPublicPhotosCount();
}
album.update();

TX.commit();

Nous voyons que le scénario de transaction d'entreprise le plus courant consiste à lire les données de la base de données en mémoire du serveur d'applications, à modifier quelque chose et à enregistrer les nouvelles valeurs dans la base de données. En général, dans une telle transaction, nous mettons à jour plusieurs entités, plusieurs tables.

Lors de l'exĂ©cution d'une transaction, une modification concurrente des mĂȘmes donnĂ©es par un autre systĂšme peut se produire. Par exemple, un anti-spam peut dĂ©cider qu'un utilisateur est suspect et que toutes les photos de cet utilisateur ne doivent plus ĂȘtre publiques, qu'elles doivent ĂȘtre soumises Ă  modĂ©ration, ce qui signifie que photo.status doit ĂȘtre changĂ© en une autre valeur et que les compteurs correspondants doivent ĂȘtre ajustĂ©s. Il est Ă©vident que si cette opĂ©ration se produit sans garanties d'atome d'application et d'isolation des modifications concurrentes, comme dans ACID, le rĂ©sultat ne sera pas celui attendu — soit le compteur de photos affichera une valeur incorrecte, soit toutes les photos ne seront pas soumises Ă  modĂ©ration.

Un tel code, manipulant différentes entités commerciales dans le cadre d'une seule transaction, a été écrit en trÚs grand nombre au fil des ans depuis l'existence de Odnoklassniki. De notre expérience en migrations vers NoSQL avec Eventual Consistency , nous savons que les plus grandes difficultés (et les délais associés) sont liées à la nécessité de développer du code destiné à maintenir la cohérence des données. Par conséquent, la principale exigence pour le nouveau stockage était d'assurer des transactions ACID réelles pour la logique appliquée.

D'autres exigences tout aussi importantes étaient :

  • En cas d'Ă©chec du centre de donnĂ©es, Ă  la fois la lecture et l'Ă©criture doivent ĂȘtre disponibles dans le nouveau stockage.
  • Maintien de la vitesse actuelle de dĂ©veloppement. Cela signifie qu'en travaillant avec le nouveau stockage, le volume de code doit rester Ă  peu prĂšs le mĂȘme, sans qu'il soit nĂ©cessaire d'ajouter quoi que ce soit au stockage, de dĂ©velopper des algorithmes de rĂ©solution de conflits, de maintenir des index secondaires, etc.
  • La rapiditĂ© de fonctionnement du nouveau stockage doit ĂȘtre suffisamment Ă©levĂ©e, tant pour la lecture des donnĂ©es que pour le traitement des transactions, ce qui signifiait en effet l'inapplicabilitĂ© de solutions acadĂ©miquement strictes, universelles, mais lentes, comme par exemple les commits Ă  deux phases.
  • Mise Ă  l'Ă©chelle automatique Ă  la volĂ©e.
  • Utilisation de serveurs ordinaires Ă  bas prix, sans avoir besoin d'acheter du matĂ©riel exotique.
  • PossibilitĂ© de dĂ©veloppement du stockage par les dĂ©veloppeurs de l'entreprise. En d'autres termes, la prioritĂ© Ă©tait donnĂ©e aux solutions propriĂ©taires ou open source, de prĂ©fĂ©rence en Java.

Solutions, solutions

En analysant les solutions possibles, nous sommes arrivés à deux choix possibles d'architecture :

Le premier consiste Ă  prendre n'importe quel serveur SQL et Ă  mettre en Ɠuvre la redondance nĂ©cessaire, le mĂ©canisme de mise Ă  l'Ă©chelle, le cluster tolĂ©rant aux pannes, la rĂ©solution des conflits et des transactions ACID distribuĂ©es, fiables et rapides. Nous avons Ă©valuĂ© cette option comme Ă©tant plutĂŽt non triviale et laborieuse.

La deuxiĂšme option consiste Ă  prendre un stockage NoSQL prĂȘt Ă  l'emploi avec mise Ă  l'Ă©chelle intĂ©grĂ©e, un cluster tolĂ©rant aux pannes, la rĂ©solution des conflits et Ă  implĂ©menter nous-mĂȘmes les transactions et SQL. À premiĂšre vue, mĂȘme la tĂąche de mise en Ɠuvre de SQL, sans parler des transactions ACID, semble une tĂąche de plusieurs annĂ©es. Mais ensuite, nous avons rĂ©alisĂ© que l'ensemble des fonctionnalitĂ©s SQL que nous utilisons en pratique s'Ă©loigne autant de l'ANSI SQL que Cassandra CQL s'Ă©loigne de l'ANSI SQL. AprĂšs un examen plus attentif, nous avons compris qu'il est suffisamment proche de ce dont nous avons besoin.

Cassandra et CQL

Alors, qu'est-ce qui rend Cassandra intéressante, quelles sont ses capacités ?

Tout d'abord, il est possible de créer des tables prenant en charge différents types de données, et l'on peut effectuer des opérations SELECT ou UPDATE sur la clé primaire.

CREATE TABLE photos (id bigint KEY, owner bigint,
);
SELECT * FROM photos WHERE id=?;
UPDATE photos SET 
 WHERE id=?;

Pour garantir la cohĂ©rence des donnĂ©es des rĂ©pliques, Cassandra utilise une approche basĂ©e sur le quorum. Dans le cas le plus simple, cela signifie que lorsqu'on dĂ©ploie trois rĂ©pliques d'une mĂȘme ligne sur diffĂ©rentes nƓuds du cluster, l'Ă©criture est considĂ©rĂ©e comme rĂ©ussie si la majoritĂ© des nƓuds (c'est-Ă -dire deux sur trois) ont confirmĂ© le succĂšs de cette opĂ©ration d'Ă©criture. Les donnĂ©es de la ligne sont considĂ©rĂ©es comme cohĂ©rentes si, lors de la lecture, la majoritĂ© des nƓuds ont Ă©tĂ© interrogĂ©s et ont confirmĂ© leur Ă©tat. Ainsi, en ayant trois rĂ©pliques, on garantit une cohĂ©rence des donnĂ©es complĂšte et instantanĂ©e en cas de dĂ©faillance d'un nƓud. Cette approche nous a permis de mettre en place un schĂ©ma encore plus fiable : toujours envoyer des requĂȘtes aux trois rĂ©pliques, en attendant la rĂ©ponse des deux plus rapides. La rĂ©ponse tardive de la troisiĂšme rĂ©plique est alors rejetĂ©e. Le nƓud dont la rĂ©ponse est en retard peut rencontrer de graves problĂšmes : ralentissements, collecte de dĂ©chets dans la JVM, rĂ©cupĂ©ration de mĂ©moire directe dans le noyau linux, dĂ©faillance matĂ©rielle, coupure rĂ©seau. Cependant, cela n'affecte en rien les opĂ©rations du client ni les donnĂ©es.

L'approche oĂč nous contactons trois nƓuds et recevons la rĂ©ponse de deux est appelĂ©e spĂ©culation: la demande d'extras rĂ©pliques est envoyĂ©e avant mĂȘme qu'elle ne « tombe ».

Un autre des avantages de Cassandra est le Batchlog - un mĂ©canisme garantissant soit l'application complĂšte, soit le rejet complet d'un lot de modifications que vous apportez. Cela nous permet de rĂ©soudre A dans ACID - l'atomicitĂ© prĂȘte Ă  l'emploi.

Ce qui se rapproche le plus des transactions dans Cassandra, ce sont les so-called «transactions légÚres». Mais elles sont loin des « véritables » transactions ACID : en réalité, il s'agit de la possibilité de faire CAS sur les données d'un seul enregistrement, en utilisant le consensus par le protocole lourd Paxos. Par conséquent, la vitesse de telles transactions est faible.

Ce qui nous a manqué dans Cassandra

Ainsi, nous devions implémenter de véritables transactions ACID dans Cassandra. Avec lesquelles nous pourrions facilement réaliser deux autres capacités pratiques des SGBD classiques : des index rapides et cohérents, ce qui nous permettrait d'effectuer des sélections de données non seulement par clé primaire et un générateur de ID auto-incrémentaux monotones normal.

C*One

Ainsi est nĂ©e une nouvelle base de donnĂ©es C*One, composĂ©e de trois types de nƓuds serveurs :

  • Stockage - (presque) serveurs standard Cassandra, responsables du stockage des donnĂ©es sur les disques locaux. À mesure que la charge et le volume des donnĂ©es augmentent, leur nombre peut facilement ĂȘtre Ă©voluĂ© en dizaines et centaines.
  • Les coordinateurs de transactions garantissent l'exĂ©cution des transactions.
  • Les clients sont des serveurs d'applications qui rĂ©alisent des opĂ©rations commerciales et initient des transactions. Il peut y avoir des milliers de tels clients.

NewSQL = NoSQL+ACID

Tous les serveurs de tous types font partie d'un cluster commun, utilisant un protocole interne de messagerie Cassandra pour communiquer entre eux et gossip Ă©changer des informations de cluster. GrĂące Ă  Heartbeat, les serveurs prennent connaissance des pannes mutuelles, maintiennent un schĂ©ma de donnĂ©es cohĂ©rent — tables, leur structure et rĂ©plication ; schĂ©ma de partitionnement, topologie du cluster, etc.

Clients

NewSQL = NoSQL+ACID

Au lieu des pilotes standard, un mode Fat Client est utilisĂ©. Ce nƓud ne stocke pas de donnĂ©es, mais peut jouer le rĂŽle de coordinateur d'exĂ©cution des requĂȘtes, c'est-Ă -dire que le client exĂ©cute lui-mĂȘme la fonction de coordinateur de ses requĂȘtes : il interroge les rĂ©pliques du stockage et rĂ©sout les conflits. Cela est non seulement plus fiable et plus rapide que le pilote standard, qui nĂ©cessite une communication avec un coordinateur distant, mais permet Ă©galement de gĂ©rer le transfert des requĂȘtes. En dehors d'une transaction ouverte sur le client, les requĂȘtes sont dirigĂ©es vers les stockages. Si le client a ouvert une transaction, toutes les requĂȘtes dans le cadre de celle-ci sont envoyĂ©es au coordinateur de transactions.
NewSQL = NoSQL+ACID

Coordinateur de transactions C*One

Le coordinateur est ce que nous avons mis en Ɠuvre pour C*One depuis le dĂ©but. Il est responsable de la gestion des transactions, des verrous et de l'ordre d'application des transactions.

Pour chaque transaction traitĂ©e, le coordinateur gĂ©nĂšre un horodatage : chaque horodatage suivant est supĂ©rieur Ă  celui de la transaction prĂ©cĂ©dente. Comme dans Cassandra, le systĂšme de rĂ©solution des conflits est basĂ© sur les horodatages (parmi deux enregistrements conflictuels, celui avec l'horodatage le plus rĂ©cent est considĂ©rĂ© comme valide), le conflit sera toujours rĂ©solu en faveur de la transaction suivante. Ainsi, nous avons rĂ©alisĂ© les horloges de Lamport — une maniĂšre peu coĂ»teuse de rĂ©soudre les conflits dans un systĂšme rĂ©parti.

Verrous

Pour garantir l'isolation, nous avons dĂ©cidĂ© d'utiliser la mĂ©thode la plus simple — les verrous pessimistes basĂ©s sur la clĂ© primaire de l'enregistrement. En d'autres termes, dans la transaction, l'enregistrement doit d'abord ĂȘtre verrouillĂ©, puis lu, modifiĂ© et enregistrĂ©. Ce n'est qu'aprĂšs une validation rĂ©ussie que l'enregistrement peut ĂȘtre dĂ©verrouillĂ©, afin que les transactions concurrentes puissent l'utiliser.

La mise en Ɠuvre d'un tel verrouillage est simple dans un environnement non distribuĂ©. Dans un systĂšme distribuĂ©, il existe deux approches principales : soit mettre en Ɠuvre un verrouillage distribuĂ© sur le cluster, soit rĂ©partir les transactions de maniĂšre Ă  ce que les transactions impliquant un mĂȘme enregistrement soient toujours traitĂ©es par le mĂȘme coordinateur.

Étant donnĂ© que, dans notre cas, les donnĂ©es sont dĂ©jĂ  rĂ©parties en groupes de transactions locales dans SQL, il a Ă©tĂ© dĂ©cidĂ© d'attribuer des coordinateurs aux groupes de transactions locales : un coordinateur gĂšre toutes les transactions avec un jeton de 0 Ă  9, le second avec un jeton de 10 Ă  19, et ainsi de suite. En consĂ©quence, chaque instance de coordinateur devient le maĂźtre du groupe de transactions.

Ainsi, les verrouillages peuvent ĂȘtre rĂ©alisĂ©s sous forme de simple HashMap dans la mĂ©moire du coordinateur.

Pannes des coordinateurs

Étant donnĂ© qu'un coordinateur ne gĂšre qu'un groupe de transactions, il est trĂšs important de dĂ©tecter rapidement toute dĂ©faillance pour que la nouvelle tentative d'exĂ©cuter une transaction soit rĂ©alisĂ©e dans le dĂ©lai imparti. Pour cela, nous avons appliquĂ© un protocole de heartbeat en quorum entiĂšrement connectĂ© :

Dans chaque centre de donnĂ©es, au moins deux nƓuds coordinateurs sont placĂ©s. PĂ©riodiquement, chaque coordinateur envoie un message heartbeat aux autres coordinateurs, leur rapportant son bon fonctionnement ainsi que les messages heartbeat qu'il a reçus des autres coordinateurs du cluster.

NewSQL = NoSQL+ACID

En recevant des informations similaires des autres dans leurs messages heartbeat, chaque coordinateur dĂ©termine quelles nƓuds du cluster fonctionnent et lesquelles ne le font pas, en suivant le principe du quorum : si le nƓud X a reçu la confirmation de la plupart des nƓuds du cluster concernant la bonne rĂ©ception des messages du nƓud Y, cela signifie que Y est opĂ©rationnel. Et inversement, dĂšs que la plupart rapportent la perte de messages du nƓud Y, cela signifie que Y a Ă©chouĂ©. Fait intĂ©ressant, si le quorum informe le nƓud X qu'il ne reçoit plus de messages de celui-ci, le nƓud X se considĂ©rera alors comme dĂ©faillant.

Les messages de cƓur battant sont envoyĂ©s Ă  une frĂ©quence Ă©levĂ©e, environ 20 fois par seconde, avec une pĂ©riode de 50 ms. En Java, il est difficile de garantir la rĂ©ponse de l'application dans les 50 ms en raison de la durĂ©e comparable des pauses causĂ©es par le ramasse-miettes. Nous avons rĂ©ussi Ă  atteindre ce temps de rĂ©ponse en utilisant le ramasse-miettes G1, qui permet de dĂ©finir un objectif pour la durĂ©e des pauses GC. Cependant, parfois, assez rarement, les pauses du ramasse-miettes dĂ©passent les 50 ms, ce qui peut entraĂźner une dĂ©tection erronĂ©e de dĂ©faillance. Pour Ă©viter cela, le coordinateur ne signale pas la dĂ©faillance d'un nƓud distant aprĂšs la perte du premier message de cƓur battant, seulement si plusieurs sont perdus consĂ©cutivement. Ainsi, nous avons rĂ©ussi Ă  dĂ©tecter la dĂ©faillance d'un nƓud coordinateur en 200 ms.

Mais il ne suffit pas de comprendre rapidement quel nƓud a cessĂ© de fonctionner. Il faut agir.

Redondance

Le schĂ©ma classique prĂ©voit qu'en cas de dĂ©faillance du maĂźtre, de nouvelles Ă©lections soient lancĂ©es Ă  l'aide de l'un des algorithmes Ă  la mode. universels. Cependant, de tels algorithmes ont des problĂšmes bien connus de convergence dans le temps et de durĂ©e du processus Ă©lectoral lui-mĂȘme. Nous avons rĂ©ussi Ă  Ă©viter ces dĂ©lais supplĂ©mentaires grĂące Ă  un schĂ©ma de remplacement des coordinateurs dans un rĂ©seau complĂštement connectĂ© :

NewSQL = NoSQL+ACID

Supposons que nous souhaitons effectuer une transaction dans le groupe 50. DĂ©terminons Ă  l'avance le schĂ©ma de remplacement, c'est-Ă -dire quels nƓuds exĂ©cuteront les transactions du groupe 50 en cas de dĂ©faillance du coordinateur principal. Notre but est de maintenir le fonctionnement du systĂšme en cas de dĂ©faillance d'un centre de donnĂ©es. Nous allons dĂ©cider que le premier remplacement sera un nƓud d'un autre centre de donnĂ©es, et le deuxiĂšme remplacement sera un nƓud d'un troisiĂšme. Ce schĂ©ma est choisi une fois et ne change pas tant que la topologie du cluster ne change pas, c'est-Ă -dire tant que de nouveaux nƓuds ne rejoignent pas (ce qui se produit trĂšs rarement). L'ordre de sĂ©lection d'un nouveau maĂźtre actif en cas de dĂ©faillance de l'ancien sera toujours le suivant : le premier remplacement deviendra le maĂźtre actif, et s'il cesse Ă©galement de fonctionner, le deuxiĂšme remplacement prendra le relais.

Ce schéma est plus fiable qu'un algorithme universel, car pour activer un nouveau maßtre, il suffit de déterminer le fait de la défaillance de l'ancien.

Mais comment les clients sauront-ils quel maßtre est actif en ce moment ? Il est impossible d'envoyer des informations à des milliers de clients en 50 ms. Il peut arriver qu'un client envoie une demande d'ouverture de transaction sans savoir que ce maßtre n'est plus actif, et la demande pourrait se bloquer à cause d'un timeout. Pour éviter cela, les clients envoient spéculativement une demande d'ouverture de transaction simultanément au maßtre du groupe et à ses deux réserves, mais seule la réponse du maßtre actif sera traitée. Toute communication ultérieure dans le cadre de la transaction sera uniquement avec le maßtre actif.

Les maĂźtres de rĂ©serve placent les demandes reçues pour des transactions qui ne leur appartiennent pas dans une file d'attente de transactions non rĂ©alisĂ©es, oĂč elles restent pendant un certain temps. Si le maĂźtre actif meurt, un nouveau maĂźtre traite les demandes d'ouverture de transactions de sa file d'attente et rĂ©pond au client. Si le client a dĂ©jĂ  rĂ©ussi Ă  ouvrir une transaction avec l'ancien maĂźtre, la seconde rĂ©ponse est ignorĂ©e (et, de toute Ă©vidence, cette transaction ne sera pas finalisĂ©e et devra ĂȘtre rĂ©pĂ©tĂ©e par le client).

Comment fonctionne une transaction

Supposons qu'un client a envoyé une demande d'ouverture de transaction au coordinateur pour une certaine entité avec une certaine clé primaire. Le coordinateur bloque cette entité et l'ajoute à la table des verrouillages en mémoire. Si nécessaire, le coordinateur lit cette entité depuis le stockage et sauvegarde les données obtenues dans l'état de la transaction en mémoire du coordinateur.

NewSQL = NoSQL+ACID

Lorsque le client souhaite modifier des donnĂ©es dans la transaction, il envoie une demande de modification d'entitĂ© au coordinateur, qui place les nouvelles donnĂ©es dans la table d'Ă©tat des transactions en mĂ©moire. À ce stade, l'enregistrement est terminĂ© — aucune Ă©criture dans le stockage n'est effectuĂ©e.

NewSQL = NoSQL+ACID

Lorsque le client demande ses propres données modifiées dans le cadre d'une transaction active, le coordinateur agit comme suit :

  • si l'ID est dĂ©jĂ  prĂ©sent dans la transaction, les donnĂ©es sont prises de la mĂ©moire ;
  • si l'ID est absent en mĂ©moire, les donnĂ©es manquantes sont lues des nƓuds de stockage, combinĂ©es avec celles dĂ©jĂ  prĂ©sentes en mĂ©moire, et le rĂ©sultat est renvoyĂ© au client.

Ainsi, le client peut lire ses propres modifications, tandis que les autres clients ne voient pas ces modifications, car elles ne sont stockĂ©es que dans la mĂ©moire du coordinateur, elles ne sont pas encore prĂ©sentes dans les nƓuds Cassandra.

NewSQL = NoSQL+ACID

Lorsque le client envoie un commit, l'état en mémoire du service est enregistré par le coordinateur dans un batch enregistré, qui est ensuite envoyé aux systÚmes de stockage Cassandra sous forme de batch enregistré. Les systÚmes de stockage effectuent tout le nécessaire pour que ce batch soit appliqué de maniÚre atomique (entiÚrement) et renvoient une réponse au coordinateur, qui libÚre alors les verrous et confirme le succÚs de la transaction au client.

NewSQL = NoSQL+ACID

Et pour annuler, il suffit au coordinateur de libérer la mémoire occupée par l'état de la transaction.

À la suite des amĂ©liorations dĂ©crites ci-dessus, nous avons mis en Ɠuvre les principes ACID :

  • AtomicitĂ©. Cela garantit qu'aucune transaction ne sera enregistrĂ©e partiellement dans le systĂšme, soit toutes ses sous-opĂ©rations seront exĂ©cutĂ©es, soit aucune d'entre elles ne le sera. Ce principe est respectĂ© chez nous grĂące au batch enregistrĂ© dans Cassandra.
  • CohĂ©rence. Chaque transaction rĂ©ussie enregistre par dĂ©finition uniquement des rĂ©sultats valides. Si, aprĂšs l'ouverture de la transaction et l'exĂ©cution d'une partie des opĂ©rations, il est constatĂ© que le rĂ©sultat est invalide, un rollback est effectuĂ©.
  • Isolation. Lors de l'exĂ©cution de la transaction, les transactions parallĂšles ne doivent pas affecter son rĂ©sultat. Les transactions concurrentes sont isolĂ©es par le biais de verrous pessimistes sur le coordinateur. Pour les lectures en dehors de la transaction, le principe d'isolation est respectĂ© au niveau Read Committed.
  • RĂ©silience. IndĂ©pendamment des problĂšmes sur les niveaux infĂ©rieurs — coupure de courant, dĂ©faillance matĂ©rielle — les modifications apportĂ©es par une transaction ayant Ă©tĂ© correctement finalisĂ©e doivent rester enregistrĂ©es aprĂšs la reprise des opĂ©rations.

Lecture par index

Prenons une table simple :

CREATE TABLE photos (
id bigint primary key,
owner bigint,
modified timestamp,

)

Elle a un ID (clĂ© primaire), un propriĂ©taire et une date de modification. Nous devons exĂ©cuter une requĂȘte trĂšs simple — sĂ©lectionner des donnĂ©es par propriĂ©taire avec une date de modification « au cours des derniĂšres 24 heures ».

SELECT *
WHERE owner=?
AND modified>?

Pour que ce type de requĂȘte soit exĂ©cutĂ© rapidement, dans une base de donnĂ©es SQL classique, il est nĂ©cessaire de crĂ©er un index sur les colonnes (owner, modified). Nous pouvons faire cela assez facilement, car nous avons maintenant des garanties ACID !

Index dans C*One

Il existe une table source avec des photos, oĂč l'ID de l'enregistrement est la clĂ© primaire.

NewSQL = NoSQL+ACID

Pour l'index, C*One crée une nouvelle table qui est une copie de la table d'origine. La clé coïncide avec l'expression indexée, tout en incluant également la clé primaire de l'enregistrement de la table d'origine :

NewSQL = NoSQL+ACID

DĂ©sormais, la requĂȘte « propriĂ©taire pour les derniĂšres 24 heures » peut ĂȘtre réécrite comme un select d'une autre table :

SELECT * FROM i1_test
WHERE owner=?
AND modified > ?

La cohérence des données de la table d'origine photos et de l'index i1 est automatiquement maintenue par le coordonnateur. Sur la base du seul schéma de données, lors de la réception d'une modification, le coordonnateur génÚre et mémorise la modification non seulement de la table principale, mais aussi des modifications des copies. Aucune action supplémentaire sur la table d'index n'est effectuée, les journaux ne sont pas lus, les verrous ne sont pas utilisés. En d'autres termes, l'ajout d'index consomme presque aucun ressource et n'a pratiquement aucun impact sur la vitesse d'application des modifications.

Avec ACID, nous avons rĂ©ussi Ă  mettre en Ɠuvre des index « comme en SQL ». Ils offrent la cohĂ©rence, peuvent Ă©voluer, fonctionnent rapidement, peuvent ĂȘtre composĂ©s et intĂ©grĂ©s dans le langage de requĂȘtes CQL. Aucun changement dans le code applicatif n'est nĂ©cessaire pour soutenir les index. C'est aussi simple qu'en SQL. Et surtout, les index n'affectent pas la vitesse d'exĂ©cution des modifications de la table de transactions d'origine.

Ce qui a été réalisé

Nous avons développé C*One il y a trois ans et l'avons mis en service.

Qu'avons-nous obtenu en fin de compte ? Évaluons cela Ă  travers le sous-systĂšme de traitement et de stockage des photographies, l'un des types de donnĂ©es les plus importants dans le rĂ©seau social. Il ne s'agit pas des fichiers photos eux-mĂȘmes, mais de toutes sortes de mĂ©tadonnĂ©es. Actuellement, il y a environ 20 milliards de ces enregistrements dans « Odnoklassniki », le systĂšme traite 80 000 requĂȘtes de lecture par seconde, jusqu'Ă  8 000 transactions ACID par seconde, liĂ©es Ă  la modification des donnĂ©es.

Lorsque nous utilisions SQL avec un facteur de réplication = 1 (mais en RAID 10), les métadonnées des photos étaient stockées sur un cluster hautement disponible de 32 machines avec Microsoft SQL Server (plus 11 de sauvegarde). De plus, 10 serveurs étaient réservés pour le stockage des sauvegardes. Au total, 50 machines coûteuses. Pendant ce temps, le systÚme fonctionnait à charge nominale, sans marge.

AprĂšs la migration vers le nouveau systĂšme, nous avons obtenu un facteur de rĂ©plication = 3 — une copie dans chaque centre de donnĂ©es. Le systĂšme se compose de 63 nƓuds de stockage Cassandra et de 6 machines de coordonnateurs, soit 69 serveurs au total. Mais ces machines sont beaucoup moins chĂšres, leur coĂ»t total reprĂ©sente environ 30 % du coĂ»t du systĂšme SQL. Pendant ce temps, la charge reste Ă  un niveau de 30 %.

Avec l'introduction de C*One, les latences ont également diminué : dans SQL, l'opération d'écriture prenait environ 4,5 ms. Avec C*One, cela prend environ 1,6 ms. La durée des transactions est en moyenne inférieure à 40 ms, le commit s'effectue en 2 ms, et la durée de lecture et d'écriture est en moyenne de 2 ms. Le 99Úme percentile est à seulement 3-3,1 ms, et le nombre de timeouts a diminué de 100 fois, tout cela grùce à l'utilisation généralisée de spéculations.

À ce jour, la plupart des nƓuds SQL Server ont Ă©tĂ© retirĂ©s de l'exploitation, et les nouveaux produits sont dĂ©veloppĂ©s uniquement avec C*One. Nous avons adaptĂ© C*One pour qu'il fonctionne dans notre cloud. one-cloud, ce qui a permis d'accĂ©lĂ©rer le dĂ©ploiement de nouveaux clusters, de simplifier la configuration et d'automatiser l'exploitation. Sans le code source, cela aurait Ă©tĂ© beaucoup plus compliquĂ© et bricolĂ©.

Nous travaillons actuellement Ă  la migration de nos autres entrepĂŽts vers le cloud - mais c'est une toute autre histoire.

Source : habr.com

Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS đŸ”„ Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster