Modification du schéma des tables PostgreSQL sans longs verrous. Conférence Yandex

Si en même temps de nombreuses opérations sont effectuées pour modifier le schéma de la base de données, le service ne peut pas fonctionner correctement lors de l'enregistrement. Le développeur Vladimir Kolyasinsky a expliqué quelles opérations dans PostgreSQL nécessitent des verrous à long terme et comment l'équipe Yandex.Connect fournit un accès en écriture à presque 100% au service pendant de telles opérations. De plus, vous découvrirez la bibliothèque de Django, conçue pour automatiser une partie des processus décrits.


Nous avons des charges lourdes, des milliers de RPS et des temps d'arrêt en quelques minutes, sans parler de plus de temps, sont inacceptables. Il est nécessaire que les migrations se passent inaperçues par l'utilisateur. Et avec de telles charges, il ne sera pas possible de se lever à quatre heures du matin, de rouler quelque chose quand il n'y a pas de charge et de se recoucher - parce que la charge circule 24 heures sur 24.

- Bonsoir à tous! Je m'appelle Vladimir, je travaille chez Yandex depuis cinq ans. Au cours des deux dernières années, j'ai développé des services internes et des services pour les organisations.

Un peu sur ce que ces services sont pour les organisations. Depuis un certain temps, nous utilisons un grand nombre de services internes: un wiki pour stocker et échanger des données, un messager pour une communication rapide avec des collègues, un tracker pour organiser le processus de travail, des formulaires pour mener des enquêtes à l'intérieur et à l'extérieur, et de nombreux autres services.

Il y a quelque temps, nous avons décidé que nos services sont sympas et qu'ils peuvent être utiles non seulement à l'intérieur de Yandex, mais aussi à des personnes à l'extérieur. Nous avons commencé à les intégrer à une plate-forme unifiée Yandex.Connect, en y ajoutant des services externes existants, tels que Mail pour un domaine.



Je développe actuellement le Form Designer et le Wiki. La pile utilisée est principalement des services écrits en Python des deuxième et troisième versions; Django 1.9-1.11. En tant que base de données, la plupart est PostgreSQL. C'est aussi du céleri avec MongoDB et SQS comme courtiers. Tout cela fonctionne dans Docker.

Passons au problème auquel nous sommes confrontés. Les services sont populaires, ils sont utilisés par des centaines de milliers de personnes chaque jour, les données s'accumulent, les tables sont de plus en plus nombreuses et, au fil du temps, de nombreuses opérations de modification des schémas de base de données, qui ont été effectuées inaperçues par les utilisateurs hier, commencent à entraver le fonctionnement normal des services.

Aujourd'hui, nous allons parler de la façon dont nous gérons ces situations et de la manière dont nous atteignons une haute disponibilité des services de lecture et d'écriture.

Tout d'abord, considérons quelles opérations avec PostgreSQL nécessitent de longs verrous sur la table. Par verrouillage, j'entends tout type de verrou qui interfère avec le fonctionnement normal de la table - que ce soit l'accès exclusif, qui interfère avec l'écriture et la lecture, ou des niveaux de verrouillage plus faibles qui empêchent uniquement l'écriture.

Ensuite, nous verrons comment éviter les verrous lors de telles opérations. Ensuite, nous parlerons des opérations avec PostgreSQL qui sont initialement rapides et ne nécessitent pas de longs verrous. Et à la fin, parlons de notre bibliothèque zero_downtime_migrations, que nous utilisons pour automatiser certaines des techniques décrites précédemment pour éviter de longs verrous.

Opérations nécessitant un verrouillage long:



Création d'un index. Par défaut, il ne bloque pas les opérations de lecture dans la table, mais toutes les opérations d'écriture seront bloquées pendant toute la durée de création de l'index; en conséquence, le service sera en lecture seule.

De plus, de telles opérations incluent l'ajout d'une nouvelle colonne avec une valeur par défaut, car sous le capot, PostgreSQL va écraser la table entière, et pour cette fois, elle sera bloquée à la fois en lecture et en écriture. De plus, tous ses indices seront écrasés.

À propos du changement de type de colonne - une chose similaire se produira, la plaque sera également écrasée à nouveau. Il convient de noter que cela prend non seulement beaucoup de temps sur les grandes tables, mais aussi pour une courte période nécessite jusqu'à doubler la quantité de mémoire libre occupée par la table.

En outre, l'opération VACUUM FULL nécessite le même niveau de verrouillage que les opérations précédentes - c'est un accès exclusif. VACUUM FULL bloquera également toutes les opérations de lecture et d'écriture dans la table.

