Retour au cours

data / sql

Sharding : architecture et implémentation

Leçon 241 exercice

Explication

Ce que vous allez apprendre

  • Distinguer clairement le sharding (plusieurs serveurs) du partitionnement (un seul serveur)
  • Choisir une clé de sharding pertinente pour garder ensemble les données jointes
  • Comprendre pourquoi un simple modulo casse tout lors d'un resharding
  • Découvrir le hachage cohérent comme solution à ce problème
  • Connaître les deux approches de routage : postgres_fdw et sharding applicatif

Dans quel contexte ?

Une plateforme SaaS multi-tenant atteint les limites physiques d'un seul serveur PostgreSQL : plus de CPU, plus de disque à ajouter sur cette machine. Il faut répartir les données de la table clients sur plusieurs serveurs distincts. Le choix de la clé de sharding (souvent client_id) devient alors une décision structurante à ne pas prendre à la légère, comme le détaille cette leçon.

D'abord, la limite du partitionnement déjà vu

Le partitionnement (leçon 19) découpe une table en tiroirs, mais tous ces tiroirs restent rangés dans le MÊME meuble, sur la même machine physique. Un jour, même la machine la plus puissante atteint ses limites de stockage ou de calcul.

La solution : distribuer sur plusieurs serveurs

Le sharding répond à cette limite en répartissant les données ENTRE plusieurs serveurs indépendants, chacun hébergeant une portion ("un shard"). Ce n'est plus un seul meuble avec plusieurs tiroirs : ce sont plusieurs meubles, dans des pièces différentes.

Étape 1 : la décision la plus importante, la clé de sharding

Il faut décider quel critère détermine sur quel serveur atterrit chaque ligne — souvent un hachage de l'identifiant client. Ce choix est structurant : si des données fréquemment jointes (une commande ET ses lignes) se retrouvent sur des shards différents, chaque jointure devient extrêmement coûteuse, car un JOIN natif ne peut pas franchir la frontière entre deux serveurs.

Il reste un problème : ajouter un serveur casse tout

Avec un simple modulo (hash(id) % nb_shards), ajouter un seul nouveau serveur change le résultat du modulo pour presque toutes les clés existantes. Il faudrait alors redéplacer la quasi-totalité des données, ce qui n'est pas praticable en production.

ApprocheRôleLimite
Partitionnement (leçon 19)Découpe UNE table sur UN serveurNe résout pas la saturation physique
ShardingRépartit les données sur PLUSIEURS serveursPas de JOIN natif entre shards
Modulo simple (hash(id) % n)Répartition initialeCasse tout lors d'un resharding
Hachage cohérentRépartition stableNe redéplace qu'une petite fraction des clés

Piège fréquent

Choisir une clé de sharding qui sépare des données fréquemment jointes (par exemple sharder commandes par id de commande et lignes_commande par id de ligne) rend chaque jointure quasiment impossible à faire efficacement, car un JOIN ne peut pas franchir la frontière entre deux serveurs.

Étape 2 : le hachage cohérent limite les dégâts

Le hachage cohérent résout précisément ce problème : lors d'un changement du nombre de shards, seule une petite fraction des clés doit être redéplacée, au lieu de presque toutes. C'est ce qui rend le resharding supportable en production.

Étape 3 : comment router une requête vers le bon shard

Deux approches existent : un routeur SQL (par exemple postgres_fdw, qui fédère plusieurs bases distantes) ou un sharding applicatif, où le client calcule lui-même le shard cible avant même d'ouvrir la connexion.

Piège à retenir

Le sharding introduit une vraie complexité (pas de JOIN inter-shards, transactions distribuées coûteuses avec le 2PC) : ce n'est pas un réflexe à appliquer par anticipation, mais une solution de dernier recours, réservée aux volumes qui dépassent réellement un serveur unique.

Vers la suite

Après avoir vu comment distribuer les données brutes, la prochaine leçon revient à un format de données bien précis : le JSONB, avec des outils encore plus avancés que ceux vus en leçon 20.

Commandes & code

Sharding : architecture et implémentation

Le sharding distribue une table sur PLUSIEURS serveurs, contrairement au partitionnement qui reste sur un seul.

sql
-- Approche 1 : Foreign Data Wrapper -- fédère plusieurs bases PostgreSQL distantes
CREATE EXTENSION IF NOT EXISTS postgres_fdw;

