Migration d'un schéma de base de données sans interruption pour postgresql en utilisant django comme exemple

Présentation


Bonjour, Habr!


Je veux partager l'expérience de l'écriture de migrations pour postgres et django. Il s'agira principalement de postgres, django est un bon ajout ici, car il a une migration automatique du schéma de données pour les changements de modèle hors de la boîte, c'est-à-dire qu'il a une liste assez complète d'opérations de travail pour changer le schéma. Django peut être remplacé par n'importe quel framework / bibliothèque préféré - les approches seront très probablement similaires.


Je ne décrirai pas comment j'en suis arrivé à cela, mais maintenant, en lisant la documentation, j'ai compris l'idée qu'il était nécessaire de le faire plus tôt avec plus de soin et de sensibilisation, donc je le recommande vivement.


Avant d'aller plus loin, permettez-moi de faire les hypothèses suivantes.


Vous pouvez diviser la logique de travail avec la base de données de la plupart des applications en 3 parties:


  1. Migrations - modification du schéma de la base de données (tables), supposons que nous les exécutions toujours dans un seul thread.
  2. Logique d'entreprise - travail direct avec les données (dans les tableaux d'utilisateurs), fonctionne avec les mêmes données en permanence et de manière compétitive.
  3. Migrations de données - ne modifient pas les schémas de données, ils fonctionnent essentiellement comme la logique métier, par défaut, lorsque nous parlons de logique métier, nous entendons également les migrations de données.

Le temps d'arrêt est un état lorsqu'une partie de notre logique métier n'est pas disponible / tombe / est chargée pendant un temps notable pour l'utilisateur, supposons que cela dure quelques secondes.


L'absence de temps d'arrêt peut être une condition critique pour une entreprise, qui doit être respectée par tous les efforts.


Processus de déploiement


