Deep EVM #26 : Sharding vs partitionnement — Architecture pour tables massives
Engineering Team
La différence fondamentale
Le partitionnement et le sharding divisent tous les deux les données en morceaux plus petits, mais opèrent à des niveaux différents. Le partitionnement divise une table sur le même serveur de base de données. Le sharding distribue les données sur plusieurs serveurs. Comprendre quand utiliser chaque approche est essentiel.
Partitionnement (même serveur)
┌─────────────────────────────┐
│ Serveur PostgreSQL │
│ ┌─────────┐ ┌─────────┐ │
│ │ Part. 1 │ │ Part. 2 │ │
│ └─────────┘ └─────────┘ │
│ ┌─────────┐ ┌─────────┐ │
│ │ Part. 3 │ │ Part. 4 │ │
│ └─────────┘ └─────────┘ │
└─────────────────────────────┘
Avantages : transactions ACID, requêtes cross-partition transparentes, pas de changement d’application.
Sharding (serveurs multiples)
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Shard 1 │ │ Shard 2 │ │ Shard 3 │
│ (PG) │ │ (PG) │ │ (PG) │
└──────────┘ └──────────┘ └──────────┘
↑ ↑ ↑
└──────────────┼──────────────┘
│
┌────────────────┐
│ Router/Proxy │
└────────────────┘
Avantages : mise à l’échelle horizontale du débit et du stockage, isolation des défaillances.
Hachage cohérent
Pour distribuer les données uniformément entre les shards :
use std::collections::BTreeMap;
struct ConsistentHash {
ring: BTreeMap<u64, ShardId>,
virtual_nodes: u32,
}
impl ConsistentHash {
fn get_shard(&self, key: &[u8]) -> ShardId {
let hash = xxhash(key);
// Trouver le premier nœud >= hash sur l'anneau
self.ring.range(hash..)
.next()
.or_else(|| self.ring.iter().next()) // Boucler
.map(|(_, shard)| *shard)
.unwrap()
}
}
Le hachage cohérent minimise la redistribution des données lors de l’ajout/suppression de shards.
Requêtes cross-shard
Le défi principal du sharding : les requêtes qui touchent plusieurs shards.
async fn search_all_shards(
shards: &[ShardConnection],
query: &str,
) -> Vec<Result> {
// Exécuter en parallèle sur tous les shards
let futures = shards.iter()
.map(|shard| shard.query(query));
let results = futures::future::join_all(futures).await;
// Fusionner et trier les résultats
results.into_iter()
.flat_map(|r| r.unwrap_or_default())
.sorted_by_key(|r| r.score)
.collect()
}
Quand choisir quoi
| Critère | Partitionnement | Sharding |
|---|---|---|
| Taille des données | < 1 TB | > 1 TB |
| Débit d’écriture | < 50K/s | > 50K/s |
| Transactions ACID | Oui | Limité |
| Complexité opérationnelle | Faible | Élevée |
| Requêtes cross-données | Transparentes | Complexes |
| Coût | Un serveur | Multiple serveurs |
Règle pratique : commencez par le partitionnement. Passez au sharding seulement quand un seul serveur ne suffit plus, malgré les optimisations.
Resharding
Ajouter des shards à un système existant est l’une des opérations les plus complexes :
- Ajouter les nouveaux shards
- Mettre à jour la fonction de hachage
- Migrer les données affectées en arrière-plan
- Basculer le routage progressivement
- Nettoyer les anciennes données
Le hachage cohérent minimise les données à déplacer — idéalement, seulement 1/N des données quand on passe de N à N+1 shards.
Conclusion
Le partitionnement est votre premier outil de mise à l’échelle — simple, transparent, pas de changement applicatif. Le sharding est le recours quand un seul serveur atteint ses limites. Planifiez votre clé de sharding soigneusement car en changer est extrêmement coûteux.