Blog Zenika

#CodeTheWorld

ArchitectureCrafting Software

Migration d'un legacy avec GoReplay

Remplacer un composant critique d’un SI sans adopter une démarche incrémentale est une stratégie risquée. Mais il arrive que la mise en oeuvre d’un tel projet soit également inéluctable : lorsque le fonctionnel est complexe, mal maîtrisé, et que le code existant est mal testé et qu’il freine depuis trop longtemps l’implémentation de nouvelles fonctionnalités métier.

everything is going to be ok ...
everything is going to be ok …

Une fois la nécessité d’un big bang acceptée, du moins sur le principe, il va falloir mettre en place une procédure permettant de garantir, autant que possible, que la mise en production de votre nouvelle application se passera sereinement. C’est là l’objet de cet article. La procédure que nous allons voir a été utilisée avec succès pour migrer un composant majeur du système d’information de Libon. Il s’agissait de l’application backend responsable de la gestion des crédits de communication des utilisateurs ainsi que des autorisations d’appels.

Note: à partir de maintenant, nous appellerons legacy le composant legacy et experimental son remplacant.

 
Un impératif : ne vous inspirez pas de l’existant, reproduisez le !
Pourquoi reproduire l’existant à l’identique ?

  • pour pouvoir mettre legacy ou experimental en production de manière transparente pour les autres composants de votre SI
  • pour faciliter la mise en place d’une phase de double-run (nous en parlerons un peu plus tard).
  • dans un contexte où la connaissance du domaine s’est perdue, vous ne voulez pas jouer à la roulette russe avec les règles métier.

Vous n’êtes pas satisfait de l’API d’origine ? Ravalez votre frustration et reprenez la telle quelle ! Les règles métier sont inutilement complexes ? Faîtes avec ! On ne parle pas de dette technique pour rien, et vous allez devoir la trimbaler pendant encore un moment…
Les seuls éléments que vous pouvez modifier sont ceux qui pourront également l’être sur legacy. Mais ne comptez pas trop là dessus : si vous pouviez facilement corriger votre legacy vous ne seriez sans doute pas en train de tout réécrire. En revanche, vous êtes libre de modéliser votre nouvelle base de données comme bon vous semble. Une fois que experimental sera en production vous aurez tout le loisir de modifier règles métier et APIs.
 
Migration des données de production.
Dans le cas où vous décidez de changer votre modèle de données, vous allez devoir mettre en place un batch permettant de migrer les données de l’ancien modèle vers le nouveau ainsi que de valider cette migration. Avant tout, il est probable que legacy ait connu des mises au point difficiles et que des traces de ces accidents soient présents dans sa base de données. C’est le moment de partir à la recherche de ces incohérences et de les corriger en production. Toujours dans l’optique de faciliter l’étape de migration, n’hésitez pas à supprimer toutes données qui ne seraient plus utilisées. Pour prévenir toute erreur de manipulation, pensez à sauvegarder dans des tables temporaires tout ce que vous serez amené à supprimer ou à modifier.
N’oubliez pas que le jour J vous devrez arrêter tous les services concernés le temps de la migration. Demandez donc à votre ops préféré de mettre à votre disposition une machine de compétition qui vous permettra d’exécuter vos batchs de migration et de validation aussi vite que possible. Plus vous aurez de CPU et de RAM, plus vous pourrez paralléliser et limiter vos I/Os.
Idéalement, le code de validation des données ne devrait pas être écrit par le groupe responsable du code de migration. Ces deux groupes doivent travailler isolément pour éviter que des erreurs de compréhension du modèle de données de legacy ne se répandent dans les deux batchs par simple contagion.
 