Les deux dernières opérations ajoutent des propriétés uniques à la colonne et, en général, ajoutent CONSTRAINT. Ils nécessitent également un verrouillage pour la durée de la vérification des données, bien qu'ils prennent beaucoup moins de temps que ceux considérés précédemment, car ils n'écrasent pas les tables sous le capot.





Création d'un index. C'est assez simple ici, il peut être créé en utilisant le mot clé CONCURRENTLY. Quelle est la différence? Cette opération prendra plus de temps, car pas un, mais plusieurs passages dans la table seront effectués, et elle attendra également la fin de toutes les opérations en cours qui peuvent potentiellement modifier l'index. Et il peut également échouer - par exemple, si un index unique est violé lors de la création d'un index unique. Ensuite, l'index sera marqué comme non valide, et il devra être supprimé et recréé. La commande REINDEX n'est pas recommandée, car elle fonctionne de la même manière que la CREATE INDEX normale, c'est-à-dire qu'elle verrouille la table pour l'écriture.

En ce qui concerne la suppression de l'index - à partir de la version 9.3, vous pouvez également supprimer l'index de manière CONCURRENTE pour éviter le blocage lors de sa suppression, bien qu'en général, cette opération soit si rapide.



Examinons l'ajout d'une nouvelle colonne avec une valeur par défaut. Voici une opération standard qui est effectuée lorsque nous voulons exécuter une telle commande, y compris Django effectue une telle opération.

Comment puis-je le réécrire pour éviter d'écraser la table? Tout d'abord, dans une transaction, ajoutez une nouvelle colonne sans valeur par défaut et ajoutez une valeur par défaut dans une demande distincte. Quelle est la différence ici? Lorsque nous ajoutons une valeur par défaut à une colonne existante, cela ne change pas les données existantes dans le tableau. Seules les métadonnées changent. Autrement dit, pour toutes les nouvelles lignes, cette valeur par défaut sera déjà garantie. Il nous reste à mettre à jour toutes les lignes existantes qui étaient dans la table au moment où cette commande a été exécutée. Ce que nous ferons par lots de plusieurs milliers de copies afin de ne pas bloquer pendant longtemps une grande quantité de données.

Après avoir mis à jour toutes les données, il ne reste plus qu'à exécuter SET NOT NULL si nous créons une colonne NOT NULL. Si nous ne créons pas, alors ne le faisons pas. De cette façon, vous pouvez éviter d'écraser la table lors de ce type de modification.

Une telle séquence de commandes prend plus de temps que l'exécution d'une commande régulière, car elle dépend de la taille de la table et du nombre d'index qu'elle contient, et la commande habituelle bloque simplement toutes les opérations et écrase la table quelle que soit la charge, car il n'y a pas de charge pour le moment. Mais cela n'a pas tellement d'importance, car pendant l'opération, la table est disponible pour la lecture et l'écriture. Cela prend beaucoup de temps, il vous suffit de suivre cela et c'est tout.



À propos de la modification du type de colonne. L'approche est similaire à l'ajout d'une colonne avec une valeur par défaut. Nous ajoutons d'abord une colonne distincte du type dont nous avons besoin, puis ajoutons des déclencheurs pour modifier les données de la colonne d'origine pour écrire dans les deux colonnes à la fois, dans une nouvelle avec le type de données dont nous avons besoin. Pour toutes les nouvelles entrées, elles iront immédiatement dans ces deux colonnes. Nous devons mettre à jour tous ceux existants. Ce que nous faisons par portions, comme sur la diapositive précédente, était similaire.

Après cela, il reste dans une transaction pour supprimer le déclencheur, supprimer l'ancienne colonne et renommer l'ancienne colonne en une nouvelle. Ainsi, nous avons obtenu le même résultat: nous avons changé le type de la colonne, tout en verrouillant la table n'était pas long.



À propos de l'ajout d'une colonne unique. Un verrou est pris au moment de la création. Cela peut être évité si vous savez que l'unicité dans PostgreSQL est garantie en créant un index unique. Nous pouvons nous-mêmes construire l'index unique requis à l'aide de CONCURRENTLY. Et après avoir construit cet index, créez CONSTRAINT à l'aide de cet index. Après cela, la définition de l'index initial de la table disparaîtra et le résultat que la définition de la table nous montrera ne sera pas différent après avoir effectué ces deux opérations.



Et en général, lors de l'ajout de CONTRAINTE. Vous pouvez utiliser cette technique pour éviter le blocage lors de la vérification des données. Nous ajoutons d'abord CONSTRAINT avec le mot clé NOT VALID. Cela signifie que cette CONTRAINTE n'est pas garantie d'être exécutée pour toutes les lignes de la table. Mais en même temps, pour toutes les nouvelles lignes, cette CONTRAINTE sera déjà appliquée et les exceptions correspondantes seront levées si elle n'est pas exécutée.

