Guide de survie MongoDB

Toutes les bonnes startups meurent rapidement ou se développent à l'échelle. Nous modéliserons une telle startup, qui concerne d'abord les fonctionnalités, puis les performances. Nous allons améliorer les performances avec MongoDB, une solution de stockage de données NoSQL populaire. MongoDB est facile à démarrer et de nombreux problèmes ont des solutions prêtes à l'emploi. Cependant, lorsque la charge augmente, un râteau sort que personne ne vous avait prévenu avant ... jusqu'à aujourd'hui!

image

La modélisation est effectuée par Sergey Zagursky , responsable de l'infrastructure backend en général, et MongoDB en particulier, dans Joom . Il a également été vu dans le côté serveur du développement du MMORPG Skyforge. Comme Sergei le décrit lui-même, il est «un preneur de cônes professionnel avec son propre front et son propre râteau». Au microscope, un projet qui utilise une stratégie d'accumulation pour gérer la dette technique. Dans cette version texte du rapport en HighLoad ++, nous allons passer par ordre chronologique de l'occurrence du problème à la solution à l'aide de MongoDB.

Premières difficultés


Nous modélisons une startup qui bourre les bosses. La première étape de la vie - les fonctionnalités sont lancées dans notre startup et, de manière inattendue, les utilisateurs viennent. Notre petit-petit serveur MongoDB a une charge dont nous n'avons jamais rêvé. Mais nous sommes dans le cloud, nous sommes une startup! Nous faisons les choses les plus simples possibles: regardez les demandes - oh, et ici nous avons soustrait la correction entière pour chaque utilisateur, ici nous construirons les indices, nous y ajouterons le matériel, et ici nous mettrons en cache.
Tout - nous vivons!

Si des problèmes peuvent être résolus par des moyens aussi simples, ils doivent être résolus de cette manière.

Mais la voie future d'un démarrage réussi est un retard lent et douloureux du moment de mise à l'échelle horizontale. Je vais essayer de donner des conseils sur la façon de survivre à cette période, de passer à l'échelle et de ne pas marcher sur le râteau.

Enregistrement lent


C'est l'un des problèmes que vous pouvez rencontrer. Que faire si vous la rencontrez et que les méthodes ci-dessus ne vous aident pas? Réponse: mode de garantie de durabilité dans MongoDB par défaut . En trois mots, cela fonctionne comme ceci:

  • Nous sommes arrivés à la ligne principale et avons dit: "Écrivez!".
  • Réplique primaire enregistrée.
  • Après cela, des répliques secondaires ont été lues d'elle et ils ont dit primaire: "Nous avons enregistré!"

Au moment où la plupart des répliques secondaires l'ont fait, la demande est considérée comme terminée et le contrôle revient au pilote dans l'application. De telles garanties nous permettent d'être sûrs que lorsque le contrôle est revenu à l'application, la durabilité n'ira nulle part, même si MongoDB se couche, à l'exception de catastrophes absolument terribles.

Heureusement, MongoDB est une telle base de données qui vous permet de réduire les garanties de durabilité pour chaque demande individuelle.

Pour les demandes importantes, nous pouvons laisser les garanties de durabilité maximale par défaut, et pour certaines demandes, nous pouvons les réduire.

Classes de demande


La première couche de garanties que nous pouvons supprimer est de ne pas attendre la confirmation de l'enregistrement par la plupart des répliques . Cela économise la latence, mais n'ajoute pas de bande passante. Mais parfois, la latence est ce dont vous avez besoin, surtout si le cluster est un peu surchargé et que les répliques secondaires ne fonctionnent pas aussi vite que nous le souhaiterions.

{w:1, j:true} 

Si nous écrivons des enregistrements avec de telles garanties, alors au moment où nous obtenons le contrôle dans l'application, nous ne savons plus si l'enregistrement sera vivant après une sorte d'accident. Mais généralement, elle est toujours en vie.

La prochaine garantie, qui affecte également la bande passante et la latence, est de désactiver la confirmation de journalisation . Une entrée de journal est quand même écrite. Le magazine est l'un des mécanismes fondamentaux. Si nous désactivons la confirmation de l'écriture, nous ne faisons pas deux choses: fsync sur le journal et n'attendons pas qu'il se termine . Cela peut économiser beaucoup de ressources de disque et obtenir une augmentation multiple du débit en changeant simplement la durabilité de la garantie.

 {w:1, j:false} 