CREATE SERVER shard_1 FOREIGN DATA WRAPPER postgres_fdw
    OPTIONS (host 'db-shard-1.interne', dbname 'clients_shard1', port '5432');
CREATE SERVER shard_2 FOREIGN DATA WRAPPER postgres_fdw
    OPTIONS (host 'db-shard-2.interne', dbname 'clients_shard2', port '5432');

CREATE USER MAPPING FOR app_user SERVER shard_1
    OPTIONS (user 'app_user', password 'secret');
CREATE USER MAPPING FOR app_user SERVER shard_2
    OPTIONS (user 'app_user', password 'secret');

-- Table locale partitionnée dont chaque partition pointe vers un shard distant
CREATE TABLE clients (
    id     BIGINT NOT NULL,
    nom    VARCHAR(200),
    region VARCHAR(50)
) PARTITION BY HASH (id);

CREATE FOREIGN TABLE clients_shard1 PARTITION OF clients
    FOR VALUES WITH (MODULUS 2, REMAINDER 0)
    SERVER shard_1;
CREATE FOREIGN TABLE clients_shard2 PARTITION OF clients
    FOR VALUES WITH (MODULUS 2, REMAINDER 1)
    SERVER shard_2;

-- Une requête sur "id" connu est routée UNIQUEMENT vers le bon shard (partition pruning distant)
SELECT * FROM clients WHERE id = 42;

-- Approche 2 : sharding applicatif -- le client calcule lui-même le shard cible
-- def shard_pour(client_id: int, nb_shards: int) -> int:
--     return hash(client_id) % nb_shards
-- L'application ouvre directement la connexion au bon shard, sans routeur SQL intermédiaire

-- Hachage cohérent (consistent hashing) : limite les clés déplacées lors d'un resharding,
-- contrairement à un simple modulo qui redistribue presque toutes les clés dès qu'on change nb_shards.
-- Principe : chaque shard occupe plusieurs points sur un anneau de hachage ; une clé est assignée
-- au premier point de shard rencontré en tournant sur l'anneau à partir de hash(clé).

-- Table de routage centrale (shard map), généralement mise en cache côté application
CREATE TABLE shard_map (
    plage_debut BIGINT NOT NULL,
    plage_fin   BIGINT NOT NULL,
    shard_hote  VARCHAR(100) NOT NULL
);

-- Limite fondamentale : pas de JOIN natif entre deux shards différents
-- -> choisir une clé de sharding qui garde ENSEMBLE les données fréquemment jointes
-- (sharder par client_id garde commandes + lignes_commande du même client sur le même shard)

-- Transactions distribuées : PostgreSQL propose le 2PC entre shards, coûteux et rarement utilisé
-- en pratique -- on préfère la cohérence éventuelle gérée côté application
PREPARE TRANSACTION 'transfert_456';   -- phase 1 : chaque shard promet de committer
COMMIT PREPARED 'transfert_456';        -- phase 2 : validation définitive

Résumé

  • Le sharding distribue horizontalement ENTRE plusieurs serveurs, contrairement au partitionnement qui reste local à un serveur.
  • postgres_fdw + partitions étrangères permettent de router une requête vers le bon shard depuis SQL directement.
  • Le hachage cohérent limite le nombre de clés à redéplacer lors de l'ajout/retrait d'un shard.
  • Choisir la clé de sharding en fonction des données fréquemment jointes ensemble : les JOIN inter-shards sont impossibles ou très coûteux.
  • Le 2PC (PREPARE TRANSACTION) existe pour les transactions distribuées mais reste coûteux : à réserver aux cas critiques.

Exercices pratiques

1 disponible
1

Mission : revoir une stratégie de sharding qui casse toutes les jointures

Objectif : Diagnostiquer une clé de sharding mal choisie qui sépare des données jointes, et raisonner sur le resharding et les transactions distribuées.

Contexte

La plateforme SaaS a shardé commandes par id de commande et lignes_commande par id de ligne, chacun réparti avec un simple hash(id) % nb_shards. Depuis, chaque page "détail de commande" (qui joint commandes et lignes_commande) est devenue extrêmement lente, et l'équipe infra redoute d'ajouter un troisième shard car elle sait que cela va redéplacer une grande partie des données existantes.

Résoudre l’exercice →