WTF Tech: MongoDB + Nodejs + Kue

Comment un job batch a failli tuer un projet à cause d’un design MongoDB mal pensé. Série WTF Tech - épisode 1.
Je vous présente une série d'articles que je vais rédiger dont l'intitulé commence par WTF Tech.
L'idée est de vous raconter toutes les fois où je suis intervenu sur des projets qui avaient un vrai problème de conception, afin que vous ne reproduisiez pas les mêmes erreurs.
Le but n'est pas de dénigrer les projets, les auteurs, mais plutôt de se nourrir de ces échecs, et de ne pas fail à votre tour.
Le cadre
Il y a quelques années, je suis intervenu sur un projet qui avait lui-même plusieurs années d'existence.
Nous n'interviendrons pas sur le frontend dans cet article, c'est pourquoi nous ne mentionnerons pas cette partie.
La stack backend : Nodejs + MongoDB et des jobs qui s'appuient sur Kue.js.
C'est quoi Kue.js
On va dire que c'est une sorte de message queue orienté job qui utilise un système d'events et elle tourne sur une stack Node/Redis.
Jusque-là, rien d'anormal.
Cependant, le temps passe, et quelques semaines après mon arrivée, un bug se produit.
Je vous rassure tout de suite, je n'étais jamais intervenu sur cette partie du projet, donc je n'en suis pas à l'origine, mais attendez de comprendre toute l'histoire.
L'équipe m'appelle en catastrophe pour intervenir sur le sujet, car des jobs batch importants sont en échec, et cela s'avère critique pour le projet.
Ok, j'essaie de comprendre ce qu'il se passe. Qu'est-ce qui est fait pour que le job s'arrête comme ça, alors qu'il a fonctionné sans problème pendant plusieurs années ?
Si les managers avaient été idiots, ils auraient pu croire que c'était moi, car au niveau du timing, cela ne jouait pas en ma faveur. Mais ça restait une boîte intelligente où la culture du blame n'existait pas et où l'on a cherché ensemble des solutions à ce problème.
En revanche, je spoil un peu : c'est un bug causé par un problème qui a pris racine depuis très longtemps dans le projet.
L'investigation
Je me penche alors sur le problème et essaie de comprendre plusieurs points :
- Que fait le job (à quoi il sert)
- Comment il fonctionne (quelles sont les étapes)
- Identifier les problèmes potentiels
Ok, tâchons de répondre à ces questions :
Le rôle du job :
-> Le projet sert à faire des opérations permettant de payer des tiers, d'où la criticité.
Le fonctionnement du job :
-> Il part d'une liste d'items (> 5000 documents) depuis la base de données MongoDB, traite des opérations, met à jour ces mêmes items en base.
Un problème potentiel
-> Les développeurs se sont dit : Ok, étant donné que mon job met à jour les items de ma collection, il faut que je les snapshot à un instant T, que je fasse les opérations sur des items isolés directement dans le job batch afin d'éviter de monter la charge sur le backend.
De cette façon, il y aura une grosse opération de lecture, suivie d'une grosse opération d'écriture. Mais pas de boucle d'opérations incessantes en base.
Donc, nous avons affaire à un dump entier de la base directement dans la message queue.

-> Autre problème potentiel, les développeurs ont utilisé le document lui-même pour stocker ses propres opérations.
En gros, il y avait une clé dans le document qui était un array d'éléments.
Ce qui signifie que dans le temps, un document ne peut faire que grossir.

Solution du problème
Les deux problèmes identifiés juste au-dessus ont fini par se produire.
- Kue croulait sous le poids du message entrant, il prenait plusieurs gigas dans la queue à chaque fois que le job se déclenchait.
- MongoDB était très mal utilisé, les développeurs avaient carrément abusé de son modèle document, en ignorant purement et simplement la taille limite d'un document en base : 16 Mo. Ils inséraient continuellement des éléments dans les arrays du document.
De loin, on dirait que 16 Mo, c'est beaucoup pour un JSON. Mais croyez-moi, après plusieurs années, cette limite sera rapidement atteinte.
De même, MongoDB a une autre limite sur sa taille de requête. On ne fait pas un .find() sur une base entière comme ça gratuitement.
Il faut batcher. Dans mon cas, j'ai opté pour une centaine d'items à la fois.
J'ai déplacé les arrays dans une autre collection et utilisé Mongoose pour faire les relations entre un document et l'autre.
Rien de mystique, c'est quelque chose qui ressemble à ça :
const useSchema = new mongoose.Schema({
name: String,
label: String,
operations: [{ type: mongoose.Types.ObjectId, ref: "UserOperation" }]
})
const userOperations = new mongoose.Schema({
value: Number,
operationDate: { type: Date, default: Date.now }
})
Mongoose permet de récupérer les informations en cascade avec la méthode .populate()
Enfin, j'ai arrêté d'envoyer la base entière dans Kue.
Je procédais à un dump de la base dans plusieurs fichiers JSON et j'envoyais à Kue les paths vers ces fichiers, de manière à ce que le job lise le fichier, fasse les opérations pour chaque document et mette ensuite à jour la base sur les deux collections distinctes.
Ce n'est pas forcément la méthode la plus optimale, je trouve qu'une base secondaire pour les opérations aurait été préférable, mais cela nécessite une machine supplémentaire vu la taille de notre base et le besoin en RAM.
Bilan
Le problème rencontré aurait pu avoir des retombées bien plus catastrophiques tant les opérations étaient au cœur du produit.
Ceci étant, le cœur du problème venait de la conception.
Un bug latent, qui a toujours existé par son design.
Il fallait juste que la base atteigne une taille critique pour qu'il se déclenche.
Je pense qu'il a été causé par un manque d'expérience des développeurs sur une technologie nouvelle lorsqu'ils l'ont adoptée. Et beaucoup de gens auraient pu faire la même erreur.
MongoDB est une technologie très attirante car elle permet d'aller vite, est souple aux changements de modèles, et pour une startup qui évolue, c'est parfait. Vous pourrez adapter les schémas de données à la volée, en fonction du besoin.
Cependant, comme on peut le voir, un abus de ce qui rend la technologie intéressante a failli faire sombrer le projet.
Il convient donc de se renseigner un maximum lorsque l'on adopte un nouvel outil, un nouveau langage, etc. De regarder avec beaucoup d'attention ses limites, et d'anticiper les impacts potentiels de ces limites sur notre utilisation actuelle, voire à venir.

Alexandre P.
Développeur passionné depuis plus de 20 ans, j'ai une appétence particulière pour les défis techniques et changer de technologie ne me fait pas froid aux yeux.
Poursuivre la lecture dans la rubrique Dev