Les garanties de durabilité les plus strictes désactivent toute reconnaissance . Nous ne recevrons que la confirmation que la demande a atteint la réplique principale. Cela permettra d'économiser la latence et n'augmentera en aucun cas le débit.

 {w:0, j:false} —   . 

Nous recevrons également diverses autres choses, par exemple, l'enregistrement a échoué en raison d'un conflit avec une clé unique.

À quelles opérations cela s'applique-t-il?


Je vais vous parler de l'application à la configuration dans Joom. En plus de la charge des utilisateurs, dans laquelle il n'y a pas de concessions de durabilité, il y a une charge qui peut être décrite comme une charge de lot en arrière-plan: mise à jour, recomptage des évaluations, collecte de données analytiques.

Ces opérations d'arrière-plan peuvent prendre des heures, mais sont conçues de telle sorte que si une interruption, par exemple, un backend se bloque, elles ne perdront pas le résultat de tout leur travail, mais reprendront à partir du point dans le passé récent. La réduction de la garantie de durabilité est utile pour de telles tâches, d'autant plus que fsync dans le journal, comme toutes les autres opérations, augmentera également la latence pour la lecture.

Lire l'échelle


Le problème suivant est une bande passante de lecture insuffisante . Rappelez-vous que dans notre cluster, il n'y a pas seulement des répliques primaires, mais aussi des répliques secondaires à partir desquelles vous pouvez lire . Faisons-le.

Vous pouvez lire, mais il y a des nuances. Les données légèrement obsolètes proviendront de répliques secondaires - de 0,5 à 1 seconde. Dans la plupart des cas, cela est normal, mais le comportement de la réplique secondaire est différent de celui des répliques principales.

Sur le secondaire, il y a le processus d'utilisation d'oplog, qui n'est pas sur le réplica principal. Ce processus n'est pas conçu pour une faible latence - seuls les développeurs MongoDB ne se sont pas souciés de cela. Dans certaines conditions, le processus d'utilisation de l'oplog du primaire au secondaire peut entraîner des retards allant jusqu'à 10 s.

Les répliques secondaires ne conviennent pas aux requêtes des utilisateurs - les expériences des utilisateurs font un pas rapide dans le bac.

Sur les grappes non ombrées, ces pics sont moins visibles, mais toujours là. Les clusters d'éclat souffrent car oplog est particulièrement affecté par la suppression, et la suppression fait partie du travail de l'équilibreur . L'équilibreur supprime de manière fiable et avec goût des documents par dizaines de milliers en peu de temps.

Nombre de connexions


Le prochain facteur à considérer est la limite du nombre de connexions sur les instances MongoDB . Par défaut, il n'y a pas de restrictions, à l' exception des ressources du système d'exploitation - vous pouvez vous connecter pendant que cela le permet.

Cependant, plus les demandes simultanées sont simultanées, plus elles s'exécutent lentement. Les performances se dégradent de façon non linéaire . Par conséquent, si un pic de demandes nous parvient, il vaut mieux servir 80% que de ne pas servir 100%. Le nombre de connexions doit être limité directement à MongoDB.

Mais il y a des bugs qui peuvent causer des problèmes à cause de cela. En particulier, le pool de connexions côté MongoDB est commun aux connexions intracluster utilisateur et service . Si l'application "a mangé" toutes les connexions de ce pool, l'intégrité peut être violée dans le cluster.

Nous l'avons appris lorsque nous allions reconstruire l'index, et comme nous devions supprimer l'unicité de l'index, la procédure a traversé plusieurs étapes. Dans MongoDB, vous ne pouvez pas créer le même à côté de l'index, mais sans l'unicité. Par conséquent, nous voulions:

  • Construire un index similaire sans unicité
  • supprimer l'index avec unicité;
  • Construisez un index sans unicité au lieu de distant;
  • supprimer temporairement.

Lorsque l'index temporaire était encore en cours de finalisation sur le secondaire, nous avons commencé à supprimer l'index unique. À ce stade, le MongoDB secondaire a annoncé son verrouillage. Certaines métadonnées ont été bloquées et, dans la plupart des cas, tous les enregistrements se sont arrêtés: ils se sont accrochés dans le pool de connexions et ont attendu qu'ils confirment que l'enregistrement était passé. Toutes les lectures sur le secondaire se sont également arrêtées car le journal global a été capturé.

Le cluster dans un état aussi intéressant a également perdu sa connectivité. Parfois, il est apparu et lorsque deux remarques se sont connectées, ils ont essayé de faire un choix dans leur état qu'ils ne pouvaient pas faire, car ils ont un verrou global.

Morale de l'histoire: le nombre de connexions doit être surveillé.

