Aller au contenu
English

Migrations

  • Leçon 10
  • 50 min
  • Module 3 : Accéder aux données depuis une API

Ce que vous saurez faire

Modifiez la base de données d’une API en cours d’exécution sans surprises. Écrivez un fichier numéroté par changement, appliquez-le une seule fois et ne le modifiez jamais. Faites un essai sur beaucoup de données. Aucun changement ne doit bloquer longtemps une table occupée.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 50 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

  • Le projet après Gestion des connexions : python -m pytest affiche 74 passed (74 tests réussis).
  • Les données volumineuses chargées dans votre base de données, tirées de l’exercice guidé de cette leçon. Deux terminaux.

Après cette leçon, votre projet correspond à l’instantané end-of-m03. Le rapport qui garde un verrou, la migration modifiée et les fichiers temporaires sont des cas construits. Testé sur macOS (Apple silicon); les commandes Windows n’ont pas été testées. Les durées ont été mesurées sur un ordinateur Apple M4 sans autre activité. Vos durées seront différentes; la tendance restera la même.

Une ville répare ses routes avec des ordres de travail numérotés. Elle exécute chaque ordre une seule fois et ne réécrit jamais un ordre terminé. Si la réparation était incorrecte, la ville écrit un nouvel ordre. Ainsi, chacun peut lire la liste et connaître l’état actuel de la route.

La même réparation peut être facile ou difficile. Sur une route d’essai tranquille, fermer une voie pendant une heure ne dérange personne. Sur la rue principale à l’heure de pointe, une fermeture de deux minutes crée une file d’un kilomètre. Pire encore, une voiture qui voulait seulement tourner à droite attend aussi, car elle est coincée derrière la file.

Un changement de base de données, c’est un chantier sur la rue qu’emprunte l’API.

Une migration est un fichier SQL numéroté qui modifie la base de données, appliqué par ticket_api/migrate.py. Le projet a repris cet outil d’exécution du cours sur la CI/CD. L’outil applique les fichiers dans l’ordre de leurs noms, inscrit chacun dans schema_migrations et exécute chacun dans sa propre transaction. Votre base de données en contient six : 001 à 004 de la configuration initiale, puis 005 et 006 du module 2.

Ces règles donnent un ordre et un historique. Elles ne répondent pas aux trois questions que se pose maintenant le service de soutien. Que se passe-t-il si quelqu’un modifie un fichier déjà exécuté? Combien de temps prend un changement sur 300 000 billets de soutien? Et quel effet a un changement sur l’API qui lit la table en même temps?

Le problème : rapide sur peu de données, bloquant sur beaucoup de données

Section intitulée « Le problème : rapide sur peu de données, bloquant sur beaucoup de données »

La migration 006 ajoute deux colonnes à tickets et les remplit, dans une seule transaction. Sur les données peu volumineuses, elle a pris 0,01 secondes. Mei l’a testée de nouveau sur les données volumineuses, pendant qu’un lecteur demandait un billet toutes les 10 ms :

  • Données peu volumineuses
  • Données volumineuses
  • Attente de lecture (volumineuses)
Secondes par migration sur les données peu volumineuses (200 billets) et volumineuses (300 000 billets), et attente maximale d'une lecture simple pendant la migration sur les données volumineuses. Médianes de 3 exécutions de scripts.rehearse --from 005. 008 est la migration d'index du module 4.
Description textuelle du graphique

Trois barres par migration. Les deux premières indiquent les secondes sur les données peu volumineuses et volumineuses. La troisième montre l’attente maximale d’une lecture par clé pendant l’exécution sur les données volumineuses. 005 (contraintes) : 0,02 s sur peu de données, 1,16 s sur beaucoup de données. Les lectures ont attendu 0,88 s. 006 (deux colonnes et leur remplissage) : 0,01 s sur peu de données, 1,83 s sur beaucoup de données. Les lectures de tickets ont attendu 1,81 s. 007 (le coût en numeric) : 0,02 s sur peu de données, 0,37 s sur beaucoup de données. Les lectures de ai_runs ont attendu 0,35 s. 008 (trois index, créés avec CONCURRENTLY dans le module 4) : 0,02 s sur peu de données, 0,97 s sur beaucoup de données. Les lectures ont attendu 0 s. Les étiquettes arrondissent à une décimale; les valeurs inférieures à 0,05 s n’ont pas d’étiquette.

