NouveauSQL = NoSQL+ACID

NouveauSQL = NoSQL+ACID
Jusqu'à récemment, Odnoklassniki stockait environ 50 To de données traitées en temps réel dans SQL Server. Pour un tel volume, il est presque impossible de fournir un accès rapide et fiable, voire insensible aux pannes au centre de données, à l'aide d'un SGBD SQL. Généralement, dans de tels cas, l'un des stockages NoSQL est utilisé, mais tout ne peut pas être transféré vers NoSQL : certaines entités nécessitent des garanties de transaction ACID.

Cela nous a conduit à utiliser le stockage NewSQL, c'est-à-dire un SGBD qui offre la tolérance aux pannes, l'évolutivité et les performances des systèmes NoSQL, tout en conservant les garanties ACID familières aux systèmes classiques. Il existe peu de systèmes industriels fonctionnels de cette nouvelle classe, c'est pourquoi nous avons mis en œuvre nous-mêmes un tel système et l'avons mis en exploitation commerciale.

Comment cela fonctionne et ce qui s'est passé - lisez sous la coupe.

Aujourd'hui, l'audience mensuelle d'Odnoklassniki s'élève à plus de 70 millions de visiteurs uniques. Nous Nous sommes dans les cinq premiers plus grand réseau social au monde, et parmi les vingt sites sur lesquels les utilisateurs passent le plus de temps. L'infrastructure OK gère des charges très élevées : plus d'un million de requêtes HTTP/s par front. Certaines parties d'un parc de serveurs de plus de 8000 1 éléments sont situées à proximité les unes des autres, dans quatre centres de données de Moscou, ce qui permet une latence réseau inférieure à XNUMX ms entre eux.

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

Cependant, ce sont tous des clusters NoSQL ordinaires utilisés pour le stockage faiblement coordonné données. Nous voulions remplacer le principal stockage cohérent, Microsoft SQL Server, utilisé depuis la création d'Odnoklassniki. Le stockage se composait de plus de 300 machines SQL Server Standard Edition, qui contenaient 50 To de données – entités commerciales. Ces données sont modifiées dans le cadre des transactions ACID et nécessitent haute consistance.

Pour distribuer les données sur les nœuds SQL Server, nous avons utilisé à la fois vertical et horizontal partitionnement (partage). Historiquement, nous utilisions un schéma simple de partage des données : chaque entité était associée à un jeton - une fonction de l'ID de l'entité. Les entités portant le même jeton ont été placées sur le même serveur SQL. La relation maître-détails a été implémentée de manière à ce que les jetons des enregistrements principaux et enfants correspondent toujours et soient situés sur le même serveur. Dans un réseau social, presque tous les enregistrements sont générés au nom de l'utilisateur, ce qui signifie que toutes les données utilisateur d'un sous-système fonctionnel sont stockées sur un seul serveur. Autrement dit, une transaction commerciale impliquait presque toujours des tables d'un serveur SQL, ce qui permettait d'assurer la cohérence des données à l'aide de transactions ACID locales, sans avoir besoin d'utiliser lent et peu fiable transactions ACID distribuées.

Grâce au sharding et pour accélérer SQL :

  • Nous n'utilisons pas de contraintes de clé étrangère, car lors du partitionnement, l'ID d'entité peut être situé sur un autre serveur.
  • Nous n'utilisons pas de procédures stockées ni de déclencheurs en raison de la charge supplémentaire sur le processeur du SGBD.
  • Nous n'utilisons pas de JOIN à cause de tout ce qui précède et de nombreuses lectures aléatoires sur le disque.
  • En dehors d’une transaction, nous utilisons le niveau d’isolement Read Uncommit pour réduire les blocages.
  • Nous effectuons uniquement des transactions courtes (en moyenne inférieures à 100 ms).
  • Nous n'utilisons pas UPDATE et DELETE multi-lignes en raison du grand nombre de blocages - nous ne mettons à jour qu'un seul enregistrement à la fois.
  • Nous effectuons toujours des requêtes uniquement sur les index - une requête avec un plan d'analyse de table complet signifie pour nous une surcharge de la base de données et son échec.