Il y a un râteau MongoDB bien connu, qui est encore si souvent attaqué que j'ai décidé de faire une courte promenade dessus.

Ne perdez pas de documents


Si vous envoyez une demande par index à MongoDB, la demande peut ne pas retourner tous les documents qui remplissent la condition, et dans des cas complètement inattendus. Cela est dû au fait que lorsque nous allons au début de l'index, le document, qui à la fin, se déplace vers le début pour les documents que nous avons passés. Cela est uniquement dû à la mutabilité de l'indice . Pour une itération fiable, utilisez des index sur des champs non stables et il n'y aura pas de difficultés.
MongoDB a ses propres vues sur les index à utiliser. La solution est simple - avec l'aide de $ hint, nous forçons MongoDB à utiliser l'index que nous avons spécifié .

Tailles de collection


Notre startup se développe, il y a beaucoup de données, mais je ne veux pas ajouter de disques - nous en avons déjà ajouté trois fois le mois dernier. Voyons ce qui est stocké dans nos données, regardons la taille des documents. Comment comprendre où dans la collection vous pouvez réduire la taille? Selon deux paramètres.

  • La taille des documents spécifiques pour jouer avec leur longueur: Object.bsonsize() ;
  • Selon la taille moyenne du document dans la collection : db.c.stats().avgObjectSize .

Comment affecter la taille du document?


J'ai des réponses non spécifiques à cette question. Tout d'abord, un nom de champ long augmente la taille du document. Dans chaque document, tous les noms de champ sont copiés, donc si le document a un nom de champ long, la taille du nom doit être ajoutée à la taille de chaque document. Si vous avez une collection avec un grand nombre de petits documents sur plusieurs champs, alors nommez les champs avec des noms courts: "A", "B", "CD" - un maximum de deux lettres. Sur le disque, cela est compensé par la compression , mais tout est stocké dans le cache tel quel.

La deuxième astuce est que parfois certains champs de faible cardinalité peuvent être placés au nom de la collection . Par exemple, un tel champ peut être une langue. Si nous avons une collection avec des traductions en russe, anglais, français et un champ avec des informations sur la langue stockée, la valeur de ce champ peut être mise dans le nom de la collection. Nous allons donc réduire la taille des documents et pouvons réduire le nombre et la taille des index - de simples économies! Cela ne peut pas toujours être fait, car il existe parfois des index dans le document qui ne fonctionneront pas si la collection est divisée en différentes collections.

Dernier conseil sur la taille du document - utilisez le champ _id . Si vos données ont une clé unique naturelle, mettez-la directement dans le champ id_field. Même si la clé est composite - utilisez un identifiant composite. Il est parfaitement indexé. Il n'y a qu'un petit râteau - si votre marshaller change parfois l'ordre des champs, alors id avec les mêmes valeurs de champ, mais avec un ordre différent sera considéré comme un id différent en termes d'index unique dans MongoDB. Dans certains cas, cela peut se produire dans Go.

Tailles d'index


L'index stocke une copie des champs qui y sont inclus . La taille de l'index est constituée des données indexées. Si nous essayons d'indexer de grands champs, préparez-vous à ce que la taille de l'index soit grande.

Le deuxième moment gonfle fortement les index: les champs de tableau dans l'index multiplient les autres champs du document dans cet index . Soyez prudent avec les grands tableaux dans les documents: ne pas indexer autre chose sur le tableau, ou jouer avec l'ordre dans lequel les champs de l'index sont répertoriés.

L'ordre des champs est important , surtout si l'un des champs d'index est un tableau . Si les champs diffèrent en cardinalité, et dans un champ le nombre de valeurs possibles est très différent du nombre de valeurs possibles dans un autre, alors il est logique de les construire en augmentant la cardinalité. Vous pouvez facilement économiser 50% de la taille de l'index si vous échangez des champs avec une cardinalité différente. La permutation des champs peut donner une réduction de taille plus importante.

Parfois, lorsque le champ contient une grande valeur, nous n'avons pas besoin de comparer plus ou moins cette valeur, mais plutôt une comparaison claire de l'égalité. Ensuite, l' index sur le champ avec un contenu lourd peut être remplacé par l'index sur le hachage de ce champ . Des copies de hachage seront stockées dans l'index, pas des copies de ces champs.

Supprimer des documents


J'ai déjà mentionné que la suppression de documents est une opération désagréable et il vaut mieux ne pas les supprimer si possible. Lors de la conception d'un schéma de données, essayez d'envisager de minimiser la suppression de données individuelles ou de supprimer des collections entières. ils pourraient être supprimés avec des collections entières. La suppression de collections est une opération bon marché et la suppression de milliers de documents individuels est une opération difficile.