Validation fonctionnelle.
Vous avez lancé la première release de experimental et vous vous êtes assuré de son bon fonctionnement à l’aide de nombreux tests. Ce n’est toutefois pas suffisant : certaines configurations de données, certains comportements à la limite, vous auront sans doute échappé. Comment s’assurer qu’aucun bug majeur ne sera découvert après la mise en production ? En amenant la production à votre application grâce à GoReplay.
GoReplay est une application open-source écrite en Go et dont le rôle est d’écouter afin de dupliquer un trafic réseau. Pour ce faire, GoReplay utilise libpcap : il s’agit de la librairie qui est utilisée par tcpdump et Wireshark pour capturer des paquets de données. Cette opération se déroule donc de manière transparente : vous n’avez pas besoin de configurer quoi que ce soit sur votre serveur de production.
En plus de rejouer des requêtes, GoReplay vous permet également de les modifier avant envoi ainsi que de comparer les réponses des serveurs ciblés. Les possibilités sont en réalité plus nombreuses grâce au système d’extension proposé (middleware, dans la terminologie GoReplay). Il n’existe en revanche pas d’extension prête à l’usage, vous devrez donc dédier du temps à implémenter le comportement désiré.

fonctionnement d'un middleware GoReplay
fonctionnement d’un middleware GoReplay

Nous allons utiliser les différentes possibilités de GoReplay pour mettre en place notre processus de validation métier. Pour cela nous avons besoin d’une instance de legacy et de experimental dans un environnement de validation dédié.
 
environnement
Et voici dans le détail le processus que vous pouvez mettre en place :
validation

  1. Exécutez un dump de la base de données legacy de production.
  2. Dans le même temps, lancez GoReplay afin d’enregistrer dans un fichier de log les requêtes de production.
  3. Chargez le dump dans la base de données legacy de validation.
  4. Migrez ces données vers la base de données de experimental. Assurez-vous que ces données ont été migrées correctement à l’aide du batch de validation dont nous parlions plus haut.
  5. Lancez GoReplay pour rejouer les requêtes de production sur legacy et experimental. Utilisez la fonctionnalité middleware de GoReplay pour comparer les réponses de vos deux serveurs. Elles doivent être identiques. Si votre environnement de validation ne tient pas la charge, GoReplay vous permet de réduire la fréquence avec laquelle il jouera vos requêtes.

Dans la mesure où les étapes 1 et 2 ne peuvent pas être tout à fait synchronisées, il est possible que le résultat des premières requêtes que vous enregistrez (et donc que vous allez rejouer) soit déjà présentes dans le dump que vous allez extraire de votre base de données de production. Cela ne devrait pas poser de problème : la création d’un utilisateur pourtant déjà présent en base devrait échouer de la même manière sur legacy et experimental. C’est la seule chose qui importe.
 
Gestion des conflits entre legacy et experimental.
Même si vous vous êtes efforcé de diverger le moins possible du comportement de legacy, il est très probable que vous deviez gérer de nombreux edge-case dans votre middleware. Ainsi, d’éventuels timestamps générés par legacy et experimental ne seront pas identiques à la milliseconde près. Dans le cas où ils apparaîtraient dans vos payloads, votre middleware devra tolérer de tels écarts.

validation-date
un conflit mineur lors d’une validation fonctionnelle

Un autre problème concerne les identifiants techniques :

  • dans le cas où les identifiants des ressources créées sont générés aléatoirement vous n’aurez aucune correspondance entre legacy et experimental. Vous n’aurez non plus de correspondance avec les identifiants générés en production. Cela signifie que certaines des requêtes que vous allez rejouer cibleront des identifiants inconnus par legacy et experimental. Ce n’est en soit pas un problème, du moment que legacy et experimental réagissent de la même manière (code 404 par exemple).
  • les choses sont plus compliquées dans le cas des identifiants auto-incrémentés : même si vos séquences sont répliquées sur legacy et experimental, vous risquez de voir un décalage se produire avec le temps pour peu qu’une création de ressource échoue sur l’une ou l’autre des applications. En conséquence, les identifiants générés en production et présents dans les requêtes de rejeu ne cibleront pas les même ressources sur legacy et experimental. Ici, une des solution peut être d’initialiser vos séquences de manière à être certain que l’intersection entre les valeurs générées par la production, par legacy et par experimental soit vide (on se retrouve alors dans une situation similaire à celle où les identifiants sont générés aléatoirement).
  • dans tous les cas, une différence entre les identifiants générés en production et en validation risque de diminuer considérablement l’intérêt d’un double-run sur le long terme puisque toutes les requêtes ciblant ces ressources ne pourront pas aboutir.