Ce que cela nous apprend. Sur les données peu volumineuses, chaque migration semble instantanée : les petites barres sont trop courtes pour être visibles. Sur les données volumineuses, 006 a pris 1,83 secondes. Pendant 1,81 de ces secondes, aucune requête ne pouvait lire un seul billet. La table est aussi passée de 75 Mo à 152 Mo. 008 a aussi pris presque une seconde, mais personne n’a attendu : le problème n’est pas la durée, c’est le blocage.

Pourquoi les lectures ont-elles attendu? Chaque instruction prend un verrou sur les tables qu’elle utilise. Ce verrou indique ce que les autres peuvent faire avec la table en même temps. Un SELECT normal prend le verrou le plus faible, qui permet presque tout. ALTER TABLE … ADD COLUMN prend le plus fort, ACCESS EXCLUSIVE (accès exclusif), qui ne permet rien, même pas une lecture. PostgreSQL garde chaque verrou jusqu’à la fin de la transaction. Dans 006, le UPDATE des 300 000 lignes s’exécute dans la même transaction. Le verrou le plus fort reste donc en place pendant tout le remplissage.

Et pourquoi la table a-t-elle doublé de taille? PostgreSQL ne modifie jamais une ligne sur place. Un UPDATE écrit une nouvelle version de chaque ligne. L’ancienne version reste jusqu’à ce qu’un nettoyage appelé VACUUM réutilise son espace. Les 300 000 lignes mises à jour ont créé 300 000 nouvelles versions.

L’idée : faire un essai sur des données représentatives

Section intitulée « L’idée : faire un essai sur des données représentatives »

Pour éviter les surprises, faites un essai. Avant d’exécuter une migration sur la vraie base de données, exécutez-la sur une copie contenant des données semblables aux vraies. Il faut les mêmes tables, la même taille et le même mélange de valeurs. Ce sont des données représentatives. Les données volumineuses du cours sont les données représentatives du service de soutien.

scripts/rehearse.py le fait en une commande. Il crée une base de données distincte, <your database>_rehearsal. Il applique les migrations précédant la première à tester, charge les données volumineuses, puis applique les autres une à une. Pendant chaque migration, un lecteur demande une ligne par clé dans chaque table, toutes les 10 ms. L’attente maximale montre combien de temps la migration a bloqué les lectures normales. Votre propre base de données ne change pas. Voici l’exécution de l’auteur, à partir de 005 :

Loading the large data into tickets_rehearsal (migrations up to 004_help_desk_tables) ...
migration seconds longest read wait (s): tickets messages ai_runs attachments
005_integrity_constraints 1.29 0.93 0.70 0.17 0.47
006_ticket_message_counts 2.11 2.08 0.00 0.00 0.01
007_ai_run_cost_numeric 0.44 0.00 0.00 0.41 0.00

Il s’agit d’une seule exécution; ses chiffres diffèrent donc un peu des médianes du graphique. La tendance est ce qui compte : 006 bloque tickets, et 007 bloque ai_runs. 005 bloque plusieurs tables pendant moins d’une seconde. En effet, SET NOT NULL et les nouvelles vérifications lisent chaque ligne sous un verrou fort.

006 a déjà été exécutée sur chaque base de données du cours; elle reste donc telle quelle. Modifier une migration appliquée est la chose qu’un outil d’exécution de migrations doit interdire (voir les nouvelles règles). La leçon concerne le prochain changement de ce genre. Divisez-le en petites étapes pour qu’aucune étape ne garde un verrou fort longtemps :

À gauche, 006 telle quelle : ALTER TABLE tickets ADD COLUMN message_count … NOT NULL DEFAULT 0, puis UPDATE tickets pour les 300 000 lignes, dans une seule transaction, sous un verrou ACCESS EXCLUSIVE gardé jusqu'à COMMIT : lectures de tickets bloquées 1,8 s, table de 75 à 152 Mo. À droite, le prochain changement écrit de façon sûre : la migration A ajoute la colonne, qui peut être vide, avec un verrou bref. La migration B ou un script remplit les valeurs par lots de quelques milliers de lignes, chaque lot dans sa propre transaction. La migration C, plus tard, ajoute la règle (NOT NULL, CHECK). Résultat : aucun long blocage; chaque verrou dure quelques millisecondes.
Une transaction qui ajoute et remplit une colonne, comparée au même changement en trois étapes.
Description textuelle du diagramme

