Quand l’oubli des fondamentaux du code finit par coûter cher

J’utilise MongoDB comme base de données principale et j’en suis globalement satisfait. Mais j’ai l’impression que depuis un certain temps la tendance consiste à le dénigrer. Souhaitant comprendre ce qu’on lui reproche réellement, je suis à l’affût de retours d’expériences. C’est ainsi que j’ai récemment lu cet article publié le 21 novembre 2017 : Why We Moved From NoSQL MongoDB to PostgreSQL.

Je n’ai malheureusement pas appris grand chose sur MongoDB en le lisant, mais je trouve que cet article est un cas d’école qui nous montre ce qu’il se passe quand on s’assoie sur les bonnes pratiques de développement. Je vous propose donc dans cet article d’étudier ça plus en profondeur.

Résumé de l’article

L’auteur nous dit en préambule que leur base de code est écrite en JavaScript pour utiliser la même technologie côté serveur et côté client.

Il nous explique ensuite qu’il dispose d’une base de données qui contient 4 To de données (ça commence à faire pas mal !) et qu’ils ont rencontré quelques problèmes dont certains ont conduit à une indisponibilité de plusieurs heures de la base de données et donc du service. Ils ont également eu quelque fois des temps de réponse élevés sans vraiment comprendre pourquoi. C’est en effet embêtant, et pour le coup c’est certainement quelque chose qu’on peut reprocher à MongoDB (à condition qu’il soit bien utilisé tout de même).

L’auteur nous explique ensuite la raison qui les a vraiment conduits à migrer vers PostgreSQL. Comme dans beaucoup de produits qui vivent (et donc évoluent), la structure des données change avec le temps. Certains champs ont ainsi été ajoutés à des objets au cours du temps. La base de données contenait des objets en version 1, en version 2 avec un champ supplémentaire, en version 3 avec d’autres nouveaux champs et ainsi de suite. Comme il nous l’explique, il était nécessaire à chaque endroit où les objets sont utilisés de vérifier que le champ est présent avant de le lire parce que ce n’est pas toujours le cas. Il y a de nombreuses utilisations de ces champs qui sont faites sans procéder à la vérification de leur existence et ça crée de nombreux bugs qui ont des conséquences graves. Il estime d’ailleurs que 90% des bugs qu’ils ont rencontré dans leur application venaient de là.

Le coup fatal a été donné par l’introduction d’un nouveau champ obligatoire qui a nécessité une migration des données (rajouter ce champ à tous les objets). La requête pour mettre à niveau les données a pris plus de 4 heures pendant lesquelles l’application ne pouvait pas fonctionner, le champ n’étant pas présent dans tous les documents.

Il conclue son argumentaire en disant que les bases NoSQL ne convenaient pas à leur usage, et c’est la raison pour laquelle ils se sont tournés vers PostgreSQL.

Il nous explique ensuite rapidement en quoi PostgreSQL lui a permis de régler ses problèmes. Le principal argument mis en avant est le fait que PostgreSQL travaille avec des données structurées (à l’inverse de MongoDB) et que donc toutes les entrées d’une même table ont la même structure. La modification de la structure permet de rajouter les champs à toutes les lignes avec une valeur par défaut, et ce en plus de manière très peu pénalisante sur les performances du système.

Mon analyse de cet article

J’ai eu moi aussi à gérer à plusieurs reprises des changements de schéma dans la base, et ça ne s’est jamais vraiment mal passé. Et les quelques fois où j’ai eu des problèmes venaient de mauvaises manipulations de ma part. Certes, je n’ai jamais géré des bases de données aussi grosses (des téraoctets), mais pour moi les problèmes cités dans cet article ne relèvent pas du tout de MongoDB mais d’une bête erreur dans la structuration de leur code que je vais expliquer et qui s’est révélée être lourde de conséquences.

Un manque d’abstraction qui crée une forte adhérence à la base de données

L’auteur nous dit qu’il avait des problèmes partout dans son code parce que les objets (sous-entendu venant de la base de données) ne sont pas tous structurés de la même manière. Que la base de données contienne des données non homogènes, c’est une chose. Que ce soit visible partout dans le code en est une autre, et pour le coup c’est une lourde erreur. C’est ça et uniquement ça qui est à l’origine de toutes ces galères.