Les principales exigences lors du déploiement:


  1. nous avons une base de travail.
  2. nous avons plusieurs machines où la logique métier tourne.
  3. les voitures à logique métier sont cachées derrière l'équilibreur.
  4. notre application fonctionne bien avant, pendant et après la migration continue (l'ancien code fonctionne correctement avec l'ancien et le nouveau schéma de base de données).
  5. Notre application fonctionne bien avant, pendant et après la mise à jour du code sur les voitures (l'ancien et le nouveau code fonctionnent correctement avec le schéma de base de données actuel).

S'il y a un grand nombre de modifications et que le déploiement cesse de satisfaire à ces conditions, il est divisé en le nombre requis de déploiements plus petits satisfaisant à ces conditions, sinon nous avons un temps d'arrêt.


Ordre de déploiement direct:


  1. inondé la migration;
  2. supprimé une machine de l'équilibreur, mis à jour la machine et redémarré, renvoyé la machine à l'équilibreur;
  3. a répété l'étape précédente pour mettre à jour toutes les voitures.

L'ordre de déploiement inversé est pertinent pour supprimer des tables et des colonnes dans une table, lorsque nous créons automatiquement des migrations conformément au schéma modifié et validons la présence de toutes les migrations vers CI:


  1. supprimé une machine de l'équilibreur, mis à jour la machine et redémarré, renvoyé la machine à l'équilibreur;
  2. a répété l'étape précédente pour mettre à jour toutes les voitures;
  3. inondé la migration.

Théorie


Postgres est une excellente base de données, nous pouvons écrire une application qui va écrire et lire les mêmes données dans des centaines et des milliers de flux, et avec une forte probabilité, nous pouvons être sûrs que nos données resteront valides et ne seront pas corrompues, en général, ACID complet. Postgres met en œuvre plusieurs mécanismes pour y parvenir, dont l'un bloque.


Postgres a plusieurs types de verrous, vous pouvez voir plus de détails ici , dans le cadre du sujet, je ne couvrirai que les verrous de table et d'écriture.


Verrous de niveau de table


Au niveau de la table, postgres a plusieurs types de verrous , la principale caractéristique est qu'ils ont des conflits, c'est-à-dire que deux opérations avec des verrous en conflit ne peuvent pas être effectuées simultanément:


ACCESS SHAREROW SHAREROW EXCLUSIVESHARE UPDATE EXCLUSIVESHARESHARE ROW EXCLUSIVEEXCLUSIVEACCESS EXCLUSIVE
ACCESS SHAREX
ROW SHAREXX
ROW EXCLUSIVEXXXX
SHARE UPDATE EXCLUSIVEXXXXX
SHAREXXXXX
SHARE ROW EXCLUSIVEXXXXXX
EXCLUSIVEXXXXXXX
ACCESS EXCLUSIVEXXXXXXXX

Par exemple, ALTER TABLE tablename ADD COLUMN newcolumn integer et SELECT COUNT(*) FROM tablename doivent être strictement exécutés un par un, sinon nous ne pouvons pas trouver les colonnes à retourner à COUNT(*) .


Dans les migrations django (liste complète ci-dessous), il existe les opérations suivantes et leurs verrous correspondants:


blocageopérations
ACCESS EXCLUSIVECREATE SEQUENCE , CREATE SEQUENCE DROP SEQUENCE , CREATE TABLE DROP SEQUENCE CREATE TABLE , DROP SEQUENCE CREATE TABLE DROP SEQUENCE , DROP SEQUENCE ALTER TABLE , DROP INDEX
SHARECREATE INDEX
SHARE UPDATE EXCLUSIVECREATE INDEX CONCURRENTLY , DROP INDEX CONCURRENTLY , ALTER TABLE VALIDATE CONSTRAINT

Parmi les commentaires, tous les ALTER TABLE n'ont pas de verrou ACCESS EXCLUSIVE , les migrations django n'ont pas non plus CREATE INDEX CONCURRENTLY et ALTER TABLE VALIDATE CONSTRAINT , mais elles seront nécessaires pour une alternative plus sûre aux opérations standard un peu plus tard.


Si les migrations sont effectuées séquentiellement dans un thread, tout semble correct, car la migration n'entrera pas en conflit avec une autre migration, mais notre logique métier fonctionnera uniquement pendant la migration et le conflit.


blocageopérationsconflits avec les verrousconflits avec les opérations
ACCESS SHARESELECTACCESS EXCLUSIVEALTER TABLE , DROP INDEX
ROW SHARESELECT FOR UPDATEACCESS EXCLUSIVE , EXCLUSIVEALTER TABLE , DROP INDEX
ROW EXCLUSIVEINSERT , UPDATE , DELETEACCESS EXCLUSIVE , EXCLUSIVE , SHARE ROW EXCLUSIVE , SHAREALTER TABLE , DROP INDEX , CREATE INDEX

Deux points peuvent être résumés ici:


  1. s'il existe une alternative avec un verrouillage plus facile, vous pouvez l'utiliser comme CREATE INDEX et CREATE INDEX CONCURRENTLY .
  2. la plupart des migrations pour modifier le schéma de données entrent en conflit avec la logique métier, en outre, elles entrent en conflit avec ACCESS EXCLUSIVE , c'est-à-dire que nous ne serons même pas en mesure de sélectionner tout en maintenant ce verrou et potentiellement attendre un temps d'arrêt ici, sauf dans le cas où cette opération ne fonctionne pas instantanément et notre temps d'arrêt sera quelques secondes.

Il doit y avoir un choix, ou nous évitons toujours ACCESS EXCLUSIVE , c'est-à-dire que nous créons de nouvelles plaques et copions les données là-bas - de manière fiable, mais pendant longtemps pour une grande quantité de données, ou ACCESS EXCLUSIVE aussi rapide que possible et émettons des avertissements supplémentaires contre les temps d'arrêt - potentiellement dangereux, mais rapides.


Verrous d'enregistrement


Au niveau de l'enregistrement, il existe également des verrous https://www.postgresql.org/docs/current/static/explicit-locking.html#LOCKING-ROWS , ils sont également en conflit, mais ils n'affectent que notre logique métier:


FOR KEY SHAREFOR SHAREFOR NO KEY UPDATEFOR UPDATE
FOR KEY SHAREX
FOR SHAREXX
FOR NO KEY UPDATEXXX
FOR UPDATEXXXX

C'est le point principal des migrations de données, c'est-à-dire que si nous effectuons une UPDATE la migration des données sur toute la plaque, le reste de la logique métier, qui met à jour les données, attendra que le verrou soit libéré et peut dépasser notre seuil d'indisponibilité, il est donc préférable de faire des mises à jour en partie pour les migrations de données. Il convient également de noter que lors de l'utilisation de requêtes SQL plus complexes pour les migrations de données, la division en parties peut fonctionner plus rapidement, car elle peut utiliser un plan et des index plus optimaux.


L'ordre des opérations


Une autre connaissance importante est de savoir comment les opérations seront effectuées, quand et comment elles prennent et libèrent les verrous:


image


Ici, vous pouvez mettre en évidence les éléments suivants:


  1. temps d'exécution de l'opération - pour la migration, c'est le temps de maintenir le verrou, si le verrou lourd est maintenu pendant une longue période, nous aurons un temps d'arrêt, par exemple, cela peut être avec CREATE INDEX ou ALTER TABLE ADD COLUMN SET DEFAULT (dans postgres 11 c'est mieux).
  2. le temps d'attente pour les verrous conflictuels - c'est-à-dire que la migration attend jusqu'à ce que toutes les demandes conflictuelles fonctionnent, et à ce moment, de nouvelles demandes attendront notre migration, les demandes lentes peuvent être très dangereuses ici, simplement non optimales ou analytiques, donc il ne devrait pas y avoir de demandes lentes pendant la migration.
  3. le nombre de demandes par seconde - si nous avons beaucoup de demandes en cours de traitement pendant une longue période, alors les connexions gratuites peuvent se terminer rapidement et au lieu d'un endroit problématique, la base de données entière peut entrer en temps d'arrêt (il n'y aura qu'une limite de connexion pour le superutilisateur), ici vous devez éviter les demandes lentes, réduire le nombre de demandes par exemple, démarrez les migrations pendant la charge minimale, séparez les composants critiques en différents services avec leurs propres bases de données.
  4. il y a beaucoup d'opérations de migrations dans une transaction - plus il y a d'opérations dans une transaction, plus le verrou lourd est maintenu, il est donc préférable de séparer les opérations lourdes, pas de ALTER TABLE VALIDATE CONSTRAINT ou de migration de données dans une transaction avec un verrou lourd.

Délais


lock_timeout a des paramètres tels que lock_timeout et statement_timeout , qui peuvent protéger le début des migrations, à la fois contre une migration mal écrite et contre de mauvaises conditions dans lesquelles la migration peut être déclenchée. Ils peuvent être installés globalement et pour la connexion actuelle.


SET lock_timeout TO '2s' évitera les temps d'arrêt en attendant des demandes / transactions lentes avant la migration: https://www.postgresql.org/docs/current/static/runtime-config-client.html#GUC-LOCK-TIMEOUT .


SET statement_timeout TO '2s' évitera les temps d'arrêt lors du démarrage d'une migration lourde avec un verrou lourd: https://www.postgresql.org/docs/current/static/runtime-config-client.html#GUC-STATEMENT-TIMEOUT .


Deadlocks


Les blocages dans les migrations ne concernent pas les temps d'arrêt, mais ce n'est pas agréable lors de l'écriture de la migration, cela fonctionne bien dans un environnement de test, mais il intercepte les blocages lors du roulement sur le prod. Les principales sources de problèmes peuvent être un grand nombre d'opérations dans une transaction et une clé étrangère, car il crée des verrous dans les deux tables, il est donc préférable de séparer les opérations de migration, plus atomique est meilleur.


Stockage des enregistrements


Postgres stocke les valeurs de différents types de différentes manières : si les types sont stockés différemment, la conversion entre eux nécessitera une réécriture complète de toutes les valeurs, heureusement certains types sont stockés de la même manière et n'ont pas besoin d'être réécrits lorsqu'ils sont modifiés. Par exemple, les lignes sont stockées de la même manière quelle que soit la taille et la diminution / augmentation de la dimension d'une ligne ne nécessitera pas de réécriture, mais la diminution nécessite de vérifier que toutes les lignes ne dépassent pas une taille plus petite. D'autres types peuvent également être stockés de manière similaire et avoir des caractéristiques similaires.


Contrôle d'accès simultané multiversion (MVCC)


Selon la documentation , la cohérence postgres est basée sur le multiversion des données, c'est-à-dire que chaque transaction et opération voit sa propre version des données. Cette fonctionnalité gère l'accès concurrentiel et donne également un effet intéressant lors de la modification d'un schéma comme l'ajout et la suppression de colonnes ne modifie que le schéma, s'il n'y a pas d'opérations supplémentaires pour modifier les données, les index ou les constantes, après quoi les opérations d'insertion et de mise à jour à un bas niveau créeront de nouvelles enregistrements avec toutes les valeurs nécessaires, la suppression marquera l'enregistrement correspondant supprimé. VACUUM ou AUTO VACUUM est responsable du nettoyage des débris restants.


Exemple Django


Nous avons maintenant une idée de ce dont les temps d'arrêt peuvent dépendre et comment les éviter, mais avant d'appliquer les connaissances, vous pouvez regarder ce que django donne hors de la boîte ( https://github.com/django/django/blob/2.1.2/django /db/backends/base/schema.py et https://github.com/django/django/blob/2.1.2/django/db/backends/postgresql/schema.py ):


opération
1CREATE SEQUENCE
2DROP SEQUENCE
3CREATE TABLE
4DROP TABLE
5ALTER TABLE RENAME TO
6ALTER TABLE SET TABLESPACE
7ALTER TABLE ADD COLUMN [SET DEFAULT] [SET NOT NULL] [PRIMARY KEY] [UNIQUE]
8ALTER TABLE ALTER COLUMN [TYPE] [SET NOT NULL|DROP NOT NULL] [SET DEFAULT|DROP DEFAULT]
9ALTER TABLE DROP COLUMN
10ALTER TABLE RENAME COLUMN
11ALTER TABLE ADD CONSTRAINT CHECK
12ALTER TABLE DROP CONSTRAINT CHECK
13ALTER TABLE ADD CONSTRAINT FOREIGN KEY
14ALTER TABLE DROP CONSTRAINT FOREIGN KEY
15ALTER TABLE ADD CONSTRAINT PRIMARY KEY
16ALTER TABLE DROP CONSTRAINT PRIMARY KEY
17ALTER TABLE ADD CONSTRAINT UNIQUE
18ALTER TABLE DROP CONSTRAINT UNIQUE
19CREATE INDEX
20DROP INDEX

Django couvre très bien mes besoins de migration, maintenant nous pouvons discuter des opérations sûres et dangereuses pour les migrations sans temps d'arrêt à notre connaissance.


Nous appellerons des migrations plus sûres avec le verrouillage SHARE UPDATE EXCLUSIVE ou ACCESS EXCLUSIVE , qui fonctionne instantanément.
Nous appellerons des migrations dangereuses avec des verrous SHARE et ACCESS EXCLUSIVE , qui prennent un temps considérable.


Je vais laisser un lien utile vers la documentation à l'avance avec de bons exemples.


Créer et supprimer une table


CREATE SEQUENCE , DROP SEQUENCE , CREATE TABLE , DROP TABLE peuvent être appelés sûrs, car la logique métier ne fonctionne plus avec la table migrée, le comportement de suppression d'une table avec FOREIGN KEY sera un peu plus tard.


Opérations de feuille de calcul fortement prises en charge


ALTER TABLE RENAME TO - Je ne peux pas appeler cela sûr, car il est difficile d'écrire une logique qui fonctionne avec une telle table avant et après la migration.


ALTER TABLE SET TABLESPACE - dangereux, car il déplace physiquement la plaque, ce qui peut prendre beaucoup de temps sur un grand volume.


En revanche, ces opérations sont assez rares, comme alternative vous pouvez proposer la création d'une nouvelle table et la copie des données dans celle-ci.


Créer et supprimer des colonnes


ALTER TABLE ADD COLUMN , ALTER TABLE DROP COLUMN - peut être appelé sûr (création sans DEFAULT / NOT NULL / PRIMARY KEY / UNIQUE), car la logique métier ne fonctionne plus avec une colonne migrée, le comportement de suppression d'une colonne avec FOREIGN KEY, d'autres constantes et index viendront plus tard.


ALTER TABLE ADD COLUMN SET DEFAULT , ALTER TABLE ADD COLUMN SET NOT NULL , ALTER TABLE ADD COLUMN PRIMARY KEY , ALTER TABLE ADD COLUMN UNIQUE - opérations dangereuses, car elles ajoutent une colonne et, sans relâcher les verrous, mettent à jour les données avec des valeurs par défaut ou créent des constructions comme alternatives, colonnes nullables et modifications supplémentaires.


Il convient de mentionner le SET DEFAULT plus rapide dans postgres 11, il peut être considéré comme sûr, mais il ne devient pas très utile dans django, car django utilise SET DEFAULT uniquement pour remplir la colonne, puis crée DROP DEFAULT , et dans l'intervalle entre la migration et la mise à jour des machines avec la logique métier, des enregistrements peuvent être créés dans lesquels la valeur par défaut sera absente, c'est-à-dire, tout de même, faire la migration des données.


Opérations fortement prises en charge sur une feuille de calcul


ALTER TABLE RENAME COLUMN - Je ne peux pas non plus l'appeler sûr, car il est difficile d'écrire une logique qui fonctionne avec une telle colonne avant et après la migration. Au contraire, cette opération ne sera pas non plus fréquente, car une alternative peut être proposée pour créer une nouvelle colonne et y copier des données.


Changement de colonne


ALTER TABLE ALTER COLUMN TYPE - l'opération peut être à la fois dangereuse et sûre. Sûr si postgres ne modifie que le schéma et que les données sont déjà stockées au format requis et que des vérifications de type supplémentaires ne sont pas nécessaires, par exemple:


  • changement de type de varchar(LESS) à varchar(MORE) ;
  • changement de type de varchar(ANY) en text ;
  • tapez le changement de numeric(LESS, SAME) à numeric(MORE, SAME) .

ALTER TABLE ALTER COLUMN SET NOT NULL est dangereux, car il passe à travers les données à l'intérieur et vérifie NULL, heureusement cette construction peut être remplacée par un autre CHECK IS NOT NULL . Il est à noter que ce remplacement conduira à un schéma différent, mais avec des propriétés identiques.


ALTER TABLE ALTER COLUMN DROP NOT NULL , ALTER TABLE ALTER COLUMN SET DEFAULT , ALTER TABLE ALTER COLUMN DROP DEFAULT - opérations sûres.


Création et suppression d'index et de constantes


ALTER TABLE ADD CONSTRAINT CHECK et ALTER TABLE ADD CONSTRAINT FOREIGN KEY sont des opérations dangereuses, mais elles peuvent être déclarées NOT VALID , puis ALTER TABLE VALIDATE CONSTRAINT .


ALTER TABLE ADD CONSTRAINT PRIMARY KEY et ALTER TABLE ADD CONSTRAINT UNIQUE pas sûrs, car ils créent un index unique à l'intérieur, mais vous pouvez créer un index unique comme CONCURRENTLY , puis créer la constante correspondante à l'aide d'un index prêt à l'emploi via USING INDEX .


CREATE INDEX est une opération dangereuse, mais un index peut être créé de manière CONCURRENTLY .


ALTER TABLE DROP CONSTRAINT CHECK , ALTER TABLE DROP CONSTRAINT FOREIGN KEY , ALTER TABLE DROP CONSTRAINT PRIMARY KEY , ALTER TABLE DROP CONSTRAINT UNIQUE , DROP INDEX - opérations sûres.


Il convient de noter que ALTER TABLE ADD CONSTRAINT FOREIGN KEY et ALTER TABLE DROP CONSTRAINT FOREIGN KEY verrouillent deux tables à la fois.


Appliquer les connaissances dans Django


Django a une opération dans les migrations pour exécuter n'importe quel SQL: https://docs.djangoproject.com/en/2.1/ref/migration-operations/#django.db.migrations.operations.RunSQL . Grâce à lui, vous pouvez définir les délais d' state_operations nécessaires et appliquer des opérations alternatives pour les migrations, en indiquant state_operations - la migration que nous state_operations .


Cela fonctionne bien pour son code, bien qu'il nécessite des écritures supplémentaires, mais vous pouvez laisser le sale boulot sur le back-end db, par exemple, https://github.com/tbicr/django-pg-zero-downtime-migrations/blob/master/django_zero_downtime_migrations_postgres_backend/schema .py recueille les pratiques décrites et remplace les opérations dangereuses par des homologues sécurisées, et cela fonctionnera pour les bibliothèques tierces.


En fin de compte


Ces pratiques m'ont permis d'obtenir un schéma identique créé par django hors de la boîte, à l'exception du remplacement de la construction CHECK IS NOT NULL au lieu de NOT NULL et de certains noms de construction (par exemple, pour ALTER TABLE ADD COLUMN UNIQUE et une alternative). Un autre compromis peut être le manque de transactionnalité pour les opérations de migration alternatives, en particulier lorsque CREATE INDEX CONCURRENTLY et ALTER TABLE VALIDATE CONSTRAINT .


Si vous n'allez pas au-delà des postgres, il peut y avoir de nombreuses options pour modifier le schéma de données, et elles peuvent être modifiées en combinaison dans des conditions spécifiques:


  • utiliser jsonb comme solution sans faille
  • la possibilité d'aller aux temps d'arrêt
  • obligation d'effectuer des migrations sans interruption

En tout cas, j'espère que le matériel s'est avéré utile soit pour augmenter la disponibilité soit pour étendre la conscience.

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


All Articles