Riadh Mnasri
← Retour au blog
5 min de lecture

Schema registry : versionner un message Kafka sans casser les consommateurs existants

Sur MissionMatch, plusieurs consommateurs indépendants lisent les mêmes événements Kafka, comme décrit dans l'article sur le découplage event-driven. Ce que cet article-là laisse de côté, c'est une question qui finit toujours par se poser : que se passe-t-il le jour où il faut changer la forme du message lui-même ?

Le contrat qu'on oublie parce qu'il est implicite

Un topic Kafka ne dit rien sur la structure de ses messages. Producteur et consommateurs s'accordent tacitement sur un format, souvent documenté nulle part ailleurs que dans le code du producteur. Renommer un champ, changer son type, ou en supprimer un qu'un consommateur lisait encore : rien n'empêche techniquement le producteur de le faire, et rien ne prévient les consommateurs avant que ça casse en production, généralement au pire moment.

Un schema registry (Confluent Schema Registry, ou l'équivalent AWS Glue Schema Registry) rend ce contrat explicite : chaque schéma (souvent en Avro, parfois Protobuf ou JSON Schema) est enregistré, versionné, et associé à un subject (typiquement <topic>-value). Le producteur ne peut plus publier un message dont le schéma n'a pas été validé contre les règles de compatibilité définies pour ce subject.

Sur MissionMatch, les événements sont aujourd'hui sérialisés en JSON brut (JsonSerializer/JsonDeserializer de Spring Kafka), sans schema registry devant le topic : exactement le point aveugle que cet article explore. Le projet fonctionne, parce qu'un seul dépôt de code gère producteurs et consommateurs à la fois, donc un changement de format est visible immédiatement à la compilation. Ce filet de sécurité disparaît dès que des équipes ou des dépôts séparés possèdent chacun un bout de la chaîne : c'est précisément le moment où un schema registry cesse d'être une option.

  Producteur                Schema Registry              Topic Kafka
 ┌──────────┐   1. valider  ┌────────────────┐            ┌──────────┐
 │ nouveau   │──────────────▶│ schéma v3       │            │ message  │
 │ message   │◀──────────────│ compatible avec │───────────▶│ + id     │
 └──────────┘  2. schema id  │ v1 et v2 ?      │  3. publier│ schéma   │
                             └────────────────┘            └──────────┘
                                     ▲
                                     │ 4. récupérer le schéma par id
                                     │
                              ┌──────────────┐
                              │ Consommateur  │
                              └──────────────┘

Le consommateur ne reçoit jamais le schéma complet dans le message : juste un identifiant compact, qu'il utilise pour aller chercher le schéma exact auprès du registry. C'est ce qui permet de faire évoluer des messages sans alourdir chaque message d'un schéma redondant.

Les quatre modes de compatibilité, et quand choisir lequel

Le registry n'empêche pas les changements : il les autorise ou les refuse selon une règle de compatibilité configurée par subject, vérifiée automatiquement à chaque tentative d'enregistrement d'un nouveau schéma.

ModeCe qu'il garantitCas d'usage typique
BACKWARDLes consommateurs qui utilisent le nouveau schéma peuvent lire les messages écrits avec l'ancienAjouter un champ optionnel, le cas le plus fréquent
FORWARDLes consommateurs qui utilisent l'ancien schéma peuvent lire les messages écrits avec le nouveauSupprimer un champ optionnel que personne ne lisait plus
FULLLes deux garanties à la fois (BACKWARD et FORWARD)Topics critiques, avec beaucoup de consommateurs qu'on ne contrôle pas tous
NONEAucune vérificationÀ éviter en dehors d'un prototype jetable

BACKWARD est le mode par défaut le plus courant, parce que le cas le plus fréquent en pratique est justement celui-là : un consommateur redéployé récemment doit continuer à lire les messages plus anciens encore présents dans le topic (Kafka conserve l'historique, contrairement à une file de messages classique).

Un exemple concret : ce qui casse, ce qui ne casse pas

Prenons un schéma inspiré du vrai événement MissionPublishedIntegrationEvent de MissionMatch, qui transporte aujourd'hui un identifiant, les compétences requises, un taux journalier et une date de début :

// Schéma v1
{
  "type": "record",
  "name": "MissionPublished",
  "fields": [
    { "name": "missionId", "type": "string" },
    { "name": "requiredSkills", "type": { "type": "array", "items": "string" } },
    { "name": "dailyRateAmount", "type": "string" },
    { "name": "startDate", "type": "string" }
  ]
}

Ajouter un champ optionnel avec une valeur par défaut est compatible BACKWARD : un consommateur qui lit un vieux message avec le nouveau schéma utilisera simplement la valeur par défaut pour le champ absent.

// Schéma v2 : compatible BACKWARD
{
  "type": "record",
  "name": "MissionPublished",
  "fields": [
    { "name": "missionId", "type": "string" },
    { "name": "requiredSkills", "type": { "type": "array", "items": "string" } },
    { "name": "dailyRateAmount", "type": "string" },
    { "name": "startDate", "type": "string" },
    { "name": "durationInDays", "type": ["null", "int"], "default": null }
  ]
}

En revanche, renommer missionId en id, ou changer son type de string à long, casse la compatibilité : ce ne sont pas des évolutions, ce sont des schémas différents du point de vue du registry, qui refusera l'enregistrement si le mode configuré l'exige.

Ce que le schema registry ne fait pas

Il vérifie la compatibilité structurelle du schéma, pas la compatibilité sémantique. Un champ status qui passait de "active" à "draft" reste structurellement valide (c'est toujours une chaîne de caractères), mais un consommateur qui ne connaît pas encore la nouvelle valeur possible peut malgré tout se comporter de façon incorrecte. Le registry protège contre les changements de forme, pas contre les changements de sens : les tests de contrat entre producteur et consommateurs restent nécessaires en complément, pas en remplacement.

Pourquoi ça vaut le coût avant même d'en avoir besoin

La tentation, en solo ou en petite équipe, est de repousser le schema registry à « quand on aura plusieurs équipes ». Le problème, c'est que le coût de son absence ne se voit pas avant le premier changement de schéma en production avec des consommateurs déjà déployés séparément : à ce moment-là, il est trop tard pour l'ajouter sans risque, puisque le passé du topic contient déjà des messages dans l'ancien format. La bonne discipline est de l'introduire dès que deux dépôts de code distincts touchent le même topic, pas après.