À gauche, 006 : la colonne et son remplissage forment une seule transaction. Le verrou le plus fort dure donc aussi longtemps que le UPDATE complet. À droite, l’ordre sûr. Ajoutez d’abord la colonne sans valeur par défaut ni règle. Cela modifie seulement la description de la table et prend quelques millisecondes. Remplissez-la ensuite par lots. Chaque lot forme une courte transaction distincte et prend seulement des verrous de ligne, qui ne bloquent pas les lectures. Enfin, ajoutez la règle quand chaque ligne a une valeur.

Ce modèle s’appelle extension et contraction. Commencez par l’extension : ajoutez la nouvelle colonne, dont rien ne dépend encore. Remplissez-la ensuite. Terminez par la contraction : resserrez le schéma, ici avec la règle, quand chaque ligne est prête. Migrations sûres, dans le cours sur la CI/CD, utilise ce modèle pour renommer une colonne; ici, il protège une grande table. Les étapes prennent plus de temps au total; la tâche À votre tour le mesure. Pour la dernière étape, PostgreSQL peut ajouter une vérification sans long blocage. ADD CONSTRAINT … NOT VALID prend un verrou bref et vérifie seulement les nouvelles lignes. VALIDATE CONSTRAINT vérifie ensuite les anciennes lignes pendant que les lectures et les écritures continuent.

Priya additionne le coût des exécutions d’IA. L’ancien outil stockait chaque coût en real, un nombre à virgule flottante d’environ 7 chiffres significatifs, conservé en binaire. Beaucoup de valeurs décimales ne peuvent pas être stockées exactement en binaire. La valeur 0.002432 est stockée sous la forme 0.0024319998919963837. L’auteur a exécuté la même somme six fois sur les données volumineuses, avant 007 :

SELECT sum(cost_usd), sum(cost_usd::numeric) FROM ai_runs;
95.059525|95.0596787
95.05952|95.0596787
95.059494|95.0596787
95.05954|95.0596787
95.05959|95.0596787
95.05959|95.0596787

La colonne de gauche contient la somme des real. Elle change d’une exécution à l’autre. PostgreSQL additionne les lignes en parallèle, dans un ordre différent chaque fois, et chaque addition arrondit le résultat. La colonne de droite contient la somme exacte, 95.0596787 dollars, chaque fois. Soyons honnêtes sur l’ampleur : l’écart est d’au plus 0,0002 dollar sur 449 734 exécutions. Personne ne perd d’argent ici. Le problème, c’est qu’un rapport donne un total différent chaque fois. Et le coût stocké n’est pas le coût qui a été écrit. Cela ne représente pas correctement l’argent.

La migration 007 change donc le type. numeric(12, 8) stocke jusqu’à 12 chiffres, dont 8 après la virgule, exactement, en décimal :

migrations/007_ai_run_cost_numeric.sql
-- Money is not a floating-point number. The old tool stored the cost of each AI
-- run as real (about 7 significant digits, in binary), so sums drift and some
-- values cannot be stored exactly. numeric(12, 8) stores the dollars exactly.
-- This rewrites the whole table and blocks it while it runs: time it on the
-- large data before you run it on a busy database.
ALTER TABLE ai_runs ALTER COLUMN cost_usd TYPE numeric(12, 8);

L’essai indiquait 0,37 secondes sur les données volumineuses, avec les lectures de ai_runs bloquées pendant 0,35 secondes. C’est acceptable pour le service de soutien, à un moment tranquille. Après 007, la somme est 95.05967870, chaque fois.

Mei exécute 007 sur une copie des données volumineuses, et quelque chose d’étrange se produit. Le rapport de Priya garde une transaction ouverte sur ai_runs : il a lu la table sans encore valider la transaction. C’est permis : un verrou de lecture et d’autres lectures partagent la table. Mais 007 a besoin du verrou le plus fort; elle attend donc le rapport. Puis chaque nouvelle lecture de ai_runs attend aussi, derrière 007. PostgreSQL traite les demandes de verrou dans l’ordre. Une lecture ne peut donc pas dépasser une migration en attente. C’est la voiture qui veut tourner, coincée derrière la file.