Nous ne pouvons valider que toutes les valeurs existantes, ce qui peut être fait avec une commande VALIDATE CONSTRAINT distincte, et en même temps, cette commande n'interfère plus avec la lecture ou l'écriture dans la table. Un tableau pour cette fois sera disponible.

Opérations qui fonctionnent initialement rapidement dans PostgreSQL et ne nécessitent pas de longs verrous:



L'une de ces opérations consiste à ajouter une colonne sans valeurs par défaut et sans restrictions. Étant donné qu'aucune modification n'est apportée à la table elle-même, seules ses métadonnées changent. Et toutes les valeurs NULL que nous voyons à la suite de SELECT sont mélangées simplement dans la sortie.

De plus, l'ajout de valeurs par défaut à une étiquette existante est une opération rapide car seules les métadonnées changent. La table et le verrou sont pris littéralement pendant les quelques millisecondes nécessaires pour entrer ces informations.

De plus, l'opération rapide de définition de SET NOT NULL, ici, prend un peu plus de temps que décrit précédemment, environ quelques secondes par table de 30 millions d'enregistrements. Ce temps peut également être évité si cela est important.

Renommer une colonne, modifier la longueur d'une colonne ne conduit pas non plus à écraser une colonne. La suppression d'une colonne et, en général, de nombreuses entités dans PostgreSQL est également une opération rapide.



Concernant l'ajout d'une colonne NOT NULL. Afin d'éviter le blocage lors de la validation, vous pouvez effectuer la méthode mentionnée précédemment - ajoutez CONSTRAINT correspondant à CHECK (colonne IS NOT NULL) NOT VALID, et validez-le avec une commande distincte.

La différence en général est que cette restriction existera au niveau de la table et non au niveau de la colonne dans la définition de la table. Une autre différence est qu'elle peut affecter les performances, environ 1%. Dans ce cas, il n'y aura pas de blocage, si le service est très chargé, même quelques secondes de blocage peuvent conduire à une énorme file d'attente de transactions s'accumulant et il y aura un problème sur le service.



La suppression de données dans PostgreSQL est généralement une opération rapide, car les données ne sont pas supprimées immédiatement, seule la colonne est marquée comme obsolète dans les attributs de la table, et les données ne seront réellement supprimées qu'après le début du prochain vide.



Parlons de la bibliothèque . Je parle de Django, de la migration. En général, Django est une bibliothèque pour Python, un framework web, à l'origine, il a été créé pour créer rapidement des sites Web comme des nouvelles, depuis lors, il a été considérablement mis à niveau. Il existe un système ORM qui vous permet de communiquer avec les enregistrements de la base de données, avec les tables, comme s'il s'agissait d'objets ou de classes Python. Autrement dit, chaque table a sa propre classe en Python. Et lorsque nous apportons des modifications à notre code Python, c'est-à-dire que nous ajoutons de nouveaux attributs tels que des colonnes à la table, Django pendant le processus de création de la migration remarque ces modifications, et crée les fichiers de migration pour apporter des modifications en miroir à la base de données elle-même afin qu'elles ne divergent pas.

La bibliothèque a été écrite pour automatiser certaines des techniques discutées précédemment pour éviter de longs verrous sur la table lors de telles migrations. Il fonctionne avec Django depuis la version 1.8 à 2.1 inclus, et Python de 2.7 à 3.7 inclus.

En ce qui concerne les fonctionnalités actuelles de la bibliothèque, cela ajoute une colonne avec une valeur par défaut sans verrous, nullable ou non, cela crée un index CONCURRENTLY, ainsi que la possibilité de redémarrer en cas de plantage. Dans l'implémentation Django standard, si nous ajoutons une colonne avec une valeur par défaut, la table est verrouillée, et si elle est grande, cela pourrait prendre 40 minutes de verrouillage selon mon expérience. La table est verrouillée, et c'est tout, attendez que les modifications soient copiées et effectuées. 30 minutes se sont écoulées - ils ont détecté l'erreur de connexion à la base de données, la migration tombe, les modifications ne sont pas validées et vous devez recommencer, attendre à nouveau 40 minutes, bloquant à nouveau la table pendant ce temps.


Lien GitHub

La bibliothèque vous permet de reprendre la migration depuis l'endroit où elle a été interrompue. Lorsque vous vous plantez et redémarrez, une boîte de dialogue s'affiche avec différentes options d'action, c'est-à-dire que vous pouvez dire de continuer la mise à jour des données. Il s'agit généralement d'une mise à jour des données car il s'agit du processus le plus long. La migration se poursuivra simplement là où elle s'était arrêtée. Une telle opération prend également plus de temps qu'une opération standard avec verrouillage de table, mais en même temps, le service reste opérationnel à ce moment.



