
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 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 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 .
Pour la rĂ©partition des donnĂ©es sur les nĆuds SQL Server, nous avons utilisĂ© tant du partitionnement vertical qu'horizontal. (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 . 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 : 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 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 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 :
- Nous verrouillons l'album par clé.
- Nous créons un enregistrement dans la table des photos.
- 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 , 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 , 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 .
- 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 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 . 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 : 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 «». Mais elles sont loin des « véritables » transactions ACID : en réalité, il s'agit de la possibilité de faire 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.

Tous les serveurs de tous types font partie d'un cluster commun, utilisant un protocole interne de messagerie Cassandra pour communiquer entre eux et Ă©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

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.

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Ă© â 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.

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 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Ă© :

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.

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.

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.

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.

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.

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 :

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. , 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