Une séquence avec quatre participants : le rapport de Priya avec une transaction ouverte, la migration 007, la table ai_runs et une lecture de l'API. À 0,0 s, le rapport prend un verrou ACCESS SHARE et le garde. À 0,5 s, la migration demande ACCESS EXCLUSIVE et attend. À 0,53 s, la lecture demande ACCESS SHARE et attend derrière la migration, même si le rapport et la lecture pourraient partager la table. Sans délai maximal d'attente de verrou : le rapport valide sa transaction à 6,0 s, la migration réécrit la colonne de 6,0 à 6,4 s, et la lecture, après 5,9 s d'attente, échoue avec le message cached plan must not change result type. Avec MIGRATION_LOCK_TIMEOUT = 2s : la migration abandonne à 2,5 s et annule sa transaction; la lecture obtient sa ligne après 2,0 s d'attente, sans erreur; le rapport valide sa transaction à 6,0 s.
Un cas construit sur les données volumineuses : un rapport garde un verrou sur ai_runs pendant 6 secondes, tandis que la migration 007 attend. La lecture vient d'une connexion qui avait exécuté la même requête plusieurs fois.
Description textuelle du diagramme

Le rapport et la lecture ne font que lire; ils pourraient donc partager la table. Mais la migration se trouve entre eux dans la file et a besoin de la table pour elle seule. La lecture attend donc la migration, qui attend le rapport. Sans limite, la lecture a attendu 5,9 secondes, puis a échoué. Avec un délai maximal d’attente de verrou de 2 secondes, la migration a abandonné à 2,51 s et quitté la file. La lecture a obtenu sa ligne après 2,0 secondes, sans erreur.

Cette image présente deux leçons.

Un délai maximal d’attente de verrou limite les dégâts. lock_timeout indique à PostgreSQL combien de temps une instruction peut attendre un verrou. Le nouveau migrate.py le définit pour chaque migration à partir de MIGRATION_LOCK_TIMEOUT, avec 5s par défaut. Une migration qui attend plus longtemps s’arrête avec un message clair et ne change rien. Relancez-la à un moment plus tranquille. Une migration arrêtée coûte peu; une API arrêtée coûte cher.

Un changement de type brise les requêtes préparées par une application en cours d’exécution. psycopg prépare une requête sur le serveur après 5 exécutions sur une même connexion. PostgreSQL garde ensuite son plan, avec le type de chaque colonne du résultat. Après 007, le plan indique real et la colonne indique numeric. L’auteur a vérifié la suite dans un test construit. Une seule connexion de longue durée échouait à chaque exécution avec cached plan must not change result type (le plan en cache ne doit pas changer le type du résultat), jusqu’à sa reconnexion. L’API du projet, avec son pool, échouait une fois par connexion du pool qui avait préparé la requête. Elle fonctionnait ensuite de nouveau.

ADD COLUMN produit le même effet sur un SELECT * préparé. C’est pourquoi le SELECT * FROM tickets de la liste des billets a échoué une fois après l’ajout d’une colonne, dans le même test. Après une migration qui change ce qu’une requête retourne, redémarrez donc l’API ou renouvelez les connexions du pool. Mieux encore, nommez vos colonnes plutôt que d’utiliser SELECT *; le module 4 le fait.

Le nouveau ticket_api/migrate.py ajoute trois règles aux anciennes.

Une migration appliquée ne change jamais. L’outil stocke maintenant une somme de contrôle de chaque fichier dans schema_migrations, une empreinte SHA-256 de ses octets. Si un fichier appliqué change, rien ne s’exécute :

Migration stopped: 005_integrity_constraints.sql was changed after it was applied. Put it back as it was, and write the change as a new migration.

Pourquoi être si strict? Un fichier modifié s’exécute seulement sur les nouvelles bases de données. Les anciennes gardent l’ancienne version; les deux sortes de bases divergent donc sans que personne le remarque. Ce n’est pas théorique. Pendant la création du projet du cours, l’auteur a modifié 012_erasure.sql après son application. Cette règle a arrêté l’exécution suivante avec le même message. La solution était une nouvelle migration.

