10 millions de users, 1TB de data, votre database PostgreSQL rame. Sharding ? Peut-être. Mais avant, explorons toutes les alternatives (plus simples).
D’abord : Avez-vous vraiment besoin de sharding ?
Alternatives plus simples
1. Vertical scaling (augmenter la machine)
DB actuelle : 8 CPU, 32GB RAM
DB upgradée : 32 CPU, 256GB RAM
Coût : $500/mois → $2000/mois
Effort : 1 heure de migration
Jusqu’où ? Machines jusqu’à 128 CPU, 4TB RAM existent.
2. Read replicas (scaling lecture)
┌──────────┐
│ Master │ ← Writes
│ (Primary)│
└────┬─────┘
│
┌───┴────┬─────────┐
│ │ │
┌▼──┐ ┌─▼─┐ ┌───▼┐
│R1 │ │R2 │ │R3 │ ← Reads
└───┘ └───┘ └────┘
Cas d’usage : 90% reads, 10% writes
Effort : 1 semaine
3. Partitioning (même DB, tables séparées)
-- Partition par date
CREATE TABLE orders_2024 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');
CREATE TABLE orders_2025 PARTITION OF orders
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
Performance : Queries 10x plus rapides sur partition
Effort : 1 semaine
4. Caching agressif
Redis cache :
- Requêtes fréquentes
- Sessions
- Rate limiting
Hit rate : 80%+ → Reduce DB load 80%
Quand sharding devient nécessaire
Signaux d’alarme
✅ Vertical scaling maxed out
- Machine biggest available
- Coût prohibitif (>$10k/mois)
✅ Write throughput saturé
- Master CPU > 80%
- Write lag croissant
- Read replicas ne suffisent plus
✅ Data size > 1TB
- Backups trop longs (>6h)
- Restore impossible en RTO
- Queries lentes malgré index
✅ Geographic distribution
- Users worldwide
- Latency critique
- Data residency laws (GDPR)
Sharding : Les stratégies
1. Hash-based sharding
# Distribuer users par ID
def get_shard(user_id: int, num_shards: int) -> int:
return hash(user_id) % num_shards
# user_id 123 → shard 1
# user_id 456 → shard 2
# user_id 789 → shard 1
Avantages :
- Distribution uniforme
- Simple à implémenter
Inconvénients :
- Resharding difficile (change le hash)
- Queries cross-shard impossibles
2. Range-based sharding
# Sharding par plage d'IDs
shard_1 : users 1-1M
shard_2 : users 1M-2M
shard_3 : users 2M-3M
def get_shard(user_id: int) -> int:
return (user_id - 1) // 1_000_000 + 1
Avantages :
- Resharding plus facile
- Range queries possibles
Inconvénients :
- Déséquilibre potentiel (shard 1 saturé)
- Hotspots
3. Geographic sharding
# Sharding par région
shard_eu : users.region = 'EU'
shard_us : users.region = 'US'
shard_asia : users.region = 'ASIA'
def get_shard(user: User) -> str:
return f'shard_{user.region.lower()}'
Avantages :
- Latency optimale
- Compliance GDPR
Inconvénients :
- Déséquilibre géographique
- Cross-region queries complexes
4. Tenant-based sharding (SaaS)
# Sharding par entreprise/tenant
shard_1 : tenants 1-1000
shard_2 : tenants 1001-2000
def get_shard(tenant_id: int) -> int:
# Gros clients = shard dédié
if tenant_id in PREMIUM_TENANTS:
return dedicated_shards[tenant_id]
# Petits clients = sharding hash
return hash(tenant_id) % NUM_SHARED_SHARDS
Avantages :
- Isolation tenants
- Performance prédictible
Inconvénients :
- Coût (shards dédiés)
Architecture sharding
Layer 1 : Application routing
// Application gère le routing
class ShardedUserRepository {
private shards: Database[];
async findUser(userId: number): Promise<User> {
const shardIndex = this.getShardIndex(userId);
const shard = this.shards[shardIndex];
return shard.query('SELECT * FROM users WHERE id = $1', [userId]);
}
private getShardIndex(userId: number): number {
return userId % this.shards.length;
}
}
Avantages :
- Contrôle total
- Logique custom possible
Inconvénients :
- Complexité app
- Chaque service doit implémenter
Layer 2 : Proxy/Middleware
┌────────┐
│ App │
└───┬────┘
│
┌───▼──────┐
│ ProxySQL │ ← Routing automatique
│ Vitess │
└───┬──────┘
│
┌───┴───┬────────┬─────────┐
│Shard1 │Shard2 │Shard3 │
└───────┴────────┴─────────┘
Solutions :
- Vitess (YouTube) : Proxy sharding MySQL
- Citus : Extension PostgreSQL
- ProxySQL : MySQL proxy
Avantages :
- App agnostic
- Resharding facilité
Inconvénients :
- Nouveau layer
- Performance overhead
Migration vers sharding
Approche progressive (6-12 mois)
Phase 1 : Dual writes (Mois 1-2)
# Écrire dans old DB ET nouveau shard
async def createUser(user: User):
# Old DB (legacy)
await old_db.insert(user)
# New shard (progressive)
shard = get_shard(user.id)
await shard.insert(user)
Phase 2 : Backfill data (Mois 3-4)
# Migrer data existante
async def backfill_shard(shard_id: int):
users = await old_db.query('''
SELECT * FROM users
WHERE MOD(id, {}) = {}
''', total_shards, shard_id)
for user in users:
await shards[shard_id].insert(user)
Phase 3 : Dual reads (Mois 5-6)
# Lire du shard, fallback vers old DB
async def getUser(user_id: int):
shard = get_shard(user_id)
user = await shard.query('SELECT * FROM users WHERE id = $1', [user_id])
if not user:
# Fallback (data pas encore migrée)
user = await old_db.query('SELECT * FROM users WHERE id = $1', [user_id])
return user
Phase 4 : Cutover (Mois 7-8)
# Lire seulement du shard
async def getUser(user_id: int):
shard = get_shard(user_id)
return await shard.query('SELECT * FROM users WHERE id = $1', [user_id])
# Old DB → Read-only → Archive → Delete
Problèmes complexes
Problème 1 : Cross-shard queries
-- Impossible avec sharding
SELECT u.name, o.total
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.total > 1000;
-- users et orders dans shards différents
Solutions :
A. Dénormalisation
-- Dupliquer user data dans orders
CREATE TABLE orders (
id INT,
user_id INT,
user_name VARCHAR, -- Dupliqué !
user_email VARCHAR, -- Dupliqué !
total DECIMAL
);
-- Query devient possible
SELECT user_name, total
FROM orders
WHERE total > 1000;
B. Application-level joins
# Fetch de chaque shard
orders = await fetch_orders_from_all_shards(total > 1000)
user_ids = [o.user_id for o in orders]
# Fetch users (chaque shard concerné)
users = await fetch_users_by_ids(user_ids)
# Join en mémoire
results = join_in_memory(users, orders)
C. Analytics database
Sharded OLTP DBs → ETL → Data Warehouse (Snowflake, BigQuery)
↓
Run analytics here
Problème 2 : Transactions distribuées
# Transaction cross-shard = complexe
async def transferMoney(from_user_id, to_user_id, amount):
shard1 = get_shard(from_user_id)
shard2 = get_shard(to_user_id)
# Si différents shards → transaction distribuée
if shard1 != shard2:
# Two-phase commit (2PC) ou Saga pattern
await distributed_transaction([
lambda: shard1.debit(from_user_id, amount),
lambda: shard2.credit(to_user_id, amount)
])
Éviter si possible :
- Garder transactions dans même shard
- Utiliser event sourcing
- Eventual consistency
Problème 3 : Resharding (ajouter shards)
# 2 shards → 4 shards
# hash(user_id) % 2 → hash(user_id) % 4
# → 50% des users changent de shard
# Migration massive requise
Solutions :
A. Consistent hashing
# Minimise data movement (10-20% au lieu de 50%)
from consistent_hash import ConsistentHash
ring = ConsistentHash()
ring.add_node('shard1')
ring.add_node('shard2')
# Ajouter shard3
ring.add_node('shard3') # Seulement 33% migré
B. Pre-sharding
# Créer 256 logical shards dès le début
# Mapper sur 4 physical shards
logical_shard = hash(user_id) % 256
physical_shard = logical_to_physical[logical_shard]
# Resharding = remapper (pas de data move)
Métriques et monitoring
Dashboard sharding
┌──────────────────────────────────────┐
│ Shard Health │
├──────────────────────────────────────┤
│ Shard 1: 🟢 Healthy (45% CPU) │
│ Shard 2: 🟢 Healthy (52% CPU) │
│ Shard 3: 🟡 Warning (78% CPU) │
│ Shard 4: 🟢 Healthy (41% CPU) │
│ │
│ Data Distribution: │
│ Shard 1: 240GB (24%) │
│ Shard 2: 260GB (26%) │
│ Shard 3: 280GB (28%) ⚠️ Hotspot │
│ Shard 4: 220GB (22%) │
│ │
│ Query Performance: │
│ p50: 12ms (✅ < 50ms) │
│ p95: 45ms (✅ < 200ms) │
│ p99: 120ms (🟡 target: < 500ms) │
└──────────────────────────────────────┘
Cas réel : SaaS B2B (tenant sharding)
Situation
- 5000 tenants
- 10 tenants = 70% traffic (déséquilibre)
- 1 DB saturée
Solution
# Stratégie hybrid
PREMIUM_TENANTS = [1, 5, 12, 23, 45] # Gros clients
def get_db(tenant_id):
if tenant_id in PREMIUM_TENANTS:
return dedicated_dbs[tenant_id] # Shard dédié
# Petits tenants : hash sharding
shard_id = hash(tenant_id) % 10
return shared_shards[shard_id]
Résultats
- Performance : p95 latency 450ms → 85ms (-81%)
- Isolation : 0 noisy neighbor problems
- Coût : +$3k/mois (5 shards dédiés)
- Satisfaction : Enterprise clients 9.2/10
Conclusion
Sharding n’est pas votre premier choix.
Alternatives plus simples :
- Vertical scaling
- Read replicas
- Caching
- Partitioning
Mais si nécessaire :
- Choisir bonne stratégie (hash, range, geo)
- Migration progressive (6-12 mois)
- Monitoring intensif
Complexité réelle :
- Cross-shard queries
- Transactions distribuées
- Resharding
Commencez simple. Shardez seulement si vraiment requis.
Et vous, avez-vous shardé ? Retours ?