Ces étapes nous ont permis d'obtenir des performances presque maximales des serveurs SQL. Mais les problèmes sont devenus de plus en plus nombreux. Regardons-les.

Problèmes avec SQL

  • Puisque nous utilisions le partitionnement auto-écrit, l’ajout de nouveaux fragments était effectué manuellement par les administrateurs. Pendant tout ce temps, les réplicas de données évolutifs ne répondaient pas aux demandes.
  • À mesure que le nombre d'enregistrements dans la table augmente, la vitesse d'insertion et de modification diminue ; lors de l'ajout d'index à une table existante, la vitesse diminue d'un facteur ; la création et la recréation d'index se produisent avec des temps d'arrêt.
  • Avoir une petite quantité de Windows pour SQL Server en production rend la gestion de l'infrastructure difficile

Mais le principal problème est

tolérance aux pannes

Le serveur SQL classique a une mauvaise tolérance aux pannes. Disons que vous n'avez qu'un seul serveur de base de données et qu'il tombe en panne tous les trois ans. Pendant ce temps, le site est indisponible pendant 20 minutes, ce qui est acceptable. Si vous disposez de 64 serveurs, le site est indisponible une fois toutes les trois semaines. Et si vous disposez de 200 serveurs, alors le site ne fonctionne pas toutes les semaines. C'est un problème.

Que peut-on faire pour améliorer la tolérance aux pannes d’un serveur SQL ? Wikipédia nous invite à construire cluster hautement disponible: où en cas de panne de l'un des composants, il existe un composant de secours.

Cela nécessite un parc d'équipements coûteux : nombreuses duplications, fibre optique, stockage partagé, et l'inclusion d'une réserve ne fonctionne pas de manière fiable : environ 10 % des commutations se terminent par la panne du nœud de secours comme un train derrière le nœud principal.

Mais le principal inconvénient d'un cluster aussi hautement disponible est une disponibilité nulle en cas de panne du centre de données dans lequel il se trouve. Odnoklassniki dispose de quatre centres de données et nous devons assurer le fonctionnement en cas de panne totale de l'un d'eux.

Pour cela, nous pourrions utiliser Multi-maître réplication intégrée à SQL Server. Cette solution est beaucoup plus coûteuse en raison du coût du logiciel et souffre de problèmes bien connus de réplication : des retards de transaction imprévisibles avec la réplication synchrone et des retards dans l'application des réplications (et, par conséquent, des modifications perdues) avec la réplication asynchrone. L'implicite résolution manuelle des conflits rend cette option totalement 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.

Transaction simple

Considérons la transaction la plus simple, du point de vue d'un programmeur SQL appliqué : ajouter une photo à un album. Les albums et les photographies sont stockés dans différentes plaques. L'album dispose d'un compteur de photos public. Ensuite, une telle transaction est divisée en les étapes suivantes :

  1. Nous fermons l'album par clé.
  2. Créez une entrée dans le tableau des photos.
  3. Si la photo a un statut public, ajoutez un compteur de photos publiques à l'album, mettez à jour l'enregistrement et validez la transaction.

Ou en 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 le plus courant pour une transaction commerciale consiste à lire les données de la base de données dans la mémoire du serveur d'applications, à modifier quelque chose et à enregistrer les nouvelles valeurs dans la base de données. Habituellement, dans une telle transaction, nous mettons à jour plusieurs entités, plusieurs tables.

Lors de l'exécution d'une transaction, une modification simultanée des mêmes données à partir d'un autre système peut se produire. Par exemple, Antispam peut décider que l'utilisateur est suspect d'une manière ou d'une autre et que, par conséquent, toutes les photos de l'utilisateur ne doivent plus être publiques, elles doivent être envoyées pour modération, ce qui signifie changer photo.status en une autre valeur et désactiver les compteurs correspondants. Évidemment, si cette opération se produit sans garanties d'atomicité d'application et d'isolement des modifications concurrentes, comme dans ACID, le résultat ne sera pas celui souhaité - soit le compteur de photos affichera une valeur incorrecte, soit toutes les photos ne seront pas envoyées pour modération.