Une migration n’attend pas indéfiniment. MIGRATION_LOCK_TIMEOUT, comme expliqué plus haut.

Certaines instructions ne peuvent pas s’exécuter dans une transaction. CREATE INDEX CONCURRENTLY crée un index sans bloquer les écritures, et PostgreSQL l’interdit dans une transaction. Un fichier dont la première ligne est -- migrate: no-transaction s’exécute hors transaction, une instruction à la fois. Le module 4 en a besoin pour ses index. Si la création concurrente d’un index échoue à mi-chemin, elle peut laisser un index marqué comme invalide. L’outil vérifie ce cas et s’arrête en indiquant le nom de l’index.

Beaucoup d’équipes utilisent un outil pour cela : Alembic avec SQLAlchemy, ou Flyway. Ces outils enregistrent les versions et les sommes de contrôle de la même façon. Ce cours garde son outil d’exécution de 150 lignes pour que vous puissiez lire chaque règle.

  1. Dans ~/projects/ticket-api, avec l’environnement virtuel actif, copiez les quatre fichiers de cette leçon depuis l’instantané end-of-m03 :

    Fenêtre de terminal
    S=../nextia-learning-labs/C20/snapshots/end-of-m03
    for f in ticket_api/migrate.py migrations/007_ai_run_cost_numeric.sql scripts/rehearse.py tests/test_migrations.py; do
    cp "$S/$f" "$f"
    done
  2. Vérifiez ce qui est en attente : python -m ticket_api.migrate --status. À sa première exécution, le nouvel exécuteur enregistre aussi les sommes de contrôle des six fichiers appliqués.

  3. Faites une répétition à partir de 005 sur les données volumineuses : python -m scripts.rehearse --from 005. Elle prend environ une demi-minute et ne modifie pas votre base de données.

  4. Additionnez les coûts avant le changement : docker compose exec db psql -U tickets -d tickets -c "SELECT sum(cost_usd), sum(cost_usd::numeric) FROM ai_runs;". Exécutez cette commande deux ou trois fois.

  5. Appliquez 007 : python -m ticket_api.migrate. Puis exécutez de nouveau la somme de l’étape 4.

  6. Exécutez les tests : python -m pytest. Vous devriez voir 79 passed (79 tests réussis). Votre projet correspond maintenant à l’instantané end-of-m03.

  7. Essayez la règle de somme de contrôle. Ajoutez une ligne à un fichier appliqué et exécutez les migrations :

    Fenêtre de terminal
    echo "-- a small edit" >> migrations/005_integrity_constraints.sql
    python -m ticket_api.migrate

    Puis restaurez le fichier depuis l’instantané : cp "$S/migrations/005_integrity_constraints.sql" migrations/ (Windows : Copy-Item "$S\migrations\005_integrity_constraints.sql" migrations\). Exécutez de nouveau python -m ticket_api.migrate.

  8. Essayez la règle sans transaction avec un fichier temporaire. Créez migrations/900_index.sql avec une ligne, CREATE INDEX CONCURRENTLY tickets_subject_idx ON tickets (subject);, et exécutez python -m ticket_api.migrate. Puis supprimez le fichier.

Afficher le résultat attendu

L’étape 2 affiche applied (appliqué) pour 001 à 006 et pending 007_ai_run_cost_numeric (007_ai_run_cost_numeric en attente). L’étape 3 affiche un tableau comme celui ci-dessus. L’étape 4 affiche chaque fois la somme exacte 95.0596787 dans la colonne de droite. La colonne de gauche varie un peu entre les exécutions. L’étape 5 affiche applied 007_ai_run_cost_numeric (007_ai_run_cost_numeric appliqué) et Database is up to date (7 migrations). (la base de données est à jour, 7 migrations). La somme est alors 95.05967870. L’étape 7 affiche le message Migration stopped: 005_integrity_constraints.sql was changed after it was applied. … (migration arrêtée : 005_integrity_constraints.sql a été modifié après son application). Après la restauration du fichier, elle affiche Database is up to date (7 migrations). L’étape 8 affiche :

Migration stopped: 900_index.sql failed: CREATE INDEX CONCURRENTLY cannot run inside a transaction block

Il ne reste rien de 900, car la transaction échouée a été annulée. Si python -m ticket_api.migrate --status affiche encore un fichier 900, vous ne l’avez pas supprimé.