Si vous avez encore besoin de supprimer un grand nombre de documents, assurez-vous d' effectuer une limitation , sinon la suppression en masse des documents affectera la latence de lecture et sera désagréable. C'est particulièrement mauvais pour la latence sur le secondaire.

Cela vaut la peine de faire une sorte de «stylo» pour tourner la limitation - il est très difficile de relever le niveau la première fois. Nous l'avons traversé tellement de fois que la limitation est devinée à partir de la troisième, quatrième fois. Dans un premier temps, envisagez la possibilité de le resserrer.

Si vous supprimez plus de 30% d'une grande collection, transférez des documents actifs vers la collection voisine et supprimez l'ancienne collection dans son ensemble. Il est clair qu'il y a des nuances, car la charge est commutée de l'ancienne à la nouvelle collection, mais changez si possible.

Une autre façon de supprimer des documents est l'index TTL , qui est un index qui indexe le champ qui contient l'horodatage Mongo, qui contient la date à laquelle le document est mort. Le moment venu, MongoDB supprimera automatiquement ce document.

L'index TTL est pratique, mais il n'y a pas de limitation dans l'implémentation. MongoDB ne se soucie pas de la façon de supprimer ces suppressions. Si vous essayez de supprimer un million de documents en même temps, vous aurez pendant quelques minutes un cluster inutilisable qui ne traite que de la suppression et rien de plus. Pour éviter que cela ne se produise, ajoutez un caractère aléatoire , répartissez le TTL autant que votre logique métier et les effets spéciaux sur la latence le permettent. Il est impératif d'étaler TTL si vous avez des raisons logiques commerciales naturelles qui concentrent la suppression à un moment donné.

Partage


Nous avons essayé de reporter ce moment, mais il est venu - nous devons encore évoluer horizontalement. Pour MongoDB, c'est un partage.

Si vous doutez que vous avez besoin de partage, vous n'en avez pas besoin.

Le sharding complique la vie d'un développeur et se déroule de différentes manières. Dans une entreprise, nous l'appelons taxe de partage. Lorsque nous partitionnons une collection, les performances spécifiques de la collection diminuent : MongoDB nécessite un index séparé pour le partitionnement et des paramètres supplémentaires doivent être transmis à la demande afin de pouvoir être exécuté plus efficacement.

Certaines choses tranchantes ne fonctionnent tout simplement pas bien. Par exemple, c'est une mauvaise idée d'utiliser des requêtes avec skip , surtout si vous avez beaucoup de documents. Vous donnez la commande: «Ignorer 100 000 documents».

MongoDB pense de cette façon: «D'abord, deuxième, troisième ... cent millième, allons plus loin. Et nous le rendrons à l'utilisateur. »

Dans une collection non partagée, MongoDB effectuera une opération quelque part en lui-même. En forme de tesson - elle lit vraiment et envoie tous les 100 000 documents à un proxy de partitionnement - en mongos , qui déjà de son côté filtreront et élimineront les 100 000 premiers. Une caractéristique désagréable à garder à l'esprit.

Le code deviendra certainement plus compliqué avec le partitionnement - vous devrez faire glisser la clé de partitionnement à de nombreux endroits. Ce n'est pas toujours pratique et pas toujours possible. Certaines requêtes iront soit en diffusion soit en multidiffusion, ce qui n'ajoute pas non plus l'évolutivité. Venez au choix d'une clé par laquelle le sharding sera plus précis.

Dans les collections de fragments, l'opération de count interrompue . Elle commence à rendre un numéro de plus qu'en réalité - elle peut mentir 2 fois. La raison réside dans le processus d'équilibrage, lorsque les documents sont versés d'un fragment à l'autre. Lorsque les documents ont été versés sur le fragment voisin, mais n'ont pas encore été supprimés sur celui d'origine, le count quand même. Les développeurs de MongoDB n'appellent pas cela un bug - c'est une telle fonctionnalité. Je ne sais pas s'ils vont le réparer ou non.

Un cluster mélangé est beaucoup plus difficile à administrer . Devops cessera de vous accueillir, car le processus de suppression d'une sauvegarde devient radicalement plus compliqué. Lors du partage, le besoin d'automatisation de l'infrastructure clignote comme une alarme incendie - quelque chose que vous auriez pu faire sans auparavant.

Fonctionnement du partage dans MongoDB