mistmatch_id_prodmistmatch_id_validation
Une solution idéale consisterait à réaliser un mapping au sein du middleware GoReplay entre les identifiants de production, de legacy et de experimental. Il faudrait alors pouvoir, lors du rejeu, modifier les requêtes de manière à insérer le bon identifiant pour chaque plateforme. A ce jour, une telle fonctionnalité n’a pas encore été implémentée dans GoReplay.
Enfin, même des temps de réponses différents peuvent suffire à provoquer des divergences. Ainsi, si la durée de création d’une ressource prend plus de temps sur experimental que sur legacy, et que cette requête de création est immédiatement suivie pour un appel à GET sur la même ressource, vous risquez de générer une erreur sur experimental.
validation-ordre
Ici, une solution simple consiste à configurer GoReplay de manière à ce qu’il augmente artificiellement la latence entre deux requêtes successives.
De prime abord, on pourrait croire que l’ensemble de ces différences entre les réponses de legacy et de experimental sont mineures et qu’il est donc inutile de passer du temps à les aplanir en complexifiant le code de validation. Le problème est que, aussi mineures soient elles, elles risquent de polluer vos logs et de masquer de véritables bugs. Pour s’en convaincre, il suffit de faire le calcul suivant : considérez 100.000 requêtes enregistrées au cours d’une seule journée et un taux d’échec de 0.5% lors du rejeu dû à des raisons purement techniques (identifiants autogénérés différents, écart d’une seconde entre deux timestamps etc), c’est 500 triplets (requêtes, réponse de legacy, réponse de exprimental) qui apparaîtront dans vos logs d’erreurs et que vous devrez analyser un par un pour déterminer s’ils sont ou non liés à des bugs dans experimental. Ce n’est pas une situation acceptable.
 
Validation technique.
Vous êtes désormais confiant dans le code de experimental. Il vous reste à prouver que l’application tiendra la charge une fois en production. On pourrait croire que cette preuve a déjà été apportée lors de la phase de validation fonctionnelle, mais souvenez vous que nous testions alors une instance de experimental au sein d’un environnement isolé. Ici l’idée est de procéder à une validation technique dans des conditions identiques à celles de la production (même hardware, même nombre d’instances, même base de données).
Pour cela vous pouvez encore tirer profit des fonctionnalités de GoReplay : déployez experimental en production et configurez GoReplay pour dupliquer toutes les requêtes en temps réel. L’application legacy fait toujours foi pour vos utilisateurs et vous n’avez pas besoin de comparer les réponses cette fois-ci. Vous devez juste vous assurer qu’après plusieurs jours sous le régime de la production experimental ne souffre pas de fuites mémoire ou de ralentissements liés à un garbage collector mal optimisé (gceasy est votre ami). En conséquence vous n’avez pas besoin d’arrêter vos services de production le temps de migrer vos données : certaines requêtes ne pourront pas être traitées par experimental mais cela importe peu.
Chez Libon, cette phase de validation technique n’a pas été superflue : elle a mis en évidence des problèmes de garbage collector que nous avons résolu en retravaillant la modélisation de nos event (nous utilisons un architecture event-sourced) ainsi que les algorithmes d’invalidation de nos caches applicatifs.
 
Épilogue
Les nombreuses phases de validation fonctionnelle et technique qui ont précédé la mise en production ont permis de rassurer les équipes sur la stabilité du code ainsi que de les familiariser avec la procédure de migration des données.
Mises à part les 30 minutes de downtime nécessaires à la migration des données, nous avons pu remplacer un composant legacy majeur de notre SI en toute transparence pour nos clients. Un seul bug mineur a été détecté deux semaines après la mise en production de experimental. Il s’agissait d’une opération d’administration qui n’avait jamais été effectuée au cours des phases de double-run…

Une réflexion sur “Migration d'un legacy avec GoReplay

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

En savoir plus sur Blog Zenika

Abonnez-vous pour poursuivre la lecture et avoir accès à l’ensemble des archives.

Continue reading