Intro
Cette version française explique MongoDB architecture explained with practical examples avec le même objectif pratique que l article source : aider le lecteur à comprendre le contexte, les décisions à prendre et les points à vérifier avant de passer à l action.
MongoDB alimente un large éventail d'applications, des APIs requête-réponse aux systèmes événementiels. Ce guide explique MongoDB architecture avec un angle résolument pratique : comment les données traversent le système, quels leviers vous contrôlez pour la durabilité et la latence, où se situent les points de défaillance, et du code prêt à exécuter aujourd'hui. Vous repartirez avec un workflow clair de mise en production et un plan pilote local rapide à mettre en place et simple à évaluer.
MongoDB en un coup d'œil : composants clés
Briques essentielles :
- mongod : le processus serveur de base de données. Chaque instance stocke les 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 fournissant redondance et bascule automatique. Un primary reçoit les écritures ; les secondaries répliquent via l'oplog.
- Oplog : une collection ordonnée et capée d'opérations que les secondaries appliquent pour converger vers 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 : B-Tree ou spécialisés (hashed, text, TTL) ; critiques pour une latence de lecture prévisible et des écritures efficaces.
- Write et read concerns : 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 -> primary.
- Le primary écrit via WiredTiger, journalisé sur disque.
- L'opération est consignée dans l'oplog.
- Les secondaries récupèrent et appliquent les entrées d'oplog.
- L'accusé de réception au client dépend du writeConcern.
Croquis ASCII :
Client -> Driver -> Primary -> Journal -> Oplog -> Secondaries
^ |
|_________________________|
Chemin de lecture :
- Les lectures vont vers le primary ou les secondaries selon le readPreference.
- Le planificateur de requêtes choisit un index ; sans index utile, des scans complets de collection augmentent la latence.
Leviers de contrôle :
- writeConcern : 1, majority, ou valeurs personnalisées. Des niveaux plus élevés augmentent durabilité et latence.
- readConcern : local, majority, linearizable. Des garanties plus strictes peuvent réduire la disponibilité ou accroître la latence.
- readPreference : primary, primaryPreferred, secondary, etc.
- Transactions : ACID multi-document sur un replica set ou un cluster shardé ; elles coûtent de la coordination, à n'utiliser que si nécessaire.
- Écritures retentables et timeouts : retryWrites=true et maxTimeMS pour gérer proprement les défaillances transitoires.
Points de défaillance et arbitrages
Modes de panne fréquents et atténuations :
- Crash du primary ou partition réseau : une élection promeut un secondary. Mitiger avec un nombre impair de votants et un writeConcern=majority.
- Stalls disque ou système de fichiers : le journaling et fsync peuvent bloquer les écritures. Utilisez un stockage rapide et surveillez l'iowait.
- Oplog trop petit : les secondaries prennent du retard et nécessitent une resynchronisation complète. Dimensionnez l'oplog pour couvrir le lag en pic et en maintenance.
- Shard chaud ou répartition inégale : une mauvaise clé de sharding crée des hotspots. Choisissez des clés à forte cardinalité, bien distribuées et de préférence immuables.
- Manques d'index : scans complets sous charge. Ajoutez et dimensionnez correctement les index ; évitez la sur-indexation qui ralentit les écritures et consomme la RAM.
- Documents volumineux : limite de 16 Mo par document. Normalisez avec discernement ou segmentez les gros contenus.
- Pression sur le cache : un working set plus grand que la RAM augmente les fautes de page. Ajoutez de la RAM, ajustez les index, ou archivez les données froides.
Arbitrages à peser :
- Durabilité vs latence : les écritures en majority coûtent plus de latence mais protègent contre les rollbacks.
- Cohérence vs disponibilité : des readConcern/readPreference stricts peuvent réduire la disponibilité lors des bascules.
- Simplicité vs échelle : un seul replica set est plus simple ; le sharding ajoutera de la complexité mais fait passer à l'échelle les écritures et le stockage.
Parcours opérationnel
Étapes vers la production :
- Exigences : modèle de données, patterns d'accès, cibles de durabilité, RPO/RTO.
- Topologie : démarrez avec un replica set à 3 membres. Ajoutez le sharding seulement si le débit d'écriture ou la taille des données l'exige.
- Schéma et index : concevez autour des requêtes ; écrivez des tests qui valident la couverture d'index.
- Contrôles de lecture/écriture : choisissez writeConcern et readConcern selon votre tolérance aux pannes.
- Résilience : activez retryable writes, définissez des timeouts socket et de sélection de serveur, et le pooling de connexions.
- Observabilité : fixez des SLOs ; collectez métriques et logs ; alertez sur le lag de réplication, la pression cache et les requêtes lentes.
- Sauvegarde et restauration : planifiez dumps ou snapshots ; répétez les restaurations régulièrement.
- Sécurité : activez l'authentification et TLS ; rôles à privilèges minimaux ; rotation des identifiants et des clés.
- Plan de capacité : dimensionnez la RAM pour le working set ; provisionnez l'IOPS pour les pics d'écriture.
- Game days : répétez bascule, remplacement de nœud et restauration hors production.
Exemples pratiques : Node.js et Express API
Exemple de chaîne de connexion (replica set, retryable writes, ack majority) :
mongodb://localhost:27017, localhost:27018, localhost:27019/appdb?replicaSet=rs0&retryWrites=true&w=majority
API Express avec index et une transaction :
// deps package.json : 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');
// Index : 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 en double' });
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 de stock atomique via 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 consigner les mises à jour de prix
products.watch([{ $match: { 'updateDescription.updatedFields.price': { $exists: true } } }])
.on('change', ev => console.log('changement de 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 le dev local (3 nœuds) :
# Trois instances mongod sur des ports et dbpaths distincts
mkdir -p ~/data/mongo/{rs0-1, rs0-2, 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 valent que si les restaurations sont répétées. Utilisez des dumps pour des jeux de données petits à moyens et des snapshots de système de fichiers pour les gros volumes. Sur un replica set, faites les sauvegardes logiques depuis un secondary pour réduire la charge du primary.
Dump cohérent avec oplog pour une fenêtre temporelle :
# Depuis un secondary pour réduire l'impact ; inclure --oplog pour la cohérence
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
Checklist :
- Consigner la version de MongoDB et la featureCompatibilityVersion avec le dump.
- Vérifier l'existence des permissions utilisateurs ou les recréer lors de la restauration.
- Chronométrer la restauration pour estimer le RTO et comparer aux cibles.
- Après restauration, lancer des tests fumigènes : compter les collections, valider les index et exécuter une petite suite de requêtes.
Supervision et sécurité essentielles
Supervision :
- Vue rapide : mongostat --discover et mongotop pour les opérations et l'activité par collection.
- Métriques serveur : dans mongosh, exécuter db.serverStatus().metrics et db.currentOp().
- Logs : observer les slow queries ; régler slowms de façon appropriée.
- SLOs clés : latence p95 lecture/écriture, lag de réplication, ratio de hit cache, fautes de page, connexions.
Sécurité de base :
- Activer l'authentification et créer des utilisateurs à privilèges minimaux.
// Dans mongosh sur le primary
use admin
// Créer un utilisateur admin
db.createUser({
user: 'siteRootAdmin',
pwd: 'REPLACE_WITH_STRONG_PASSWORD',
roles: [ { role: 'root', db: 'admin' } ]
})
Redémarrer mongod avec l'autorisation activée, ou via le fichier de config :
# mongod.conf
security:
authorization: enabled
Puis créer un utilisateur applicatif restreint à la base de l'application :
use appdb
db.createUser({
user: 'app_user',
pwd: 'REPLACE_WITH_STRONG_PASSWORD',
roles: [ { role: 'readWrite', db: 'appdb' } ]
})
TLS au repos et en transit :
- Générer des certificats serveur et activer TLS dans mongod.conf :
net:
tls:
mode: requireTLS
certificateKeyFile: /path/to/server.pem
- Utiliser TLS dans l'URI du driver : ajouter tls=true et vérifier les certificats.
Audit et hygiène :
- Rotation des identifiants, restriction d'accès réseau, et désactivation des interfaces inutilisées.
- Revue régulière des rôles et préférence pour les rôles intégrés lorsque possible.
Plan pilote local
Objectif : un pilote sûr, étroit et mesurable, exécutable sur un laptop.
Périmètre :
- Un replica set à 3 nœuds sur localhost.
- Une petite API Express réalisant des opérations CRUD et une transaction.
- Durabilité de base : writeConcern=majority et retryable writes.
- Une répétition de sauvegarde et restauration de la base applicative.
Étapes :
- Démarrez le replica set comme montré. Attendez l'élection du primary.
- Lancez l'API Express. Semez 10k produits et un inventaire d'exemple.
- Trafic : chargez 5 minutes à 100 req/s, en lectures et écritures mixtes.
- Exercice de panne : kill -9 le processus mongod primary ; observez la bascule automatique et le taux d'erreurs de l'API. Assurez-vous que retryable writes et les timeouts évitent des échecs visibles au-delà d'un bref pic.
- Sauvegarde : exécutez mongodump sur un secondary avec --oplog. Notez durée et taille.
- Restauration : restaurez dans appdb_restored et validez que les comptes et index correspondent.
Critères de succès (mesurables) :
- p95 lecture < 30 ms et écriture < 50 ms sous charge de test sur un laptop de dev (à ajuster selon matériel).
- Reprise après bascule < 10 secondes et API qui repart sans intervention manuelle.
- Restauration achevée et contrôles échantillons réussis.
Prochain incrément pilote :
- Ajouter auth et TLS, puis répéter les mêmes exercices.
- Introduire une seconde collection et un second index pour déceler d'éventuelles régressions.
Conclusion
Vous disposez désormais d'une carte pratique de MongoDB : comment s'assemblent les MongoDB components, comment fonctionnent MongoDB data flow et les leviers de contrôle, où se situent les défaillances et comment arbitrer durabilité, cohérence et latence. Vous avez aussi des exemples exécutables, un workflow opérationnel pas à pas, et un pilote local mesurable dès le premier jour. Commencez par un replica set simple, fixez des SLOs clairs, prouvez votre plan de restauration et n'ajoutez de complexité qu'en réponse aux besoins réels d'usage.