Il y a une collection, nous voulons en quelque sorte la disperser autour des éclats. Pour ce faire, MongoDB divise la collection en morceaux à l'aide de la clé de partition , en essayant de les diviser en morceaux égaux dans l'espace de clé de partition. Vient ensuite l'équilibreur, qui présente avec diligence ces morceaux en fonction des fragments du cluster . De plus, l'équilibreur ne se soucie pas du poids de ces morceaux et du nombre de documents qu'ils contiennent, car l'équilibrage se fait pièce par pièce.

Clé de partitionnement


Décidez-vous toujours quoi tailler? Eh bien, la première question est de savoir comment choisir une clé de partitionnement. Une bonne clé a plusieurs paramètres: cardinalité élevée , non-stabilité et elle s'adapte bien aux demandes fréquentes .

Le choix naturel d'une clé de partitionnement est la clé primaire - le champ id. Si le champ id convient pour le partitionnement, il est préférable de le scinder directement dessus. C'est un excellent choix - il a une bonne cardinalité, il n'est pas stable, mais sa capacité à répondre aux demandes fréquentes est la spécificité de votre entreprise. Tirez parti de votre situation.

Je vais donner un exemple d'une clé de partitionnement échouée. J'ai déjà mentionné la collection de traductions - traductions. Il a un champ de langue qui stocke la langue. Par exemple, la collection prend en charge 100 langues et nous partageons la langue. C'est mauvais - cardinalité, le nombre de valeurs possibles n'est que de 100 pièces, ce qui est petit. Mais ce n'est pas le pire - la cardinalité suffit peut-être à ces fins. Pire, dès que nous avons parcouru la langue, nous découvrons immédiatement que nous avons 3 fois plus d'utilisateurs anglophones que les autres. Trois fois plus de demandes parviennent au fragment malheureux dans lequel se trouve l'anglais qu'à toutes les autres combinées.

Par conséquent, il convient de garder à l'esprit que parfois une clé d'éclat tend naturellement vers une répartition de charge inégale.

Équilibrage


Nous en venons au partage lorsque le besoin a mûri pour nous - notre cluster MongoDB craque, craque avec ses disques, son processeur - avec tout ce que nous pouvons. Où aller? Nulle part, et nous mélangeons héroïquement les talons des collections. Nous scindons, lançons et découvrons soudain que l' équilibrage n'est pas gratuit .

L'équilibrage passe par plusieurs étapes. L'équilibreur choisit des morceaux et des éclats, d'où et où il sera transféré. Le travail se déroule en deux phases: tout d'abord, les documents sont copiés de la source vers la cible, puis les documents qui ont été copiés sont supprimés .

Notre éclat est surchargé, il contient toutes les collections, mais la première partie de l'opération lui est facile. Mais le second - le retrait - est assez désagréable, car il mettra un éclat sur les omoplates et souffrira déjà sous charge.

Le problème est aggravé par le fait que si nous équilibrons beaucoup de morceaux, par exemple des milliers, puis avec les paramètres par défaut, tous ces morceaux sont d'abord copiés, puis un dissolvant entre et commence à les supprimer en bloc. À ce stade, la procédure n'est plus affectée et vous n'avez qu'à regarder tristement ce qui se passe.

Par conséquent, si vous approchez de l'éclatement d'un cluster surchargé, vous devez planifier, car l' équilibrage prend du temps. Il est conseillé de prendre ce temps non pas en prime time, mais en période de faible charge. Balancer - une pièce détachée déconnectée. Vous pouvez aborder l'équilibrage principal en mode manuel, éteindre l'équilibreur en prime time et l'activer lorsque la charge a diminué pour vous permettre davantage.

Si les capacités du cloud vous permettent toujours de vous adapter verticalement, il est préférable d’améliorer à l’avance la source des fragments afin de réduire légèrement tous ces effets spéciaux.

Le sharding doit être soigneusement préparé.

HighLoad ++ Siberia 2019 arrivera à Novossibirsk les 24 et 25 juin. HighLoad ++ Siberia est une opportunité pour les développeurs de Sibérie d'écouter des rapports, de parler de sujets très chargés et de plonger dans l'environnement "où tout le monde a le sien", sans avoir à parcourir plus de trois mille kilomètres à Moscou ou à Saint-Pétersbourg. Sur les 80 demandes, le Comité du programme en a approuvé 25 et nous parlons de tous les autres changements dans le programme, des annonces de rapports et d'autres nouvelles dans notre liste de diffusion. Abonnez-vous pour rester informé.

Source: https://habr.com/ru/post/fr454748/


All Articles