De nombreux codes similaires, manipulant diverses entités commerciales au sein d'une seule transaction, ont été écrits tout au long de l'existence d'Odnoklassniki. Basé sur l'expérience des migrations vers NoSQL depuis Cohérence éventuelle Nous savons que le plus grand défi (et le plus grand investissement en temps) vient du développement de code pour maintenir la cohérence des données. Par conséquent, nous avons considéré que la principale exigence pour le nouveau stockage était de prévoir de véritables transactions ACID pour la logique des applications.

D'autres exigences, non moins importantes, étaient les suivantes :

  • En cas de panne du centre de données, la lecture et l'écriture sur le nouveau stockage doivent être disponibles.
  • Maintenir la vitesse de développement actuelle. Autrement dit, lorsque vous travaillez avec un nouveau référentiel, la quantité de code doit être à peu près la même ; il ne devrait pas être nécessaire d'ajouter quoi que ce soit au référentiel, de développer des algorithmes pour résoudre les conflits, maintenir des index secondaires, etc.
  • La vitesse du nouveau stockage devait être assez élevée, tant lors de la lecture des données que lors du traitement des transactions, ce qui signifiait effectivement que les solutions académiquement rigoureuses, universelles mais lentes, comme par exemple, n'étaient pas applicables commits en deux phases.
  • Mise à l'échelle automatique à la volée.
  • Utiliser des serveurs bon marché réguliers, sans avoir besoin d’acheter du matériel exotique.
  • Possibilité de développement du stockage par les développeurs de l'entreprise. Autrement dit, la priorité a été donnée aux solutions propriétaires ou open source, de préférence en Java.

Ah, prendre des décisions

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

La première consiste à prendre n'importe quel serveur SQL et à mettre en œuvre la tolérance aux pannes, le mécanisme de mise à l'échelle, le cluster de basculement, la résolution des conflits et les transactions ACID distribuées, fiables et rapides. Nous avons évalué cette option comme très simple et exigeante en main-d'œuvre.

La deuxième option consiste à utiliser un stockage NoSQL prêt à l'emploi avec une mise à l'échelle implémentée, un cluster de basculement, une résolution des conflits et à implémenter vous-même les transactions et SQL. À première vue, même la tâche d'implémentation de SQL, sans parler des transactions ACID, semble être une tâche qui prendra des années. Mais nous avons ensuite réalisé que l'ensemble des fonctionnalités SQL que nous utilisons dans la pratique est aussi éloigné du SQL ANSI que Cassandre CQL loin de ANSI SQL. En regardant de plus près CQL, nous avons réalisé qu'il était assez proche de ce dont nous avions besoin.

Cassandra et CQL

Alors, qu'est-ce qui est intéressant à propos de Cassandra, quelles sont ses capacités ?

Tout d'abord, ici vous pouvez créer des tables prenant en charge différents types de données ; vous pouvez effectuer 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 de réplica, Cassandra utilise approche par quorum. Dans le cas le plus simple, cela signifie que lorsque trois répliques d'une même ligne sont placées sur des nœuds différents du cluster, l'écriture est considérée comme réussie si la majorité des nœuds (soit deux sur trois) ont confirmé le succès de cette opération d'écriture. . Les données des lignes sont considérées comme cohérentes si, lors de la lecture, la majorité des nœuds ont été interrogés et confirmés. Ainsi, avec trois réplicas, la cohérence complète et instantanée des données est garantie en cas de panne d'un nœud. Cette approche nous a permis de mettre en œuvre un schéma encore plus fiable : toujours envoyer des requêtes aux trois répliques, en attendant une réponse des deux plus rapides. La réponse tardive de la troisième réplique est ignorée dans ce cas. Un nœud qui tarde à répondre peut avoir de graves problèmes - freins, garbage collection dans la JVM, récupération directe de mémoire dans le noyau Linux, panne matérielle, déconnexion du réseau. Toutefois, cela n’affecte en rien les opérations ou les données du client.

L'approche lorsque nous contactons trois nœuds et recevons une réponse de deux s'appelle spéculation: une demande de répliques supplémentaires est envoyée avant même qu'elle ne « tombe ».