À propos de la connexion dans son ensemble. Il y a de la documentation; en bref, vous devez remplacer le moteur dans les paramètres de la base de données Django par le moteur de la bibliothèque. Il existe également divers mixins si vous utilisez vos moteurs pour vous connecter.



Un exemple de travail consiste à ajouter une colonne avec une valeur par défaut. Ici, nous ajoutons des colonnes avec une valeur booléenne, True par défaut. Quelles opérations sont effectuées par le SchemaEditor standard? Les opérations que vous pouvez voir si vous exécutez la migration SQL. Ceci est assez utile, par le type même de migration, on ne sait pas toujours ce que Django peut réellement y changer. Et il est utile de commencer et de voir si les opérations que nous attendons sont terminées et si quelque chose de superflu et inutile est arrivé.

Quelles commandes SchemaEditor exécute-t-il? Tout d'abord, une nouvelle colonne est ajoutée à une transaction, la valeur par défaut est ajoutée. Ensuite, jusqu'à ce qu'une telle mise à jour renvoie qu'elle a mis à jour zéro, les données seront mises à jour.

Ensuite, SET NOT NULL est défini dans la colonne et la valeur par défaut sera supprimée, répétant le comportement de Django, qui stocke la valeur par défaut non pas dans la base de données, mais au niveau logique dans le code.

Ici, en général, il y a aussi de la place pour grandir. Par exemple, vous pouvez créer un index auxiliaire pour trouver rapidement ces lignes avec une valeur NULL à l'approche de la mise à jour de la table entière.



Vous pouvez également fixer l'ID maximum pour l'heure de mise à jour lorsque nous avons commencé la migration, de sorte que par id, vous puissiez trouver rapidement des valeurs que nous n'avons pas encore mises à jour.

En général, la bibliothèque se développe, nous acceptons les demandes de pool. Peu importe - rejoignez-nous.

Il convient de noter qu'avec la croissance des bases de données, les migrations ont une propriété inévitable à ralentir. Vous devez garder une trace des verrous pris par la table, exécuter des migrations SQL pour voir quelles opérations sont appliquées. Pour notre part, dans Yandex.Connect, nous utilisons cette bibliothèque là où ses capacités le permettent. Et là où ils ne le permettent pas, nous-mêmes, de nos propres mains, fausses migrations Django, exécutons nos requêtes SQL.

Ainsi, nous atteignons une haute disponibilité des services de lecture et d'écriture. Nous avons des charges lourdes, des milliers de RPS et des temps d'arrêt en quelques minutes, sans parler de plus de temps, sont inacceptables. Il est nécessaire que les migrations se passent inaperçues par l'utilisateur. Et avec de telles charges, il ne sera pas possible de se lever à quatre heures du matin, de rouler quelque chose quand il n'y a pas de charge, et de se recoucher - car la charge circule 24h / 24.

Il convient de noter que même des opérations rapides dans PostgreSQL peuvent toujours provoquer un ralentissement du service et des erreurs en raison du fonctionnement de la file d'attente de verrouillage dans PostgreSQL.

Imaginez qu'une opération soit lancée qui, même pendant quelques millisecondes, nécessite un accès exclusif. Un exemple d'une telle opération est l'ajout d'une colonne sans valeur par défaut. Imaginez qu'au moment de son lancement dans une autre transaction, il y ait une autre opération longue - disons SELECT avec agrégation. Dans ce cas, notre opération fera la queue pour elle. Cela se produira car l'accès exclusif entre en conflit avec tous les autres types de verrous.

Pendant que notre opération d'ajout d'une colonne attend un verrou, toutes les autres le feront dans la file d'attente et ne seront pas exécutées tant qu'elle ne sera pas terminée. Dans le même temps, l'opération en cours - SELECT avec agrégation - peut ne pas entrer en conflit avec les autres, et sans notre création de la colonne, elles ne se seraient pas retrouvées dans la file d'attente, mais auraient été exécutées en parallèle.

Cette situation peut créer de gros problèmes sur le service. Par conséquent, avant de démarrer ALTER TABLE ou toute autre opération nécessitant un verrouillage exclusif d'accès, vous devez vérifier que les longues requêtes ne sont pas envoyées à la base de données pour le moment. Ou vous pouvez simplement insérer un très petit délai d'expiration du journal. Ensuite, s'il n'était pas possible de prendre rapidement la serrure, l'opération tomberait. Nous pourrions simplement le redémarrer et ne pas verrouiller la table pendant longtemps, tandis que l'opération attendra l'octroi d'une autorisation pour les verrous. C'est tout, merci.

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


All Articles