Changez une seule chose : créez la prochaine colonne de façon sûre. Le service de soutien veut subject_length, le nombre de caractères de chaque objet. Écrivez deux migrations : 900_ticket_subject_length.sql ajoute la colonne, et 901_backfill_subject_length.sql la remplit par lots de 50 000 lignes. Chaque lot a sa propre transaction. Faites une répétition sur les données volumineuses avec python -m scripts.rehearse --from 900. Faites d’abord une prédiction : combien de temps les lectures de tickets attendront-elles, par rapport à 006?

Afficher une solution et le résultat de l’auteur
migrations/900_ticket_subject_length.sql
-- Expand: a new column that may be empty. No default and no NOT NULL, so
-- PostgreSQL changes only the table's description, not its 300,000 rows.
ALTER TABLE tickets ADD COLUMN subject_length integer;
migrations/901_backfill_subject_length.sql (the first batch; repeat the UPDATE 7 times)
-- migrate: no-transaction
-- Backfill in batches: each UPDATE is its own short transaction, so no lock
-- is held for long. A statement that finds no empty row changes nothing.
UPDATE tickets SET subject_length = length(subject)
WHERE ticket_id IN (SELECT ticket_id FROM tickets WHERE subject_length IS NULL LIMIT 50000);

La première ligne de 901 indique à l’exécuteur d’envoyer chaque instruction séparément. Chaque lot est donc validé séparément. Sept lots couvrent 300 000 lignes; le septième ne trouve rien. La répétition de l’auteur :

migration seconds longest read wait (s): tickets messages ai_runs attachments
900_ticket_subject_length 0.01 0.00 0.00 0.00 0.00
901_backfill_subject_length 4.69 0.01 0.01 0.00 0.01

Les lectures de tickets ont attendu 0,01 s, contre environ 2 s pour 006. Le remplissage a pris plus de temps au total. Chaque lot cherche à nouveau les lignes vides sur un ordinateur occupé. Voilà le compromis : plus de temps au total, aucun blocage. Puis supprimez les deux fichiers. Ils servent à l’exercice; ne les exécutez jamais avec python -m ticket_api.migrate sur votre base de données. La base de données de répétition a été supprimée à la fin.

Une migration qui attend derrière une transaction ouverte

Section intitulée « Une migration qui attend derrière une transaction ouverte »

Erreur courante

Une migration attend un verrou détenu par une session oubliée

Symptôme (simulé) : python -m ticket_api.migrate s’arrête après quelques secondes avec un message de verrou, sans rien modifier. Reproduisez-le avec deux terminaux et une migration temporaire. Dans le terminal 1, ouvrez psql et laissez une transaction ouverte :

Fenêtre de terminal
docker compose exec db psql -U tickets -d tickets
BEGIN;
SELECT count(*) FROM ai_runs;

Dans le terminal 2, créez migrations/900_reviewed.sql avec la ligne ALTER TABLE ai_runs ADD COLUMN reviewed boolean;. Puis exécutez les migrations avec un délai limite court :

Fenêtre de terminal
MIGRATION_LOCK_TIMEOUT=2s python -m ticket_api.migrate
Migration stopped: 900_reviewed.sql waited more than 2s for a lock. Another transaction is using the table. Try again when it is quiet.

Cause : la transaction ouverte dans le terminal 1 détient un verrou sur ai_runs. La migration a besoin du verrou le plus fort, alors elle attend. Sans la limite, chaque lecture de ai_runs depuis l’API attendrait derrière elle.

Diagnostic : trouvez la session qui verrouille la table : SELECT pid, state, now() - xact_start AS open_for, query FROM pg_stat_activity WHERE state LIKE 'idle in transaction%';. Une session qui reste idle in transaction (inactive dans une transaction) pendant plusieurs minutes provient généralement d’une session psql, d’un rapport ou d’une connexion non libérée.

Correction :

  1. Dans le terminal 1, terminez la transaction : ROLLBACK;, puis \q.
  2. Supprimez le fichier temporaire migrations/900_reviewed.sql, pour qu’il ne soit jamais exécuté. python -m ticket_api.migrate --status doit afficher seulement 001 à 007.
  3. En situation réelle, trouvez qui détient le verrou, attendez une période calme et relancez la migration. N’augmentez pas le délai limite jusqu’à ce qu’elle passe : cela ne fait que déplacer la file d’attente vers l’API.