Un autre avantage de Cassandra est Batchlog, un mécanisme qui garantit qu'un lot de modifications que vous apportez est entièrement appliqué ou pas du tout appliqué. Cela nous permet de résoudre A dans ACID - atomicité hors des sentiers battus.

Ce qui se rapproche le plus des transactions dans Cassandra sont ce qu'on appelle «transactions légères". Mais elles sont loin d’être de « vraies » transactions ACID : en fait, c’est une opportunité de faire CAS sur les données d'un seul enregistrement, en utilisant le consensus utilisant le protocole Paxos lourd. Par conséquent, la vitesse de ces transactions est faible.

Ce qui nous manquait dans Cassandra

Nous avons donc dû implémenter de véritables transactions ACID dans Cassandra. Grâce à cela, nous pourrions facilement implémenter deux autres fonctionnalités pratiques du SGBD classique : des index rapides et cohérents, qui nous permettraient d'effectuer des sélections de données non seulement par la clé primaire, et un générateur régulier d'ID monotones à incrémentation automatique.

Cône

Ainsi un nouveau SGBD est né Cône, composé de trois types de nœuds de serveur :

  • Stockage – (presque) serveurs Cassandra standards chargés de stocker les données sur des disques locaux. À mesure que la charge et le volume des données augmentent, leur quantité peut facilement atteindre des dizaines, voire des centaines.
  • Coordonnateurs de transactions - assurent l'exécution des transactions.
  • Les clients sont des serveurs d'applications qui mettent en œuvre des opérations commerciales et lancent des transactions. Il peut y avoir des milliers de ces clients.

NouveauSQL = NoSQL+ACID

Les serveurs de tous types font partie d'un cluster commun, utilisent le protocole de message interne Cassandra pour communiquer entre eux et potins pour échanger des informations sur le cluster. Avec Heartbeat, les serveurs découvrent les pannes mutuelles, maintiennent un schéma de données unique - les tables, leur structure et leur réplication ; schéma de partitionnement, topologie de cluster, etc.

clients

NouveauSQL = NoSQL+ACID

Au lieu des pilotes standard, le mode Fat Client est utilisé. Un tel nœud ne stocke pas de données, mais peut agir en tant que coordinateur de l'exécution des requêtes, c'est-à-dire que le Client lui-même agit en tant que coordinateur de ses requêtes : il interroge les répliques de stockage et résout les conflits. Ceci est non seulement plus fiable et plus rapide que le pilote standard, qui nécessite une communication avec un coordinateur à distance, mais permet également de contrôler la transmission des requêtes. En dehors d'une transaction ouverte sur le client, les requêtes sont envoyées aux référentiels. Si le client a ouvert une transaction, toutes les demandes contenues dans la transaction sont envoyées au coordinateur de transaction.
NouveauSQL = NoSQL+ACID

Coordonnateur des transactions C*One

Le coordinateur est quelque chose que nous avons implémenté pour C*One à partir de zéro. Il est responsable de la gestion des transactions, des verrous et de l'ordre dans lequel les transactions sont appliquées.

Pour chaque transaction desservie, le coordinateur génère un horodatage : chaque transaction suivante est supérieure à la transaction précédente. Puisque le système de résolution des conflits de Cassandra est basé sur des horodatages (sur deux enregistrements en conflit, celui avec le dernier horodatage est considéré comme actuel), le conflit sera toujours résolu en faveur de la transaction suivante. Nous avons ainsi mis en œuvre Montre Lamport - un moyen peu coûteux de résoudre les conflits dans un système distribué.

Serrures

Pour garantir l'isolement, 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, lors d’une transaction, un enregistrement doit d’abord être verrouillé, puis lu, modifié et sauvegardé. Ce n'est qu'après une validation réussie qu'un 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 options principales : soit implémenter un verrouillage distribué sur le cluster, soit distribuer les transactions de manière à ce que les transactions impliquant le même enregistrement soient toujours gérées par le même coordinateur.