Il s’agit en fait d’un simple problème d’abstraction. Le code métier ne doit en aucun cas avoir connaissance et encore moins dépendre de la façon dont sont stockées les données. En fonction des besoins et du fonctionnement de la base de données, il est possible que la structure en base de données soit très différente de celle des objets métier. C’est à la partie qui gère l’accès aux données qu’incombe la responsabilité de faire cette gymnastique. Cette partie qui forme ce qu’on appelle la couche d’accès aux données (même si le modèle en couche est un peu désuet aujourd’hui) est composée d’objets permettant d’interagir avec la base de données pour chaque entité métier. On appelle couramment ces objets DAO (Data Access Object) ou repository dans le code.

Quand tous les appels à la base de données passent par une couche d’abstraction, celle-ci peut gérer les formes différentes des objets stockés en base et faire en sorte de les harmoniser pour que le reste du code manipule une structure unique. Si l’auteur de l’article a rencontré les problèmes qu’il décrit, il y a fort à parier qu’il n’a pas mis en place d’abstraction à ce niveau-là. Sinon, il aurait pu simplement corriger la cause de ses problèmes à un seul endroit et pas la conséquence un peu de partout comme il a dû le faire.

Si il n’y avait pas d’abstraction, cela signifie qu’à chaque fois qu’il y a besoin d’accéder à la base de données, le code métier fait lui-même appel à la base de données plutôt que de le déléguer à un objet dont ce serait la responsabilité (c’est malheureusement quelque chose qu’on voit souvent). Cela conduit bien souvent à des duplications de code dans la mesure où ce sont souvent les mêmes requêtes qui se répètent. Et, peut-être encore plus gênant, cela crée une forte dépendance entre le code métier et l’implémentation de la base de données. Et ça a dû justement poser problème lors de la migration vers PostgreSQL. Quand tout le code qui accède à la base de données est limité à la couche d’accès aux données, il « suffit » (à condition que les bases de données fonctionnent de manière assez similaire) de changer son implémentation et le tour est joué. Ce qui n’est pas le cas si il faut aller chercher partout dans le code. Et je ne vais pas rentrer davantage dans les détails sur cette question, mais comment teste-t-on du code métier qui contient des requêtes SQL ou MongoDB ?

Ne pas faire d’abstraction quand on a 1000 lignes de code ça peut passer. Quand la base de code grossit, ça devient plus problématique. Et ça demande un gros travail de refactoring d’introduire cette abstraction après coup, alors que ce n’est pas beaucoup plus cher de le faire au fur et à mesure.

Manque d’homogénéité dans la base de données

La structure de la base de données change forcément à un moment ou à un autre au fil des versions. C’est inévitable dans un logiciel qui évolue. Ce n’est jamais facile à gérer, mais il y a quand même des techniques pour se faciliter la vie.

Dans le cas de l’article, il me semble comprendre qu’ils ont dans leur base de données des objets en version 1, en version 2, en version 3 etc… Ce n’est pas parce que MongoDB permet de stocker des objets qui ont des structures différentes dans une même collection que c’est une bonne idée de le faire.

Même si on a vu qu’il était possible grâce à une couche d’abstraction de rendre transparent le fait que les objets ne soient pas tous structurés de la même façon, c’est encore mieux d’éviter d’avoir à le faire parce que ça ajoute beaucoup de complexité. Pour cela, il faut migrer les données (finalement ce qu’il nous dit qu’il fait avec PostgreSQL) pour convertir les anciens objets vers le nouveau format lors de chaque mise à jour qui change leur structure.

Je vois déjà venir ceux qui vont dire que c’est plus compliqué que ça en a l’air si on veut pouvoir faire la mise à jour sans coupure de service et avec retour en arrière possible. La technique de la migration de la base de données au moment de la mise à jour pose en effet plusieurs problèmes :

  • La base de données est dans un état incohérent pendant que la migration est en cours. Le service sera forcément impacté par cette situation.
  • Si on a plusieurs serveurs et qu’on souhaite les migrer à tour de rôle, quand exécute-t-on la migration ? L’ancienne version et la nouvelle version ne peuvent en réalité pas être déployés simultanément.
  • Si on a migré les données et que finalement on souhaite revenir en arrière, on ne peut pas, à moins d’avoir à sa disposition un script permettant de faire l’opposé de l’opération de migration, mais ce n’est pas forcément toujours possible.

