Architecture NoSQL distribuée multi-sites pour l'observation des plateformes de location courte durée (Paris & Lyon)
Contexte • Architecture • Stack • Démarrage • Sharding • Résultats
Association NosCités — observatoire indépendant des plateformes de location courte durée (Airbnb, Booking, etc.). Présente à Paris et Lyon, l'association mesure l'impact de ces plateformes sur l'offre de logements longue durée et apporte de la transparence dans le débat public.
L'association a fait le choix de ne pas héberger ses données chez un fournisseur cloud : ses serveurs sont à Paris et à Lyon, pour garantir l'indépendance et la confidentialité de ses analyses. Cela implique une architecture distribuée multi-sites avec des contraintes fortes de résilience, de routage géographique des données et de performance des requêtes locales.
🚨 Scénario de crise initial : suite à un crash total de la base de données Paris (par négligence ou attaque malveillante), il a fallu restaurer la base, analyser son intégrité par série de requêtes, puis concevoir une nouvelle architecture pérenne capable de prévenir ce type d'incident — le tout dans un contexte d'urgence : produire un rapport sur l'effet "JO 2024" sur l'offre de logements parisienne.
- ✅ Restaurer une base MongoDB depuis une sauvegarde et valider son intégrité
- ✅ Concevoir 11 requêtes analytiques métier sur ~96 000 logements (CLI MongoDB + Polars)
- ✅ Mettre en place une stratégie de réplication (ReplicaSet 3 nodes) pour la haute disponibilité
- ✅ Mettre en place une stratégie de sharding avec zone sharding pour distribuer géographiquement Paris/Lyon
- ✅ Exposer les données via Power BI pour permettre aux équipes d'analyse de continuer leur travail
- ✅ Conteneuriser l'environnement Python d'analyse pour la reproductibilité
flowchart TB
CSV[📁 CSV Airbnb<br/>listings_Paris.csv<br/>listings_Lyon.csv] -->|mongoimport| MONGOS
subgraph CLUSTER[MongoDB Sharded Cluster]
MONGOS[🔀 mongos<br/>Router :37017<br/>Point d'entrée]
CONFIG[(⚙️ Config Server<br/>Métadonnées du cluster)]
MONGOS -.consulte.-> CONFIG
subgraph SHARD_P[Shard Paris :37101]
direction LR
PP[(Primary)]
PS1[(Secondary)]
PS2[(Secondary)]
PP --- PS1
PS1 --- PS2
end
subgraph SHARD_L[Shard Lyon :37201]
direction LR
LP[(Primary)]
LS1[(Secondary)]
LS2[(Secondary)]
LP --- LS1
LS1 --- LS2
end
MONGOS -->|city=Paris| SHARD_P
MONGOS -->|city=Lyon| SHARD_L
end
subgraph ANALYTICS[Couche analytique]
DOCKER[🐳 Container Python<br/>p7_pipeline]
POLARS[⚡ Polars<br/>Agrégations]
CSVOUT[📊 5 CSV de KPI]
DOCKER --> POLARS
POLARS --> CSVOUT
end
subgraph BI[Couche BI]
CONNECTOR[🔌 MongoDB BI Connector<br/>+ JDBC/ODBC]
PBI[📈 Power BI Desktop]
CONNECTOR --> PBI
end
MONGOS -->|pymongo| DOCKER
MONGOS -->|SQL via BI Connector| CONNECTOR
Trois couches indépendantes :
- Couche stockage distribué : cluster MongoDB shardé avec routage géographique
- Couche analytique : container Docker Python (Polars + pymongo) pour le calcul de KPI
- Couche BI : Power BI via MongoDB BI Connector pour les analystes métier
| Composant | Technologie | Version | Rôle |
|---|---|---|---|
| Base NoSQL | MongoDB | 7.0.25 |
Stockage distribué orienté documents |
| Shell Mongo | mongosh | 2.5.10 |
Admin cluster, requêtes CLI |
| Driver Python | pymongo | — | Connexion depuis les scripts |
| Analyse | Polars | — | Requêtes analytiques complexes (group by, ranking, fenêtrage) |
| Conteneurisation | Docker + Docker Compose | — | Isolation environnement Python |
| GUI Mongo | MongoDB Compass | — | Inspection schema + dev |
| BI | Power BI Desktop | — | Dashboards métier |
| Connecteur BI | MongoDB BI Connector | — | Pont SQL ↔ MongoDB via JDBC/ODBC |
Source : Données publiques Airbnb (snapshot juin 2024) — fichiers CSV listings_Paris.csv et listings_Lyon.csv.
| Caractéristique | Valeur |
|---|---|
| Documents Paris | 95 885 |
| Documents Lyon | 9 973 (après normalisation du champ city) |
| Total cluster | 105 858 documents |
| Volume sur disque | ~362 MiB |
| Champs par document | ~70 (logement, hôte, disponibilités, évaluations) |
| Base | P7MLO |
| Collection | listings |
- Linux/macOS (testé sur Ubuntu) —
mongod,mongosetmongoshinstallés - Docker + Docker Compose
- Python 3.11+ (si exécution hors container)
- ~1 Go de RAM disponible pour le cluster MongoDB
Les commandes ci-dessous résument la procédure de déploiement local du cluster (utilisée pour la conception du projet) :
# Config server (port 37017 pour mongos)
mongod --configsvr --replSet configReplSet --port 37119 --dbpath /var/lib/mongo/cfg
# Shard Paris (ReplicaSet, port 37101)
mongod --shardsvr --replSet shardParis --port 37101 --dbpath /var/lib/mongo/shardParis
# (+ 2 nodes secondaires sur ports différents pour le ReplicaSet)
# Shard Lyon (ReplicaSet, port 37201)
mongod --shardsvr --replSet shardLyon --port 37201 --dbpath /var/lib/mongo/shardLyon
# (+ 2 nodes secondaires sur ports différents pour le ReplicaSet)
# Router mongos (point d'entrée client)
mongos --configdb configReplSet/localhost:37119 --port 37017
# Ajout des shards au cluster
mongosh --port 37017 --eval '
sh.addShard("shardParis/localhost:37101");
sh.addShard("shardLyon/localhost:37201");
'// Depuis mongosh connecté à mongos (port 37017)
sh.enableSharding("P7MLO")
// Création de la clé de sharding compound
sh.shardCollection("P7MLO.listings", { city: 1, id: 1 })
// Zone sharding : assignation des plages aux shards
sh.addShardTag("shardParis", "PARIS")
sh.addShardTag("shardLyon", "LYON")
sh.addTagRange(
"P7MLO.listings",
{ city: "Paris", id: MinKey }, { city: "Paris", id: MaxKey },
"PARIS"
)
sh.addTagRange(
"P7MLO.listings",
{ city: "Lyon", id: MinKey }, { city: "Lyon", id: MaxKey },
"LYON"
)mongoimport \
--uri "mongodb://admin@localhost:27017/P7MLO?authSource=admin" \
--collection listings \
--type csv --headerline \
--file listings_Paris.csv# Le pipeline Python s'exécute dans un container avec network_mode: host
# pour pouvoir joindre le mongos sur localhost:37017
docker compose up --buildLes 5 fichiers CSV de KPI sont produits dans ./outputs/.
// Depuis mongosh sur mongos
sh.status()
db.listings.getShardDistribution()
db.listings.find({ city: "Paris" }).explain() // doit ne cibler que shardParisNosql-database-design/
├── docker-compose.yml # Lancement du container Python
├── Docker/
│ └── Pipeline/
│ ├── Dockerfile # Image Python 3.11 + polars + pymongo
│ └── requirements.txt
├── Requetes/
│ ├── requetes_P7_1.py # Partie 1 — Restauration & validation
│ ├── requetes_P7_2.py # Partie 2 — 5 KPI analytiques (Polars)
│ └── requetes_P7_3.py # Partie 3 — Setup sharding & ReplicaSet
├── Data/ # Données sources (non versionnées)
├── outputs/ # Résultats des requêtes (CSV)
├── .env # Variables d'env (non versionné)
└── README.md
| Composant | Rôle |
|---|---|
city (prefix) |
Routage géographique — toute requête filtrant par city est locale au shard correspondant (pas de scatter-gather, pas de cross-shard query) |
id (suffix) |
Haute cardinalité — garantit une bonne distribution des chunks à l'intérieur de chaque zone et évite les hot spots |
💡 Pourquoi pas juste
{city: 1}? La cardinalité decityest extrêmement faible (2 valeurs : Paris, Lyon). MongoDB n'aurait pas pu découper équitablement les chunks → un seul jumbo chunk par ville → impossible de scaler horizontalement à l'intérieur d'une ville. La compound key avecidrésout ce problème.
Au-delà du sharding "classique", on a explicitement assigné les plages de valeurs city à des shards précis via sh.addTagRange(). Cela garantit la résidence des données par localité — exigence métier directe du projet (chaque équipe locale accède rapidement à ses propres données, conformité au choix d'indépendance de NosCités).
Chaque shard est un ReplicaSet de 3 nodes (1 primary + 2 secondaries). Justification :
- ✅ Tolérance à une panne complète d'un node sans perte de service
- ✅ Élection automatique d'un nouveau primary en cas de défaillance
- ✅ Possibilité de servir des requêtes en lecture sur les secondaries (déchargement du primary)
Choix architectural assumé du docker-compose.yml :
services:
pipeline:
build: ./Docker/Pipeline
container_name: p7_pipeline
network_mode: host # ← accès direct aux ports mongo
volumes:
- ./Requetes:/app/Requetes:ro # ← code en lecture seule
- ./outputs:/app/outputs # ← export des résultats- ✅ MongoDB et ses processus (
mongod,mongos) tournent directement sur l'hôte pour pouvoir manipuler ReplicaSet et Sharding de façon réaliste (multi-ports, multi-processus) - ✅ Seul l'environnement Python est containerisé → reproductibilité des requêtes analytiques sans dépendre de l'installation locale
- ✅
network_mode: hostpermet au container de joindrelocalhost:37017(le mongos) sans abstraction réseau - ✅
Requetes/monté en read-only → impossible pour le container de modifier le code
Pour les requêtes complexes (ranking par fenêtre, group-by multi-colonnes), Polars est utilisé à la place de pandas :
df.group_by(["mois", "neighbourhood_cleansed"])
.agg(pl.col("taux_reservation_30j").mean().alias("taux_reservation_moyen"))
.with_columns(
pl.col("taux_reservation_moyen")
.rank(method="dense", descending=True)
.over("mois")
.alias("rang")
)
.filter(pl.col("rang") <= TOP_N)→ Performances supérieures sur gros volumes, API plus expressive (window functions natives), exécution lazy.
Le script Python accepte ses paramètres de connexion via os.getenv :
MONGO_URI = os.getenv("MONGO_URI", "").strip()
MONGO_HOST = os.getenv("MONGO_HOST", "localhost").strip()
MONGO_PORT = os.getenv("MONGO_PORT", "27017").strip()
MONGO_USER = os.getenv("MONGO_USER", "").strip()
MONGO_PASS = os.getenv("MONGO_PASS", "").strip()
OUTDIR = os.getenv("OUTDIR", "/app/outputs")
TOP_N = int(os.getenv("TOP_N", "5"))→ Pas de credentials hardcodés. Aligne avec les bonnes pratiques 12-factor app.
Après import via mongos et application des zones, la répartition observée :
| Shard | Documents | Volume | Chunks | % cluster |
|---|---|---|---|---|
| shardParis | 95 885 | 328.4 MiB | 1 | 90.57% |
| shardLyon | 9 973 | 33.26 MiB | 4 | 9.42% |
| Total | 105 858 | 361.67 MiB | 5 | 100% |
→ Distribution conforme à la volumétrie réelle du marché (Paris >> Lyon). La répartition asymétrique est acceptée et assumée : elle reflète la réalité métier, ce qui est le bon comportement d'une stratégie multi-sites bien conçue.
Les commandes utilisées pour valider le bon routage des requêtes :
sh.status() // Vue d'ensemble du cluster
db.listings.getShardDistribution() // Distribution par shard
db.listings.find({ city: "Paris" }).explain() // Doit cibler UNIQUEMENT shardParis
db.listings.find({ city: "Lyon" }).explain() // Doit cibler UNIQUEMENT shardLyon| KPI | Valeur |
|---|---|
| Annonces totales | 95 885 |
| Hôtes distincts | 71 979 |
| Logements réservables instantanément | 22 094 (23%) |
| Super hôtes | 10 027 (13.93%) |
| Hôtes "professionnels" (>100 annonces) | 22 (0.03%) |
| Type | Nombre d'annonces |
|---|---|
| Logement entier | 85 733 (89%) |
| Chambre privée | 8 975 (9%) |
| Chambre d'hôtel | 776 |
| Chambre partagée | 401 |
| Catégorie | Médiane du nombre d'avis |
|---|---|
| Superhôte | 24 |
| Non superhôte | 2 |
→ Gap × 12 : les Superhôtes captent une part disproportionnée des locations effectives. C'est un indicateur fort de professionnalisation du marché.
| Rang | Quartier | Taux de réservation |
|---|---|---|
| 1 | Ménilmontant | 75.42% |
| 2 | Entrepôt | 74.81% |
| 3 | Popincourt | 74.78% |
| 4 | Buttes-Chaumont | 74.13% |
| 5 | Panthéon | 73.14% |
| Rang | Quartier | Nombre d'annonces |
|---|---|---|
| 1 | Buttes-Montmartre | 10 555 |
| 2 | Popincourt | 8 430 |
| 3 | Vaugirard | 7 802 |
| 4 | Batignolles-Monceau | 6 857 |
| 5 | Entrepôt | 6 558 |
Power BI Desktop est connecté au cluster via la chaîne :
Power BI → MongoDB BI Connector → JDBC/ODBC Driver → mongos (MongoDB)
Cette architecture permet aux analystes métier de continuer à utiliser leur outil habituel (SQL via Power BI) sans connaître MongoDB, tout en bénéficiant des avantages du cluster distribué (résilience, scalabilité, routage géographique). C'est l'incarnation du principe "séparation des préoccupations" : les data engineers gèrent l'infra, les analystes consomment.
Pistes d'amélioration identifiées pour une version production :
- 🔐 Authentification stricte sur le cluster : keyfile + TLS entre les nodes du ReplicaSet
- 📅 Snapshots automatisés des shards via
mongodumpplanifié - 📈 Monitoring via MongoDB Ops Manager ou Percona PMM
- 🔄 Pipeline d'ingestion idempotent (upsert sur clé métier
id) — déjà identifié dans le logigramme méthodologique - 🌍 Ajout d'autres villes (Marseille, Bordeaux, Nice) → ajouter une nouvelle zone sharding
- ⏱️ Données temporelles : suivre l'évolution mensuelle du marché plutôt qu'un snapshot
- 🔍 Index composés orientés requêtes BI (sur
(city, room_type, last_scraped)par exemple)
- 📄
P7_Support_de_presentation.pdf— Présentation de soutenance (architecture, choix techniques, résultats) - 📊
outputs/*.csv— Résultats des 5 requêtes analytiques
Mathieu Lowagie
Data Engineer | Service Delivery Manager — 17 ans d'expérience B2B télécoms
Projet réalisé dans le cadre du Master 2 Data Engineering (OpenClassrooms — Projet 7 "Concevez et analysez une base de données NoSQL").
Distribué sous licence MIT — voir LICENSE pour les détails.