MongoDB alimente une large gamme d'applications, des API requête-réponse aux systèmes événementiels. Ce guide explique l'architecture MongoDB en mettant l'accent sur la façon dont les données circulent dans le système, les leviers de contrôle dont vous disposez pour la durabilité et la latence, les points de défaillance courants, et du code pratique que vous pouvez exécuter dès aujourd'hui. Vous obtiendrez un workflow clair pour la préparation en production et un plan de pilote local rapide à mettre en place et simple à évaluer.
MongoDB en un coup d'œil : composants principaux
Blocs de construction clés :
- mongod : le processus serveur de base de données. Chaque instance stocke des données et participe à la réplication ou au sharding.
- Moteur de stockage (WiredTiger) : gère les données sur disque, la compression, le journaling et les caches.
- Replica set : un groupe de processus mongod offrant la redondance et le basculement automatique. Un primaire reçoit les écritures ; les secondaires répliquent via l'oplog.
- Oplog : une collection ordonnée et bornée d'opérations que les secondaires appliquent pour atteindre la cohérence.
- Sharding : partitionnement horizontal. Les shards sont des replica sets ; mongos est le routeur de requêtes sans état ; les serveurs de configuration stockent les métadonnées du cluster.
- Index : btree ou types spéciaux (haché, texte, TTL) ; essentiels pour une latence de lecture prévisible et des écritures efficaces.
- Write concern et read concern : contrôles côté client pour les niveaux de durabilité et de cohérence.
Flux de données et de contrôle
Chemin d'écriture (replica set) :
- Client -> driver -> primaire.
- Le primaire écrit dans WiredTiger, journalisé sur disque.
- L'opération est enregistrée dans l'oplog.
- Les secondaires tirent et appliquent les entrées de l'oplog.
- L'acquittement au client dépend du writeConcern.
Schéma ASCII :
Client -> Driver -> Primaire -> Journal -> Oplog -> Secondaires
^ |
|_______________________________________________|
Chemin de lecture :
- Les lectures peuvent aller vers le primaire ou les secondaires selon le readPreference.
- Le planificateur de requêtes choisit un index ; sans index utile, les parcours de collection augmentent la latence.
Leviers de contrôle :
- writeConcern : 1, majority, ou nombres personnalisés. Des valeurs plus élevées augmentent la durabilité et la latence. Exemple :
w: "majority"attend que l'écriture soit répliquée vers une majorité de membres votants avant d'acquitter. - readConcern : local, majority, linearizable. Des garanties plus strictes peuvent réduire la disponibilité ou augmenter la latence.
- readPreference : primary, primaryPreferred, secondary, etc.
- Transactions : ACID multi-documents sur un replica set ou un cluster shardé ; augmentent le coût de coordination, à utiliser seulement quand nécessaire.
- Écritures retentatives et timeouts :
retryWrites=trueetmaxTimeMSpour gérer les défaillances transitoires en toute sécurité.
Points de défaillance et compromis
Modes de défaillance courants et atténuations :
- Crash du primaire ou partition réseau : l'élection de leader promeut un secondaire. Atténuer avec un nombre impair de membres votants et writeConcern majority.
- Blocages disque ou système de fichiers : le journaling et fsync peuvent mettre en pause les écritures. Utiliser un stockage rapide et surveiller l'iowait.
- Oplog trop petit : les secondaires prennent du retard et nécessitent une resynchronisation complète. Dimensionner l'oplog pour couvrir le retard de réplication pendant les pics et la maintenance. Règle pratique : configurer l'oplog pour contenir au moins 24 heures de volume d'écriture en pointe. Pour un pic de 500 Mo/heure, utiliser
--oplogSizeMB 12000(12 Go). - Shard chaud ou distribution inégale : un mauvais choix de clé de sharding cause des points chauds. Choisir des clés à haute cardinalité, bien distribuées, et de préférence immuables.
- Index manquants : parcours complets sous charge. Ajouter et dimensionner les indexes ; éviter le sur-indexage qui ralentit les écritures et consomme de la RAM.
- Documents volumineux : limite de 16 Mo par document. Normaliser sélectivement ou diviser les charges utiles importantes.
- Pression sur le cache : ensemble de travail plus grand que la RAM augmente les défauts de page. Ajouter de la RAM, ajuster les indexes, ou archiver les données froides.
Compromis à peser :
- Durabilité vs latence : les écritures majority coûtent plus en latence mais protègent contre le rollback.
- Cohérence vs disponibilité : readConcern et readPreference stricts peuvent réduire la disponibilité pendant le basculement.
- Simplicité vs échelle : un seul replica set est plus simple ; le sharding ajoute de la complexité mais met à l'échelle les écritures et le stockage.
Aperçu du workflow
Parcours étape par étape vers la production :
- Exigences : modèle de données, patterns d'accès, cibles de durabilité, RPO/RTO.
- Topologie : commencer avec un replica set à 3 membres. Ajouter le sharding seulement quand le débit d'écriture ou la taille du jeu de données l'exige.
- Schéma et indexes : concevoir autour des requêtes ; écrire des tests qui valident la couverture des indexes.
- Contrôles lecture/écriture : choisir writeConcern et readConcern selon la tolérance aux pannes.
- Résilience : activer les écritures retentatives, définir les timeouts de sélection de socket et de serveur, et le pooling de connexions.
- Observabilité : définir des SLO ; collecter métriques et logs ; configurer des alertes pour le retard de réplication, la pression sur le cache, et les requêtes lentes.
- Sauvegarde et restauration : planifier dumps ou snapshots ; répéter les restaurations régulièrement.
- Sécurité : activer l'authentification et TLS ; utiliser des rôles au moindre privilège ; faire pivoter les identifiants et clés.
- Planification capacité : dimensionner la RAM pour l'ensemble de travail ; provisionner les IOPS de stockage pour les taux d'écriture en pointe.
- Game days : répéter le basculement, le remplacement de nœud, et la restauration en non-production.
Exemples pratiques : API Node.js et Express
Voici une API minimale mais prête pour la production utilisant le driver Node.js officiel de MongoDB.
Chaîne de connexion exemple (replica set, écritures retentatives, acquittement majority) :
mongodb://localhost:27017,localhost:27018,localhost:27019/appdb?replicaSet=rs0&retryWrites=true&w=majority
API Express avec indexes et une transaction :
// package.json deps: express, mongodb
const express = require('express');
const { MongoClient } = require('mongodb');
const uri = 'mongodb://localhost:27017,localhost:27018,localhost:27019/appdb?replicaSet=rs0&retryWrites=true&w=majority';
const client = new MongoClient(uri, {
maxPoolSize: 50,
serverSelectionTimeoutMS: 5000
});
async function main() {
await client.connect();
const db = client.db('appdb');
const products = db.collection('products');
const inventory = db.collection('inventory');
// Indexes : SKU unique, index composé pour les requêtes
await products.createIndex({ sku: 1 }, { unique: true });
await products.createIndex({ category: 1, price: 1 });
const app = express();
app.use(express.json());
app.post('/products', async (req, res) => {
try {
const doc = req.body; // { sku, name, category, price }
const r = await products.insertOne(doc, { writeConcern: { w: 'majority' } });
res.status(201).json({ id: r.insertedId });
} catch (e) {
if (e.code === 11000) return res.status(409).json({ error: 'sku dupliqué' });
res.status(500).json({ error: 'erreur serveur' });
}
});
app.get('/products', async (req, res) => {
const { category, maxPrice } = req.query;
const q = {};
if (category) q.category = category;
if (maxPrice) q.price = { $lte: Number(maxPrice) };
const docs = await products.find(q).hint({ category: 1, price: 1 }).limit(100).toArray();
res.json(docs);
});
// Transfert d'inventaire atomique utilisant une transaction
app.post('/inventory/transfer', async (req, res) => {
const { sku, fromLoc, toLoc, qty } = req.body;
const session = client.startSession();
try {
await session.withTransaction(async () => {
const dec = await inventory.updateOne(
{ sku, location: fromLoc, qty: { $gte: qty } },
{ $inc: { qty: -qty } },
{ session }
);
if (dec.matchedCount !== 1) throw new Error('stock insuffisant');
await inventory.updateOne(
{ sku, location: toLoc },
{ $inc: { qty } },
{ upsert: true, session }
);
}, {
readConcern: { level: 'local' },
writeConcern: { w: 'majority' }
});
res.json({ ok: 1 });
} catch (e) {
res.status(400).json({ error: e.message });
} finally {
await session.endSession();
}
});
// Change stream pour logger les mises à jour de prix (lecture primaire uniquement)
products.watch([{ $match: { 'updateDescription.updatedFields.price': { $exists: true } } }])
.on('change', ev => console.log('changement prix', ev.documentKey, ev.updateDescription));
app.listen(3000, () => console.log('API sur :3000'));
}
main().catch(err => {
console.error(err);
process.exit(1);
});
Replica set pour développement local (3 nœuds) :
mkdir -p ~/data/mongo/rs0-1 ~/data/mongo/rs0-2 ~/data/mongo/rs0-3
mongod --replSet rs0 --port 27017 --dbpath ~/data/mongo/rs0-1 --bind_ip localhost --oplogSizeMB 512 --fork --logpath ~/data/mongo/rs0-1/mongod.log
mongod --replSet rs0 --port 27018 --dbpath ~/data/mongo/rs0-2 --bind_ip localhost --oplogSizeMB 512 --fork --logpath ~/data/mongo/rs0-2/mongod.log
mongod --replSet rs0 --port 27019 --dbpath ~/data/mongo/rs0-3 --bind_ip localhost --oplogSizeMB 512 --fork --logpath ~/data/mongo/rs0-3/mongod.log
mongosh --port 27017 --eval 'rs.initiate({ _id: "rs0", members: [ {_id:0, host:"localhost:27017"}, {_id:1, host:"localhost:27018"}, {_id:2, host:"localhost:27019"} ] })'
Sauvegarde et restauration
Les sauvegardes ne sont utiles que si les restaurations sont répétées. Utiliser des dumps pour les jeux de données petits à moyens et des snapshots de système de fichiers pour les gros. Pour les replica sets, faire des sauvegardes logiques depuis un secondaire pour réduire la charge du primaire.
Dump cohérent avec oplog pour une fenêtre point-in-time :
mongodump --host localhost --port 27018 \
--out ./dump_$(date +%F_%H%M) --oplog
Répétition de restauration (drop et restauration d'une base de test) :
mongorestore --nsFrom 'appdb.*' --nsTo 'appdb_restored.*' --drop ./dump_2024-01-01_1200
Liste de validation :
- Enregistrer la version MongoDB et featureCompatibilityVersion avec le dump.
- Vérifier que les permissions utilisateur existent ou les recréer dans la restauration.
- Chronométrer la restauration pour estimer le RTO et comparer aux cibles.
- Après restauration, lancer des tests de fumée : compter les collections, valider les indexes, et exécuter une petite suite de requêtes.
Surveillance et sécurité essentielles
Surveillance :
- Vue rapide :
mongostat --discoveretmongotoppour les opérations et l'activité par collection. - Métriques serveur : dans mongosh, exécuter
db.serverStatus().metricsetdb.currentOp(). - Logs : surveiller les logs de requêtes lentes ; régler
slowmsappropriément. - SLO clés : latence p95 lecture/écriture, retard de réplication, taux de réussite du cache, défauts de page, connexions.
Bases de la sécurité :
- Activer l'authentification et créer des utilisateurs au moindre privilège.
// Dans mongosh sur le primaire
use admin
db.createUser({
user: 'siteRootAdmin',
pwd: 'REMPLACER_PAR_MOT_DE_PASSE_FORT',
roles: [ { role: 'root', db: 'admin' } ]
})
Redémarrer mongod avec l'autorisation activée, ou configurer dans le fichier :
# mongod.conf
security:
authorization: enabled
Puis créer un utilisateur d'application restreint à la base de données :
use appdb
db.createUser({
user: 'app_user',
pwd: 'REMPLACER_PAR_MOT_DE_PASSE_FORT',
roles: [ { role: 'readWrite', db: 'appdb' } ]
})
TLS au repos et en transit :
- Générer les certificats serveur et activer TLS dans mongod.conf :
net:
tls:
mode: requireTLS
certificateKeyFile: /chemin/vers/server.pem
- Utiliser TLS dans l'URI du driver : ajouter
tls=trueet vérifier les certificats.
Audit et hygiène :
- Faire pivoter les identifiants, restreindre l'accès réseau, et désactiver les interfaces inutilisées.
- Réviser les rôles régulièrement et préférer les rôles intégrés quand possible.
Plan de pilote local
Objectif : un pilote sûr, étroit et mesurable exécutable sur un portable.
Périmètre :
- Un replica set à 3 nœuds sur localhost.
- Une petite API Express effectuant du CRUD et une transaction.
- Durabilité de base : writeConcern=majority et écritures retentatives.
- Une répétition de sauvegarde et restauration de la base d'application.
Étapes :
- Monter le replica set comme montré plus haut. Attendre l'élection du primaire.
- Lancer l'API Express. Insérer 10k produits et un inventaire d'exemple.
- Trafic : exécuter un chargeur simple pendant 5 minutes à 100 req/s lectures/écritures mixtes.
- Exercice de panne : tuer le processus mongod primaire avec
kill -9; observer le basculement automatique et les taux d'erreur de l'API. S'assurer que les écritures retentatives et les timeouts client évitent les échecs visibles pour l'utilisateur au-delà d'un bref aléa. - Sauvegarde : exécuter mongodump sur un secondaire avec
--oplog. Noter durée et taille. - Restauration : restaurer dans appdb_restored et valider que les comptes et indexes correspondent.
Critères de succès (mesurables) :
- p95 lecture < 30 ms et écriture < 50 ms sous charge de test sur un portable de dev (ajuster au matériel).
- Récupération après basculement < 10 secondes et l'API reprend sans intervention manuelle.
- La restauration se termine et les vérifications d'échantillon passent.
Prochain incrément de pilote :
- Ajouter l'auth et TLS, puis répéter les mêmes exercices.
- Introduire une deuxième collection et un deuxième index pour vérifier les régressions.
Conclusion
Vous disposez maintenant d'une carte pratique de MongoDB : comment les composants s'assemblent, comment fonctionnent les flux de données et de contrôle, où surviennent les pannes, et comment piloter la durabilité, la cohérence et la latence. Vous avez aussi des exemples exécutables, un workflow d'opérations étape par étape, et un pilote local mesurable dès le premier jour. Commencez par un replica set simple, définissez des SLO clairs, validez votre plan de restauration, et n'ajoutez de la complexité que quand l'usage l'exige.