Read in English · Leer en español · Auf Deutsch lesen
Section 7.7 sur 7.8 · Intermédiaire · 25 min de lecture
Évolution des schémas, migrations et compatibilité
Leçon vidéo
5 min · narré en anglais, avec sous-titresÉcouter seulement (audio)
Lire la transcription
Tout schéma changera pendant que le système tourne : tables de base de données, charges utiles d’API et événements. Dans cette leçon, vous allez apprendre à faire ces changements en toute sécurité, sans interruption de service.
Imaginez un hôpital qui modifie le formulaire papier utilisé pour chaque admission, alors que les patients continuent d’arriver. S’il se contente de remplacer les formulaires du jour au lendemain, l’équipe de nuit aura en main l’ancien formulaire tandis que l’équipe de jour attendra le nouveau. Et les dossiers archivés suivent toujours l’ancienne présentation. Un hôpital prudent imprime un formulaire comportant à la fois les anciennes et les nouvelles cases, et l’utilise jusqu’à ce que tout le monde soit passé au nouveau et que les anciens dossiers aient été recopiés. Ce n’est qu’ensuite qu’il supprime les anciennes cases. Les équipes logicielles modifient la structure de leurs données exactement de cette manière prudente, étape par étape.
Trois faits rendent cela difficile. Premièrement, dans tout système déployé sans interruption de service, l’ancienne et la nouvelle version du code tournent côte à côte, pendant un déploiement progressif et après tout retour arrière. Chaque état du schéma doit donc fonctionner avec au moins deux versions du code. Deuxièmement, les données survivent au code. Les lignes écrites il y a cinq ans, les événements conservés dans un journal et les sauvegardes gardent tous l’ancienne forme. Troisièmement, les schémas sont partagés. Une colonne peut être lue par des rapports, des tâches d’extraction et d’autres services. C’est pourquoi un schéma privé, détenu derrière une interface, est tellement plus facile à modifier.
La compatibilité se définit par les lecteurs et les rédacteurs. À gauche, des données écrites avec l’ancien et le nouveau schéma ; à droite, des lecteurs utilisant chacun d’eux. Des versions identiques ne posent jamais de problème. Un changement est rétrocompatible lorsque les nouveaux lecteurs peuvent lire les anciennes données. Ajouter un champ facultatif avec une valeur par défaut est rétrocompatible : le nouveau lecteur fournit la valeur par défaut. Un changement offre une compatibilité future lorsque les anciens lecteurs peuvent lire les nouvelles données. Cela exige que les anciens lecteurs ignorent les champs qu’ils ne connaissent pas. La compatibilité totale signifie que les deux sens fonctionnent, si bien que lecteurs et rédacteurs peuvent être mis à niveau dans n’importe quel ordre. Pour une base de données, le nouveau code commence généralement à écrire avant que chaque lecteur soit redéployé ; il vous faut donc au moins la compatibilité future.
Certains changements sont sûrs. Ajouter un champ facultatif, ou une colonne nullable avec une valeur par défaut, est le changement sûr par excellence. Un champ obligatoire sans valeur par défaut casse les anciennes données : ajoutez-le donc d’abord comme facultatif, remplissez-le, et ne l’imposez qu’ensuite. Un renommage casse les deux sens à la fois ; il ne se fait donc jamais en une seule étape. Et le changement le plus dangereux garde la forme mais change le sens, par exemple faire passer un montant des pence aux livres. Tous les lecteurs se trompent alors sans que rien ne le signale. Créez toujours un nouveau champ sous un nouveau nom.
La technique pour les changements cassants s’appelle l’expansion-contraction. Ici, la plateforme de livraison de repas renomme une colonne de postcode en postal code, dans une table de quatre millions de lignes. Un simple renommage casserait le code en cours d’exécution. D’abord, donc, l’expansion : ajoutez la nouvelle colonne, vide. Ensuite, l’application écrit la même valeur dans les deux colonnes, mais lit toujours l’ancienne. Une tâche d’arrière-plan copie les lignes existantes par petits lots, puis une vérification confirme qu’aucune ligne ne diffère. L’application lit maintenant la nouvelle colonne, mais continue d’écrire dans les deux, si bien qu’un retour à la version précédente reste sûr. Puis elle cesse d’écrire dans l’ancienne colonne, et l’équipe attend qu’aucun retour arrière ne soit plus nécessaire. Enfin, la contraction : rendez la nouvelle colonne obligatoire et supprimez l’ancienne.
Voici ces six étapes déployables à la suite. Chacune fonctionne avec le code de l’étape précédente, et chacune peut faire l’objet d’un retour arrière. Remarquez que pendant les étapes un à cinq, les deux colonnes existent. Six déploiements pour un renommage, cela semble beaucoup, mais aucune mise en production n’exige d’interruption et aucune équipe n’a besoin de se coordonner. L’étape de contraction est celle que les équipes oublient. Suivez-la explicitement, sinon le schéma se remplit de colonnes à moitié migrées.
Les modifications de base de données ont leur place dans la gestion de versions, à côté du code, sous la forme d’une série ordonnée de petits scripts de migration. Des outils comme Flyway, Liquibase et Alembic les appliquent dans l’ordre, en test, en staging et en production. Ne modifiez jamais une migration qui a été appliquée où que ce soit ; écrivez-en une nouvelle. Et faites une répétition sur des données de la taille de la production, car une instruction qui prend cinquante millisecondes en test peut verrouiller une grande table pendant une heure. Les gros backfills s’exécutent comme des tâches séparées, par petits lots, en ralentissant quand les répliques prennent du retard, et peuvent être relancés sans risque. Pour les événements, un registre de schémas vérifie chaque nouvelle version par rapport à la règle de compatibilité et rejette un changement cassant avant qu’il n’atteigne la production. Donnez une valeur par défaut à tout champ que vous pourriez ajouter ou supprimer.
La section complète contient le SQL de chaque étape, davantage de détails sur les formats d’événements, et une liste de contrôle. Ensuite, dans la section sept point huit, nous verrons l’analytique et le streaming. À très vite.
Ce qu’il faut savoir
- Les schémas doivent changer alors que l’ancien et le nouveau code tournent côte à côte et que d’anciennes données existent encore ; chaque état de schéma déployé doit donc fonctionner avec au moins deux versions du code.
- La rétrocompatibilité signifie que les nouveaux lecteurs peuvent lire les anciennes données ; la compatibilité future, que les anciens lecteurs peuvent lire les nouvelles données ; la compatibilité totale permet de mettre à niveau lecteurs et rédacteurs dans n’importe quel ordre.
- L’expansion-contraction (expand and contract, ou changement parallèle) découpe un changement cassant en étapes compatibles : ajouter la nouvelle structure, pratiquer la double écriture et le remplissage rétroactif (backfill), basculer les lecteurs, cesser d’écrire l’ancienne structure, puis la supprimer.
- Les migrations sont des scripts versionnés conservés avec le code et appliqués automatiquement par des outils comme Flyway, Liquibase et Alembic ; ne modifiez jamais une migration déjà appliquée, et faites une répétition sur des données de la taille de la production.
- Les gros backfills s’exécutent comme des tâches séparées, par lots, bridées et idempotentes ; des techniques en ligne comme la construction concurrente d’index et les contraintes NOT VALID évitent les verrous prolongés.
- Les registres de schémas font respecter les règles de compatibilité des schémas d’événements ; Avro s’appuie sur les valeurs par défaut et sur la résolution entre schéma du rédacteur et schéma du lecteur, Protocol Buffers sur le fait de ne jamais réutiliser un numéro de champ.
Pourquoi les schémas doivent évoluer sans tout arrêter
Toute donnée stockée ou transmise a un schéma : les tables et les types de colonnes d’une base de données, les champs d’une réponse d’API JSON, la définition d’enregistrement d’un événement dans un flux. Les schémas changent sans cesse, parce que l’activité change : la banque ajoute un second titulaire de compte, la plateforme de livraison de repas commence à enregistrer les pourboires, un régulateur exige un nouveau champ. La question architecturale est de savoir comment modifier un schéma pendant que le système continue de fonctionner, alors que l’ancienne forme est encore utilisée par du code, par des données stockées et par d’autres équipes.
Trois faits rendent cela difficile. Premièrement, dans tout système déployé sans interruption de service, l’ancienne et la nouvelle version du code tournent côte à côte : un déploiement progressif (rolling deployment) remplace les instances une par une, un déploiement canari envoie une petite part du trafic vers la nouvelle version, et un retour arrière (rollback) ramène l’ancienne version. Chaque état du schéma doit donc fonctionner avec au moins deux versions du code. Deuxièmement, les données survivent au code. Les lignes écrites il y a cinq ans, les événements d’un journal conservé indéfiniment, les messages en attente dans une file et les sauvegardes gardent tous l’ancienne forme. Troisièmement, les schémas sont partagés : une colonne est lue par des rapports, des tâches d’extraction et parfois d’autres services, et un schéma d’événement est lu par des consommateurs dont le producteur ignore parfois jusqu’à l’existence. La section 7.1 plaidait pour que les données soient détenues derrière une interface ; c’est dans l’évolution des schémas que cette discipline porte ses fruits, car un schéma privé peut être modifié avec l’accord d’une seule équipe.
Rétrocompatibilité, compatibilité future et compatibilité totale
La compatibilité se définit en termes de lecteurs et de rédacteurs. Un changement est rétrocompatible lorsque du code utilisant le nouveau schéma peut lire des données écrites avec l’ancien. Ajouter un champ facultatif doté d’une valeur par défaut est rétrocompatible : le nouveau lecteur fournit la valeur par défaut lorsque le champ manque dans les anciennes données. Un changement offre une compatibilité future (forward compatibility) lorsque du code utilisant l’ancien schéma peut lire des données écrites avec le nouveau. Cela exige que les anciens lecteurs ignorent les champs qu’ils ne connaissent pas, ce qui est la discipline du lecteur tolérant (tolerant reader). Un changement est totalement compatible lorsqu’il est les deux à la fois, ce qui signifie que lecteurs et rédacteurs peuvent être mis à niveau dans n’importe quel ordre.
Le sens dont vous avez besoin découle de qui se met à niveau en premier. Si les consommateurs sont mis à niveau avant les producteurs (cas fréquent lorsqu’un flux a quelques consommateurs que vous contrôlez), il vous faut la rétrocompatibilité. Si ce sont les producteurs qui sont mis à niveau en premier, ce qui est le cas habituel pour une base de données (le nouveau code commence à écrire avant que chaque lecteur ait été redéployé) et pour les API publiques, il vous faut la compatibilité future. Lorsque vous ne pouvez pas contrôler l’ordre, ou devez pouvoir revenir en arrière d’un côté comme de l’autre, visez la compatibilité totale. Confluent Schema Registry, par exemple, applique par défaut une rétrocompatibilité vérifiée par rapport à la version précédente, et propose des variantes transitives qui vérifient par rapport à toutes les versions antérieures, ce qui compte lorsque des consommateurs peuvent rejouer d’anciennes données depuis le début d’un journal.
Un mot d’avertissement sur le vocabulaire. En conception d’API, rétrocompatible
s’emploie souvent de façon approximative pour dire les clients existants continuent de fonctionner
, ce qui, en termes de lecteurs et de rédacteurs, correspond à la compatibilité future pour les clients qui lisent vos réponses et à la rétrocompatibilité pour les requêtes que vous lisez de leur part. Lorsqu’une discussion repose sur ce terme, demandez quel côté change et quel côté doit continuer de fonctionner.
Quel lecteur peut lire quelles données
Changements courants et leur compatibilité
| Changement | Rétrocompatibilité (le neuf lit l’ancien) | Compatibilité future (l’ancien lit le neuf) | Approche sûre |
|---|---|---|---|
| Ajouter un champ facultatif ou une colonne nullable avec une valeur par défaut | Oui | Oui, si les lecteurs ignorent les champs inconnus | Le changement sûr par excellence |
| Ajouter un champ obligatoire sans valeur par défaut | Non : les anciennes données ne l’ont pas | Oui | L’ajouter en facultatif, faire le backfill, puis l’imposer |
| Supprimer un champ | Oui | Seulement si les anciens lecteurs ont une valeur par défaut pour ce champ | Cesser d’abord de le lire partout, puis le supprimer |
| Renommer un champ ou une colonne | Non | Non | Expansion-contraction : ajouter le nouveau, copier, basculer, supprimer l’ancien |
| Changer un type (texte en entier, int en long) | Dépend du format : élargir est souvent sûr, restreindre ne l’est pas | Cela dépend | Ajouter un nouveau champ du nouveau type et migrer |
| Changer le sens sans changer la forme (des pence aux livres) | Faux, sans que rien ne le signale | Faux, sans que rien ne le signale | Jamais : créer un nouveau champ sous un nouveau nom |
Expansion-contraction : la technique de base
La plupart des changements cassants peuvent être découpés en une suite de changements compatibles. Ce patron s’appelle expansion-contraction (expand and contract), ou changement parallèle (la description qu’en a donnée Danilo Sato en 2014 sur martinfowler.com, sous le nom de parallel change, nomme les phases expand, migrate et contract). D’abord l’expansion : ajoutez la nouvelle structure à côté de l’ancienne, de sorte que l’ancien comme le nouveau code fonctionnent. Ensuite la migration : faites en sorte que le code qui écrit maintienne les deux, copiez les données existantes, et faites passer les lecteurs à la nouvelle structure un par un. Enfin la contraction : lorsque plus rien n’utilise l’ancienne structure, supprimez-la. Chaque étape est déployée séparément, chacune est compatible avec la version qui la précède, et chacune peut faire l’objet d’un retour arrière sans perte de données.
Le prix à payer est du temps et un désordre temporaire : le changement s’étale sur plusieurs mises en production, et pendant des jours ou des semaines le schéma porte les deux formes. L’avantage est qu’aucune mise en production n’exige d’interruption de service, qu’aucun déploiement ne doit être coordonné entre équipes, et que chaque étape peut être mise en pause. Refactoring Databases (2006), de Scott Ambler et Pramod Sadalage, répertorie des dizaines de refactorisations de bases de données fondées sur la même idée, avec une période de transition pendant laquelle l’ancien et le nouveau schéma coexistent, maintenus en phase par l’application ou par des déclencheurs (triggers).
Exemple commenté
Exemple commenté : renommer une colonne sans interruption de service
La table restaurants de la plateforme de livraison de repas comporte une colonne postcode. La plateforme s’étend à des pays qui utilisent d’autres formats postaux, et l’équipe veut l’appeler postal_code. La table compte 4 millions de lignes, le service Restaurants est déployé plusieurs fois par jour par mises à jour progressives, et une tâche d’extraction nocturne lit aussi la colonne. Un simple ALTER TABLE ... RENAME COLUMN casserait toutes les instances en cours de l’ancien code à l’instant même de son commit.
| Version 1 : expansion | La migration ajoute postal_code text NULL. Depuis PostgreSQL 11, ajouter une colonne sans valeur par défaut ou avec une valeur par défaut constante est un changement qui ne touche que les métadonnées. L’ancien code ignore la nouvelle colonne ; rien ne casse. |
|---|---|
| Version 2 : double écriture | L’application écrit la même valeur dans les deux colonnes à chaque insertion et mise à jour, et lit toujours postcode. (Un déclencheur de base de données qui copie une colonne dans l’autre est une solution de rechange lorsque plusieurs applications écrivent dans la table.) |
| Étape 3 : backfill | Une tâche d’arrière-plan copie postcode dans postal_code pour les lignes existantes, par lots de 5 000 selon des plages de clé primaire, en marquant une pause entre les lots et en surveillant le retard de réplication. Une requête de vérification compte ensuite les lignes où les deux valeurs diffèrent : elle doit renvoyer 0. |
| Version 4 : bascule des lectures | L’application lit postal_code mais écrit toujours dans les deux colonnes, si bien qu’un retour à la version 2 ou 3 reste sûr. La tâche d’extraction est modifiée pour lire elle aussi la nouvelle colonne. |
| Version 5 : arrêt de l’écriture dans l’ancienne colonne | L’application n’écrit plus que dans postal_code. L’ancienne colonne ne doit plus être NOT NULL à ce stade, sinon les insertions échouent. Attendez assez longtemps pour être sûr qu’aucun retour à la version 4 ne sera nécessaire. |
| Version 6 : contraction | La migration impose NOT NULL sur postal_code (à l’aide d’une contrainte de vérification NOT VALID, validée séparément, que PostgreSQL 12 et les versions ultérieures savent utiliser pour poser NOT NULL sans long parcours bloquant) et supprime enfin postcode. |
Six déploiements pour un seul renommage, cela semble excessif, mais des étapes peuvent être combinées lorsque l’équipe contrôle chaque lecteur et accepte une courte fenêtre de retour arrière ; le principe qui compte est que chaque état déployé fonctionne avec le code d’avant et d’après.
L’étape de contraction est celle que les équipes oublient. Suivez-la explicitement (un ticket, un statut d’ADR, une règle de lint qui signale l’ancienne colonne), sinon le schéma se remplit de colonnes à moitié migrées.
Le renommage en six étapes déployables
-- Migration V41 (expand): metadata-only change, no table rewrite in PostgreSQL 11+
SET lock_timeout = '3s'; -- fail fast rather than queue behind long queries
ALTER TABLE restaurants ADD COLUMN postal_code text;
-- Backfill job (run repeatedly by a script, not inside one big migration)
UPDATE restaurants
SET postal_code = postcode
WHERE id >= :from_id AND id < :from_id + 5000 -- one small batch per transaction
AND postal_code IS DISTINCT FROM postcode;
-- Verify before switching reads
SELECT count(*) FROM restaurants WHERE postal_code IS DISTINCT FROM postcode; -- expect 0
-- Migration V44: enforce NOT NULL without a long blocking scan
ALTER TABLE restaurants ADD CONSTRAINT postal_code_nn CHECK (postal_code IS NOT NULL) NOT VALID;
ALTER TABLE restaurants VALIDATE CONSTRAINT postal_code_nn; -- scans, but allows writes
ALTER TABLE restaurants ALTER COLUMN postal_code SET NOT NULL; -- PostgreSQL 12+: uses the check, skips scan
ALTER TABLE restaurants DROP CONSTRAINT postal_code_nn;
-- Migration V45 (contract), weeks later, once nothing reads postcode
ALTER TABLE restaurants DROP COLUMN postcode;Les migrations en tant que code dans le pipeline de livraison
Les modifications de base de données ont leur place dans la gestion de versions, à côté du code de l’application, sous la forme d’une série ordonnée de migrations, chacune étant un petit script qui fait passer le schéma d’une version à la suivante. Evolutionary Database Design (publié pour la première fois en 2003, réécrit en 2016), de Pramod Sadalage et Martin Fowler, a posé cette pratique, et les outils sont mûrs. Flyway exécute dans l’ordre des fichiers SQL versionnés comme V41__add_postal_code.sql et consigne chacun dans une table flyway_schema_history. Liquibase décrit les changements sous forme de changesets en SQL, XML, YAML ou JSON, suivis dans une table DATABASECHANGELOG. Alembic, pour Python et SQLAlchemy, génère des scripts de révision comportant des fonctions upgrade() et downgrade() et enregistre la révision courante dans alembic_version. Flyway et Liquibase stockent aussi une somme de contrôle de chaque migration appliquée et s’arrêtent avec une erreur si l’une d’elles a été modifiée depuis, ce qui empêche les environnements de diverger en silence.
Dans un pipeline de livraison continue, les mêmes migrations s’exécutent sur une base de données vierge pendant les tests, puis en staging, puis en production, si bien que la production ne reçoit jamais un changement qui n’a pas été répété. Les bonnes pratiques ajoutent quelques règles. Ne modifiez jamais une migration qui a été appliquée où que ce soit ; écrivez-en une nouvelle. Placez les changements de schéma et le code qui en dépend dans des déploiements séparés, le changement de schéma en premier pour une expansion et en dernier pour une contraction. Testez les migrations sur une copie de données de la taille de la production, car une instruction qui prend 50 ms sur une base de test peut verrouiller une grande table pendant une heure. Préférez avancer avec une nouvelle migration plutôt que recourir à des migrations descendantes qui suppriment des colonnes, et les données avec elles. Enfin, laissez la base de données vous renseigner sur les verrous : dans PostgreSQL, un ALTER TABLE qui attend son verrou bloque toutes les requêtes arrivées après lui ; fixez donc un lock_timeout court et réessayez plutôt que de faire la queue derrière un rapport de longue durée.
Gros backfills et modifications de schéma en ligne
Un backfill (remplissage rétroactif) remplit une nouvelle colonne ou une nouvelle table pour les lignes existantes. Exécuté sous la forme d’un seul UPDATE portant sur 400 millions de transactions, il garde des verrous pendant des heures, génère un énorme volume de journal d’écriture anticipée (write-ahead log) que les répliques doivent rejouer (ce qui crée un retard de réplication), et laisse dans PostgreSQL des versions de lignes mortes que le vacuum doit nettoyer. L’approche professionnelle est une tâche séparée et redémarrable qui traite de petits lots par plages de clé primaire, valide (commit) chaque lot, enregistre sa progression, se bride elle-même lorsque le retard de réplication ou la charge de la base augmentent, et qui est idempotente, de sorte que relancer un lot ne cause aucun dégât. Exécutez-la en dehors de l’outil de migration, pour qu’un déploiement n’ait jamais à l’attendre.
Certains changements ne peuvent pas être faits sur place sans verrouillage. PostgreSQL propose CREATE INDEX CONCURRENTLY (qui ne peut pas s’exécuter dans un bloc de transaction, ce qu’il faut indiquer aux outils de migration) et la séquence NOT VALID puis VALIDATE CONSTRAINT présentée plus haut. Pour MySQL, des outils comme gh-ost et pt-online-schema-change construisent une copie fantôme de la table avec le nouveau schéma, la maintiennent en phase avec les écritures en cours et la mettent en place par un bref renommage. Pour changer le type d’une colonne, mieux vaut généralement une expansion-contraction vers une nouvelle colonne qu’un ALTER sur place, qui, dans la plupart des bases de données, réécrit toute la table.
Registres de schémas, Avro et Protocol Buffers
Pour les événements et les messages, l’équivalent d’un outil de migration est un registre de schémas. Les producteurs enregistrent chaque version de schéma ; le registre la vérifie par rapport à la règle de compatibilité configurée et rejette un changement cassant au moment du build ou du déploiement plutôt qu’en production. Chaque message porte un court identifiant de schéma, de sorte que les consommateurs peuvent récupérer le schéma exact avec lequel il a été écrit.
Apache Avro rend explicite le schéma du rédacteur : le décodage nécessite à la fois le schéma du rédacteur et celui du lecteur, et les règles de résolution d’Avro font correspondre les champs par leur nom, ignorent les champs du rédacteur que le lecteur ne connaît pas, et remplissent les champs du lecteur absents des données avec leurs valeurs par défaut déclarées. D’où la règle pratique : donnez une valeur par défaut à tout champ que vous pourriez ajouter ou supprimer. Dans l’exemple ci-dessous, currency et tip_pence ont été ajoutés dans des versions ultérieures avec des valeurs par défaut, si bien que les anciens événements se décodent toujours avec les nouveaux consommateurs.
Sur le réseau, Protocol Buffers identifie les champs par leur numéro, et non par leur nom. Ajouter des champs est sûr, et les anciens lecteurs conservent les champs inconnus lors d’une nouvelle sérialisation. Renommer un champ n’affecte pas l’encodage binaire (même si cela change la forme JSON). Les règles cardinales sont de ne jamais changer ni réutiliser un numéro de champ, et de marquer les numéros et les noms des champs supprimés comme reserved afin que personne ne les réutilise par accident. Confluent recommande la rétrocompatibilité transitive pour Protobuf, car l’ajout d’un nouveau type de message n’assure pas la compatibilité future. JSON Schema peut aussi être enregistré et vérifié, mais l’ouverture de JSON (une propriété inconnue est-elle autorisée ou non ?) rend les règles de compatibilité plus subtiles.
{
"type": "record",
"name": "OrderPlaced",
"namespace": "food.orders.v1",
"fields": [
{"name": "order_id", "type": "string"},
{"name": "restaurant_id", "type": "string"},
{"name": "total_pence", "type": "long"},
{"name": "currency", "type": "string", "default": "GBP"},
{"name": "tip_pence", "type": ["null", "long"], "default": null}
]
}Faire évoluer les schémas d’API et d’événements
Les API et les événements suivent les mêmes principes avec des outils différents. Pour les API, privilégiez le changement additif, exigez que les clients soient des lecteurs tolérants, et réservez les changements cassants à une nouvelle version exécutée à côté de l’ancienne, avec une supervision de l’usage pour savoir quand l’ancienne version peut être retirée (la section 6.5 présente les approches de versionnage). Les tests de contrat pilotés par le consommateur permettent à chaque consommateur de publier les champs dont il dépend réellement, si bien que le fournisseur sait quels changements sont sûrs.
Les événements sont plus difficiles que les API parce qu’ils sont stockés : un consommateur peut rejouer un topic depuis le début, et un système fondé sur l’event sourcing (section 7.5) reconstruit son état à partir d’événements écrits il y a des années. Traitez un schéma d’événement publié comme un contrat public, versionnez-le dans le registre et, lorsqu’un changement est réellement cassant, publiez pendant un certain temps un nouveau type d’événement ou un nouveau topic (OrderPlaced.v2) à côté de l’ancien plutôt que de modifier l’existant. La section 6.4 traite plus généralement de la conception des événements.
Liste de contrôle : avant de livrer un changement de schéma
- Le changement est-il additif ?Sinon, découpez-le en étapes d’expansion, de migration et de contraction, chacune déployable seule.
- Chaque état déployé fonctionne-t-il avec la version précédente et la version suivante du code ?Avec les déploiements progressifs et les retours arrière, deux versions tournent toujours ensemble.
- Qui d’autre lit ces données ?Recherchez les rapports, les tâches d’extraction, les autres services et les consommateurs d’événements ; vérifiez les abonnés du registre et le lignage dans le catalogue de données.
- La migration a-t-elle été exécutée sur des données de la taille de la production ?Mesurez la durée des verrous, les réécritures de tables et le volume de journal ; utilisez la construction concurrente d’index et les contraintes NOT VALID lorsqu’elles sont disponibles.
- Un délai d’expiration de verrou est-il défini ?Échouez vite et réessayez plutôt que de laisser un ALTER en attente bloquer tout le trafic.
- Le backfill est-il séparé, par lots, bridé, idempotent et reprenable ?Il ne doit jamais bloquer un déploiement ni submerger les répliques.
- Les nouveaux champs ont-ils des valeurs par défaut, et aucun numéro ni nom de champ n’est-il réutilisé ?Indispensable pour Avro et Protocol Buffers ; imposé par la vérification de compatibilité du registre.
- Le sens d’un champ a-t-il changé ?Un changement d’unité, de devise ou de sémantique exige un nouveau nom de champ, jamais un nom réutilisé.
- L’étape de contraction est-elle suivie ?Ouvrez un ticket ou un ADR pour que l’ancienne structure soit réellement supprimée une fois inutilisée.
Vérifiez votre compréhension
Une équipe ajoute un champ obligatoire sans valeur par défaut à un schéma Avro. Quelle compatibilité cela casse-t-il, et pourquoi ?
La rétrocompatibilité : un consommateur qui utilise le nouveau schéma pour lire d’anciennes données constate que le champ manque et n’a aucune valeur par défaut pour le remplir.
Pourquoi l’étape de double écriture se poursuit-elle après la bascule des lectures vers la nouvelle colonne ?
Pour qu’un retour à la version précédente, qui lit encore l’ancienne colonne, trouve cette colonne à jour.
Pourquoi un gros backfill ne doit-il pas être écrit sous la forme d’une seule instruction UPDATE dans une migration ?
Une énorme transaction garde des verrous longtemps, génère une rafale de journal qui crée un retard de réplication, laisse beaucoup de nettoyage, ne peut pas être reprise et bloque le déploiement ; de petits lots validés un par un évitent tout cela.
Qu’empêche le fait de marquer un numéro de champ Protocol Buffers comme reserved ?
Il empêche quiconque de réutiliser le numéro d’un champ supprimé pour un nouveau champ, ce qui ferait décoder d’anciennes données comme le mauvais champ.
Sources et lectures complémentaires
Une question sur cette section ? ArchitectBot, l’assistant d’étude IA du cours, répond d’abord à partir du cours.
Demander : « Nous devons scinder la colonne du nom de client en prénom et nom, sur une table de 50 millions de lignes, sans interruption de service. Quelles étapes et quelles versions prévoiriez-vous ? »