data / sqlalchemy
Sharding et bases de données multiples
Explication
Ce que vous allez apprendre
- Comprendre pourquoi le sharding devient nécessaire quand une seule base atteint ses limites physiques
- Router manuellement des opérations vers le bon shard avec un
enginepar shard - Automatiser ce routage avec l'API
ShardedSessionde SQLAlchemy - Reconnaître la limite fondamentale du sharding sur les
JOINentre shards différents - Choisir une clé de sharding qui garde ensemble les données fréquemment liées
Dans quel contexte ?
Une plateforme SaaS multi-clients voit sa base de données PostgreSQL principale saturer : des millions de lignes par client, des milliers de clients, et un seul serveur qui ne peut plus absorber le volume de requêtes simultanées. Répartir les clients sur plusieurs bases distinctes selon leur identifiant devient la seule option pour continuer à grandir, au prix d'une complexité d'architecture non négligeable que cette leçon détaille.
D'abord, la limite physique d'une seule base
À mesure qu'une application grandit, une seule base de données peut atteindre ses limites physiques : trop de données, trop de requêtes simultanées pour un seul serveur.
La solution : répartir sur plusieurs bases
Le sharding consiste à répartir les données sur plusieurs bases distinctes ("shards"), chacune ne contenant qu'une partie des données, généralement selon une clé de répartition comme l'identifiant du client.
| Approche | Complexité | Contrôle |
|---|---|---|
Routage manuel (un engine par shard) | faible à mettre en place | total, mais demande de la discipline |
ShardedSession (SQLAlchemy) | configuration plus lourde | routage automatisé |
Étape 1 : comprendre le principe du routage
Une fonction de hachage ou une règle métier détermine, pour chaque donnée, sur quel shard elle doit vivre. Toutes les opérations concernant ce client doivent alors être dirigées vers le bon shard, un peu comme répartir des dossiers clients dans plusieurs classeurs selon la première lettre du nom.
Étape 2 : router à la main, la solution la plus simple
L'approche manuelle, un engine par shard et une fonction de routage écrite à la main, est simple à comprendre et à déboguer, mais demande de la discipline dans tout le code applicatif.
Étape 3 : automatiser le routage
L'API ShardedSession de SQLAlchemy automatise ce routage via des fonctions de choix (shard_chooser, id_chooser, query_chooser), au prix d'une complexité de configuration plus importante.
Une limite fondamentale à connaître avant de s'engager
Le sharding brise une capacité fondamentale des bases relationnelles : le JOIN entre deux tables ne fonctionne plus si les données sont sur deux shards différents. Il faut alors soit agréger les résultats côté application, soit choisir une clé de sharding qui garde ensemble les données fréquemment liées.
Piège fréquent
Une clé de sharding mal choisie (qui sépare des données fréquemment jointes, comme un client et ses commandes) oblige à agréger les résultats manuellement côté application, ce qui annule une bonne partie du confort de l'ORM. Choisissez une clé de sharding qui garde ensemble les données consultées ensemble.
Vers la suite
C'est une décision d'architecture lourde, à ne prendre qu'une fois les limites d'une base unique réellement atteintes. La prochaine leçon revient à une échelle plus modeste : simplifier l'écriture de code autour d'une relation many-to-many enrichie.
Commandes & code
Sharding et bases de données multiples
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
# --- Approche simple : plusieurs engines, un routeur applicatif choisit le bon ---
engine_shard_0 = create_engine("postgresql://app@db-shard-0/clients")
engine_shard_1 = create_engine("postgresql://app@db-shard-1/clients")
engine_shard_2 = create_engine("postgresql://app@db-shard-2/clients")
SHARDS = [engine_shard_0, engine_shard_1, engine_shard_2]
def engine_pour_client(client_id: int):
# Hachage simple : à remplacer par un hachage cohérent en production (moins de resharding)
return SHARDS[client_id % len(SHARDS)]
def session_pour_client(client_id: int) -> Session:
engine = engine_pour_client(client_id)
return sessionmaker(bind=engine)()
with session_pour_client(client_id=42) as session:
client = session.get(Client, 42)
# --- Horizontal Sharding API native de SQLAlchemy (approche déclarative) ---
from sqlalchemy.ext.horizontal_shard import ShardedSession
shards = {"shard0": engine_shard_0, "shard1": engine_shard_1, "shard2": engine_shard_2}
def choix_shard_ecriture(mapper, instance, clause=None):
# Appelé pour choisir le shard cible d'une écriture (instance présente lors d'un flush)
return f"shard{instance.client_id % len(shards)}"
def choix_shard_lecture(query, ident):
# Appelé pour Session.get() : extrait le client_id depuis la clé primaire recherchée
return f"shard{ident[0] % len(shards)}"
def tous_les_shards(query):
# Appelé quand la requête ne permet pas de déterminer un shard unique -> fan-out
return list(shards.keys())
SessionRepartie = sessionmaker(
class_=ShardedSession,
shards=shards,
shard_chooser=choix_shard_ecriture,
id_chooser=choix_shard_lecture,
query_chooser=tous_les_shards,
)
with SessionRepartie() as session:
session.add(Commande(client_id=42, montant=99.0)) # écrit sur le shard calculé
session.commit()
resultat = session.query(Commande).filter_by(client_id=42).all() # ciblé si le shard est déductible
toutes = session.query(Commande).all() # fan-out : interroge tous les shards puis fusionne
# Limite fondamentale : un JOIN entre objets répartis sur deux shards différents n'est pas supporté
# nativement -> agréger côté application, ou dénormaliser les données fréquemment jointes ensembleRésumé
- Le routage manuel (un
enginepar shard + fonction de hachage applicative) reste l'approche la plus simple et la plus prévisible. sqlalchemy.ext.horizontal_shard.ShardedSessionautomatise le choix du shard viashard_chooser/id_chooser/query_chooser.- Une requête sans clé de sharding connue déclenche un "fan-out" : interrogation de tous les shards puis fusion applicative des résultats.
- Aucun JOIN cross-shard n'existe nativement : la clé de sharding doit garder ensemble les données fréquemment liées (ex:
client_id).
Exercices pratiques
Mission : un rapport multi-clients qui échoue sur l'architecture shardée
Objectif : Comprendre pourquoi une requête agrégée sur tous les clients échoue avec ShardedSession, et concevoir une stratégie de fan-out adaptée.
Contexte
La plateforme SaaS a migré vers ShardedSession avec un shard_chooser basé sur client_id. Une nouvelle fonctionnalité doit afficher, pour l'équipe commerciale, le montant total des commandes de tous les clients situés en France, tous shards confondus : session.query(Commande).join(Client).filter(Client.pays == "FR"). Cette requête plante ou ne renvoie que les résultats d'un seul shard, alors que des clients français existent sur les trois shards.