Dans ce cas, il n’y a pas d’autre solution que d’accepter d’avoir des objets à l’ancien et au nouveau format dans la base et donc un code qui sait lire les deux formats. Je ne rentrerai pas dans les détails de ces techniques, mais on trouve des articles sur internet qui expliquent comment faire. Quoi qu’il en soit, cette étape nécessaire ne doit être que transitoire. Une fois que le déploiement de la nouvelle version est terminé, on peut mettre à niveau les vieux objets vers le nouveau format en tâche de fond. Cela permet de retirer le code qui convertit à la volée les anciens objets au nouveau format dans la couche d’accès aux données. Et puis cela simplifie également les requêtes qui sont faites à la base de données. Quand on a des données qui ne sont pas homogènes, il faut parfois les écrire différemment en fonction des versions, par exemple je veux que tel champ soit égal à cette valeur si il existe (nouveau champ) ou que tel autre champ (son ancien nom) soit égal à une autre valeur si le nouveau champ n’existe pas. Cela peut conduire à une grande complexité qu’il faut à tout prix éviter.

En résumé

Les raisons qui ont conduit l’auteur de cet article à quitter MongoDB ne sont pas liées à MongoDB mais à un manque de rigueur et d’organisation dans leur code. On peut d’ailleurs le remercier pour avoir partagé ce retour d’expérience et m’avoir donné l’occasion de réagir via cet article. On notera également qu’il a bien pris soin de dire que MongoDB n’était pas adapté à l’utilisation qu’il en faisait et de ne pas dire que MongoDB c’est nul. C’est bien leur utilisation de MongoDB qui est responsable du problème plus que MongoDB lui-même. C’est simplement regrettable que d’être allé un peu vite en besogne en s’asseyant sur quelques bonnes pratiques les oblige à changer de base de données en cours de route, alors qu’ils auraient sans doute pu continuer avec MongoDB. Un bel exemple de dette technique qu’on finit par devoir rembourser à prix élevé.

La migration vers PostgreSQL a réglé leurs problèmes d’objets structurés différemment, mais l’histoire ne dit pas si ils en ont profité pour isoler l’accès aux données. C’est en tout cas ce que j’espère.

Conclusion

Voilà ce qu’on peut retenir de ce retour d’expérience :

  1. N’attaquez pas votre base de données en direct depuis du code métier
  2. Ce n’est pas parce que MongoDB n’impose pas une structuration forte des objets d’une même collection qu’il faut en profiter pour faire n’importe quoi. Ce genre de liberté permet parfois de nous sortir de situations cocasses, mais il peut rapidement se retourner contre nous si on en a tendance à en abuser.

L’expérience m’a montré qu’en développement, la rigueur (et la structure) ne sont pas une option. Le manque de rigueur contribue à ajouter de la dette technique qui devra être payée à un moment ou à un autre. C’est la raison pour laquelle je préfère utiliser des langages à typage statique plutôt qu’à typage dynamique. Bien qu’étant de tempérament plutôt rigoureux, je ne me fais pas suffisamment confiance pour être sûr d’écrire du code qui tient la route et ça me rassure de voir que les outils que j’utilise veillent sur ce que je fais.

C’est d’ailleurs un de ces gardes-fou (en plus de l’isolation de l’accès aux données) qui m’a permis d’éviter de rencontrer ce genre de problème avec MongoDB. Je l’utilise en Java via un outil (Jongo ou Spring Data MongoDB en l’occurrence) qui s’appuie sur une classe Java pour structurer les données de chaque collection. Même si MongoDB permet de mettre des objets très différents dans une même collection, ces outils m’empêchent tout simplement de le faire.

Comme toujours, votre avis et votre analyse de cet article sont bienvenus !

L’image d’en-tête de cet article provient de Pexels. Et vous vous êtes plutôt pour ou contre le garde-fou ?

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

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