Puisque dans notre cas, les données sont déjà réparties entre des groupes de transactions locales en SQL, il a été décidé d'attribuer des groupes de transactions locales à des coordinateurs : un coordinateur effectue toutes les transactions avec des jetons de 0 à 9, le second - avec des jetons de 10 à 19, et ainsi de suite. En conséquence, chacune des instances du coordinateur devient le maître du groupe de transactions.

Ensuite, les verrous peuvent être implémentés sous la forme d'un banal HashMap dans la mémoire du coordinateur.

Échecs du coordinateur

Puisqu'un coordinateur dessert exclusivement un groupe de transactions, il est très important de déterminer rapidement le fait de son échec afin que la deuxième tentative d'exécution de la transaction expire. Pour rendre cela rapide et fiable, nous avons utilisé un protocole de quorum heartbeat entièrement connecté :

Chaque centre de données héberge au moins deux nœuds coordinateurs. Périodiquement, chaque coordinateur envoie un message de battement de cœur aux autres coordinateurs et les informe de son fonctionnement, ainsi que des messages de battement de cœur qu'il a reçus de quels coordinateurs du cluster la dernière fois.

NouveauSQL = NoSQL+ACID

Recevant des informations similaires des autres dans le cadre de leurs messages de battement de cœur, chaque coordinateur décide lui-même quels nœuds du cluster fonctionnent et lesquels ne le sont pas, guidé par le principe du quorum : si le nœud X a reçu des informations de la majorité des nœuds du cluster sur le fonctionnement normal. réception des messages du nœud Y, alors, Y fonctionne. Et vice versa, dès que la majorité signale des messages manquants du nœud Y, alors Y a refusé. Il est curieux que si le quorum informe le nœud X qu'il ne reçoit plus de messages de sa part, alors le nœud X lui-même se considérera comme ayant échoué.

Les messages Heartbeat sont envoyés à haute fréquence, environ 20 fois par seconde, avec une période de 50 ms. En Java, il est difficile de garantir une réponse de l'application dans un délai de 50 ms en raison de la durée comparable des pauses provoquées par le garbage collector. Nous avons pu atteindre ce temps de réponse en utilisant le garbage collector G1, qui nous permet de spécifier un objectif pour la durée des pauses GC. Cependant, parfois, assez rarement, les pauses du collecteur dépassent 50 ms, ce qui peut conduire à une fausse détection de défaut. Pour éviter que cela ne se produise, le coordinateur ne signale pas une panne d'un nœud distant lorsque le premier message de battement de coeur de celui-ci disparaît, seulement si plusieurs ont disparu d'affilée. C'est ainsi que nous avons réussi à détecter une panne du nœud coordinateur en 200 MS.

Mais il ne suffit pas de comprendre rapidement quel nœud a cessé de fonctionner. Nous devons faire quelque chose à ce sujet.

Réservation

Le schéma classique consiste, en cas d'échec du maître, à déclencher une nouvelle élection en utilisant l'un des à la mode universel algorithmes. Cependant, ces algorithmes présentent des problèmes bien connus liés à la convergence temporelle et à la durée du processus électoral lui-même. Nous avons pu éviter de tels retards supplémentaires en utilisant un système de remplacement du coordinateur dans un réseau entièrement connecté :

NouveauSQL = NoSQL+ACID

Disons que nous voulons exécuter 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 dans le groupe 50 en cas de défaillance du coordinateur principal. Notre objectif est de maintenir la fonctionnalité du système en cas de panne du centre de données. Déterminons que la première réserve sera un nœud d'un autre centre de données et que la deuxième réserve sera un nœud d'un troisième. Ce schéma est sélectionné une fois et ne change pas jusqu'à ce que la topologie du cluster change, c'est-à-dire jusqu'à ce que de nouveaux nœuds y entrent (ce qui arrive très rarement). La procédure de sélection d'un nouveau maître actif en cas de panne de l'ancien sera toujours la suivante : la première réserve deviendra le maître actif, et si elle a cessé de fonctionner, la deuxième réserve deviendra le maître actif.

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