Pour un service d’IA : les tâches d’analyse gardent souvent des transactions ouvertes longtemps. C’est le cas du rapport de Priya ou d’une évaluation sur la table des exécutions d’IA. Planifiez les changements de schéma en dehors de ces tâches et gardez un délai d’attente de verrou court. Ainsi, une migration échoue rapidement au lieu de bloquer l’API.

Vérifiez votre compréhension

Un collègue trouve une faute dans un commentaire de 005_integrity_constraints.sql et la corrige dans le fichier. Que se passe-t-il au prochain déploiement, et que devrait-il faire à la place?

Afficher la réponse

Le prochain python -m ticket_api.migrate s’arrête avant d’appliquer quoi que ce soit : 005_integrity_constraints.sql was changed after it was applied (005_integrity_constraints.sql a été modifié après son application). La somme de contrôle du fichier ne correspond plus à celle de schema_migrations. Restaurez le fichier exactement comme il était et gardez la faute; un commentaire ne modifie pas la base de données. Un vrai changement va dans une nouvelle migration, comme 008_…, pour que chaque base de données le reçoive dans le même ordre.

Vérifiez votre compréhension

Faites une prédiction : la migration 006 a pris 0,01 s sur les petites données. Pourquoi un test sur ces données ne révèle-t-il pas ses 1,8 secondes de lectures bloquées? Que fait la répétition différemment?

Afficher la réponse

La durée de 006 augmente avec le nombre de lignes que son UPDATE réécrit. Pour 200 billets, elle prend des millisecondes; pour 300 000, des secondes. Le verrou est le même dans les deux cas. Mais sur 200 lignes, il dure trop peu de temps pour que quelqu’un le remarque. scripts.rehearse exécute les migrations sur une copie contenant les données volumineuses. Pendant chaque migration, un lecteur demande une ligne toutes les 10 ms. La répétition montre donc la durée et le temps d’attente des lectures normales.

Vérifiez votre compréhension

Avec MIGRATION_LOCK_TIMEOUT=2s, la migration 007 s'est arrêtée sans changer le type du coût. Était-ce un échec? Que devrait faire l'équipe ensuite?

Afficher la réponse

C’était le résultat sûr. Sans la limite, les lectures de ai_runs attendaient 5,9 secondes, puis échouaient. Avec la limite, la migration abandonnait après ses 2 secondes d’attente. Les lectures attendaient au plus 2,0 secondes, sans erreur. Ensuite, trouvez la transaction qui verrouillait la table (pg_stat_activity, idle in transaction) et relancez 007 quand elle sera terminée. Après la réussite de 007, redémarrez l’API ou renouvelez ses connexions. Le type de la colonne a changé alors que l’API avait déjà préparé des requêtes.

Récapitulatif

  • Une migration est un fichier SQL numéroté, appliqué une fois, dans l’ordre, dans sa propre transaction. L’exécuteur stocke maintenant aussi une somme de contrôle. Toute modification d’un fichier déjà appliqué arrête tout.
  • Les petites données cachent le coût. 006 a pris 0,01 s sur 200 billets et 1,83 s sur 300 000. Elle a bloqué chaque lecture de tickets pendant 1,81 s. En effet, ALTER TABLE garde le verrou le plus fort jusqu’à la fin de sa transaction.
  • Faites une répétition de chaque migration sur des données représentatives avec scripts.rehearse. Ajoutez d’abord une colonne, remplissez-la par lots et ajoutez la règle en dernier.
  • 007 stocke le coût sous forme de numeric(12, 8). La somme en real variait entre les exécutions; la somme exacte est toujours de 95.0596787 dollars.
  • Une migration en attente fait attendre chaque lecture suivante : 5,9 s et une erreur sans limite, au plus 2,0 s avec MIGRATION_LOCK_TIMEOUT=2s. Après un changement de type, redémarrez l’API ou renouvelez ses connexions.
  • Votre projet correspond à end-of-m03 : python -m pytest affiche 79 passed. Le module 4 accélère la liste lente, en commençant par son plan de requête.

Leçon suivante : Lire un plan de requête