Mais comment les clients comprendront-ils quel maître travaille actuellement ? Il est impossible d'envoyer des informations à des milliers de clients en 50 ms. Une situation est possible lorsqu'un client envoie une demande d'ouverture d'une transaction, sans savoir encore que ce maître ne fonctionne plus, et que la demande expirera. Pour éviter que cela ne se produise, les clients envoient de manière spéculative une demande d'ouverture de transaction au maître du groupe et à ses deux réserves à la fois, mais seul celui qui est actuellement le maître actif répondra à cette demande. Le client effectuera toutes les communications ultérieures dans le cadre de la transaction uniquement avec le maître actif.

Les maîtres de sauvegarde placent les demandes reçues pour des transactions qui ne leur appartiennent pas dans la file d'attente des transactions à naître, où elles sont stockées pendant un certain temps. Si le maître actif meurt, le 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à ouvert une transaction avec l'ancien maître, alors la deuxième réponse est ignorée (et, évidemment, une telle transaction ne se terminera pas et sera répétée par le client).

Comment fonctionne la transaction

Disons qu'un client envoie une requête au coordinateur pour ouvrir une transaction pour telle ou telle entité avec telle ou telle clé primaire. Le coordinateur verrouille cette entité et la place dans la table de verrouillage en mémoire. Si nécessaire, le coordinateur lit cette entité depuis le stockage et stocke les données résultantes dans un état de transaction dans la mémoire du coordinateur.

NouveauSQL = NoSQL+ACID

Lorsqu'un client souhaite modifier les données d'une transaction, il envoie une demande au coordinateur pour modifier l'entité, et le coordinateur place les nouvelles données dans la table d'état de la transaction en mémoire. Ceci termine l'enregistrement - aucun enregistrement n'est effectué sur le stockage.

NouveauSQL = NoSQL+ACID

Lorsqu'un 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à dans la transaction, alors les données sont extraites de la mémoire ;
  • s'il n'y a pas d'ID en mémoire, alors les données manquantes sont lues sur les nœuds de stockage, combinées avec celles déjà en mémoire, et le résultat est donné au client.

Ainsi, le client peut lire ses propres modifications, mais les autres clients ne voient pas ces modifications, car elles sont stockées uniquement dans la mémoire du coordinateur ; elles ne sont pas encore dans les nœuds Cassandra.

NouveauSQL = NoSQL+ACID

Lorsque le client envoie une validation, l'état qui se trouvait dans la mémoire du service est enregistré par le coordinateur dans un lot enregistré et est envoyé sous forme de lot enregistré au stockage Cassandra. Les magasins font tout le nécessaire pour que ce package soit appliqué de manière atomique (complète) et renvoient une réponse au coordinateur, qui libère les verrous et confirme le succès de la transaction au client.

NouveauSQL = NoSQL+ACID

Et pour restaurer, le coordinateur n'a qu'à libérer la mémoire occupée par l'état de la transaction.

Grâce aux améliorations ci-dessus, nous avons mis en œuvre les principes ACID :

  • Atomicité. C'est une garantie qu'aucune transaction ne sera partiellement enregistrée dans le système : soit toutes ses sous-opérations seront terminées, soit aucune ne sera terminée. Nous adhérons à ce principe via des lots enregistrés dans Cassandra.
  • Cohérence. Chaque transaction réussie, par définition, n'enregistre que des résultats valides. Si, après avoir ouvert une transaction et effectué une partie des opérations, il s'avère que le résultat n'est pas valide, une restauration est effectuée.
  • Isolement. Lorsqu'une transaction est exécutée, les transactions simultanées ne devraient pas affecter son résultat. Les transactions concurrentes sont isolées à l'aide de verrous pessimistes sur le coordinateur. Pour les lectures hors transaction, le principe d’isolement est observé au niveau Read Comended.
  • Stabilité. Quels que soient les problèmes aux niveaux inférieurs (panne du système, panne matérielle), les modifications apportées par une transaction terminée avec succès doivent rester préservées lors de la reprise des opérations.

Lecture par index

Prenons un tableau simple :

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

Il possède un identifiant (clé primaire), un propriétaire et une date de modification. Vous devez faire une demande très simple - sélectionnez les données sur le propriétaire avec la date de changement « pour le dernier jour ».

SELECT *
WHERE owner=?
AND modified>?

Pour qu'une telle requête soit traitée rapidement, dans un SGBD SQL classique il faut construire un index par colonnes (propriétaire, modifié). Nous pouvons le faire assez facilement, puisque nous bénéficions désormais des garanties ACID !

Index dans C*One

Il existe une table source avec des photographies dans laquelle l'ID d'enregistrement est la clé primaire.

NouveauSQL = NoSQL+ACID

Pour un index, C*One crée une nouvelle table qui est une copie de l'original. La clé est la même que l'expression d'index et inclut également la clé primaire de l'enregistrement de la table source :

NouveauSQL = NoSQL+ACID

Désormais, la requête « propriétaire du dernier jour » peut être réécrite sous forme de sélection dans une autre table :

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

La cohérence des données dans la table source photos et la table d'index i1 est maintenue automatiquement par le coordinateur. Sur la base du seul schéma de données, lorsqu'une modification est reçue, le coordinateur génère et stocke une modification non seulement dans la table principale, mais également dans des copies. Aucune action supplémentaire n'est effectuée sur la table d'index, les journaux ne sont pas lus et aucun verrou n'est utilisé. Autrement dit, l'ajout d'index ne consomme pratiquement aucune ressource et n'a pratiquement aucun effet sur la vitesse d'application des modifications.

Grâce à ACID, nous avons pu implémenter des index de type SQL. Ils sont cohérents, évolutifs, rapides, composables et intégrés au langage de requête CQL. Aucune modification du code de l'application n'est requise pour prendre en charge les index. Tout est aussi simple qu'en SQL. Et surtout, les index n’affectent pas la vitesse d’exécution des modifications apportées à la table de transactions d’origine.

Qu'est-il arrivé

Nous avons développé C*One il y a trois ans et l'avons lancé en exploitation commerciale.

Qu’avons-nous obtenu au final ? Évaluons cela en utilisant l'exemple du sous-système de traitement et de stockage des photos, l'un des types de données les plus importants dans un réseau social. Nous ne parlons pas des corps des photographies eux-mêmes, mais de toutes sortes de méta-informations. Aujourd'hui, Odnoklassniki compte environ 20 milliards d'enregistrements de ce type, le système traite 80 8 demandes de lecture par seconde, jusqu'à XNUMX XNUMX transactions ACID par seconde associées à la modification des données.

Lorsque nous utilisions SQL avec un facteur de réplication = 1 (mais en RAID 10), les métainformations des photos étaient stockées sur un cluster hautement disponible de 32 machines exécutant Microsoft SQL Server (plus 11 sauvegardes). 10 serveurs ont également été alloués au stockage des sauvegardes. Un total de 50 voitures chères. Dans le même temps, le système fonctionnait à charge nominale, sans réserve.

Après la migration vers le nouveau système, nous avons reçu 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 coordinatrices, pour un total de 69 serveurs. Mais ces machines sont bien moins chères, leur coût total est d'environ 30 % du coût d'un système SQL. Dans le même temps, la charge est maintenue à 30 %.

Avec l'introduction de C*One, la latence a également diminué : en SQL, une opération d'écriture prenait environ 4,5 ms. En C*One – environ 1,6 ms. La durée de la transaction est en moyenne inférieure à 40 ms, le commit est réalisé en 2 ms, la durée de lecture et d'écriture est en moyenne de 2 ms. 99e centile - seulement 3 à 3,1 ms, le nombre de délais d'attente a diminué de 100 fois - tout cela grâce à l'utilisation généralisée de la spéculation.

À l'heure actuelle, la plupart des nœuds SQL Server ont été mis hors service et les nouveaux produits sont développés uniquement à l'aide de C*One. Nous avons adapté C*One pour fonctionner dans notre cloud un seul cloud, qui a permis d'accélérer le déploiement de nouveaux clusters, de simplifier la configuration et d'automatiser le fonctionnement. Sans le code source, cela serait beaucoup plus difficile et fastidieux.

Nous travaillons actuellement au transfert de nos autres installations de stockage vers le cloud, mais c'est une toute autre histoire.

Source: habr.com

Ajouter un commentaire