Conception avancée : partitionnement, réplication et CAP#

Hide code cell source

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import pandas as pd
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

À mesure qu’une base de données grandit en volume et en trafic, les approches standards atteignent leurs limites. Une table de 10 milliards de lignes ne se gère pas comme une table de 10 000 lignes. Un service consulté par un million d’utilisateurs simultanés ne peut pas reposer sur un seul serveur. Ce chapitre couvre les techniques de partitionnement pour gérer le volume, de réplication pour la disponibilité, et le théorème CAP pour comprendre les compromis fondamentaux des systèmes distribués.

Partitionnement#

Pourquoi partitionner ?#

Le partitionnement consiste à diviser une grande table en sous-ensembles physiques appelés partitions, tout en conservant une vue logique unifiée. Les bénéfices sont multiples :

  • Performance : une requête sur janvier 2024 ne scanne que la partition correspondante

  • Maintenance : archiver ou supprimer une période entière devient instantané (DROP PARTITION)

  • Parallélisme : plusieurs partitions peuvent être lues simultanément

import sqlite3

# Simulation du concept de partitionnement par plage de dates
conn = sqlite3.connect(":memory:")

# Schéma : une table de ventes avec des partitions simulées
conn.executescript("""
    CREATE TABLE ventes_2022 (
        id INTEGER PRIMARY KEY,
        date_vente TEXT,
        montant REAL,
        produit TEXT
    );
    CREATE TABLE ventes_2023 (
        id INTEGER PRIMARY KEY,
        date_vente TEXT,
        montant REAL,
        produit TEXT
    );
    CREATE TABLE ventes_2024 (
        id INTEGER PRIMARY KEY,
        date_vente TEXT,
        montant REAL,
        produit TEXT
    );
""")

# Insertion de données
import random
from datetime import date, timedelta

random.seed(42)
produits = ["Laptop", "Souris", "Clavier", "Écran", "Casque"]

for annee, table in [(2022, "ventes_2022"), (2023, "ventes_2023"), (2024, "ventes_2024")]:
    debut = date(annee, 1, 1)
    for i in range(1, 201):
        j = random.randint(0, 364)
        d = debut + timedelta(days=j)
        conn.execute(
            f"INSERT INTO {table} VALUES (?, ?, ?, ?)",
            (i, d.isoformat(), round(random.uniform(20, 2000), 2), random.choice(produits))
        )
conn.commit()

# Vue unifiée via UNION ALL (équivalent d'une table partitionnée)
conn.execute("""
    CREATE VIEW ventes AS
        SELECT *, '2022' AS annee FROM ventes_2022
        UNION ALL
        SELECT *, '2023' AS annee FROM ventes_2023
        UNION ALL
        SELECT *, '2024' AS annee FROM ventes_2024
""")

# Requête sur une seule partition (scan réduit)
df = pd.read_sql_query("""
    SELECT annee, COUNT(*) AS nb_ventes, ROUND(SUM(montant), 2) AS total
    FROM ventes
    GROUP BY annee
    ORDER BY annee
""", conn)
print(df.to_string(index=False))
annee  nb_ventes     total
 2022        200 188766.69
 2023        200 211351.51
 2024        200 201347.84

Stratégies de partitionnement#

Partitionnement par plage (RANGE) — le plus courant pour les données temporelles :

-- PostgreSQL
CREATE TABLE ventes (
    id          SERIAL,
    date_vente  DATE NOT NULL,
    montant     NUMERIC(10,2),
    produit     TEXT
) PARTITION BY RANGE (date_vente);

CREATE TABLE ventes_2023 PARTITION OF ventes
    FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');

CREATE TABLE ventes_2024 PARTITION OF ventes
    FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

Partitionnement par liste (LIST) — par valeur discrète (région, statut) :

CREATE TABLE commandes (
    id     SERIAL,
    region TEXT,
    total  NUMERIC
) PARTITION BY LIST (region);

CREATE TABLE commandes_europe PARTITION OF commandes
    FOR VALUES IN ('FR', 'DE', 'ES', 'IT');

CREATE TABLE commandes_amerique PARTITION OF commandes
    FOR VALUES IN ('US', 'CA', 'MX');

Partitionnement par hachage (HASH) — distribution uniforme sans logique métier :

CREATE TABLE sessions (
    id       UUID,
    user_id  INT,
    données  JSONB
) PARTITION BY HASH (user_id);

CREATE TABLE sessions_0 PARTITION OF sessions
    FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE sessions_1 PARTITION OF sessions
    FOR VALUES WITH (MODULUS 4, REMAINDER 1);
-- etc.

Hide code cell source

# Visualisation des stratégies de partitionnement
fig, axes = plt.subplots(1, 3, figsize=(14, 5))

strategies = [
    {
        "titre": "RANGE (plage)",
        "couleur": "#4C72B0",
        "partitions": ["Jan-Mar", "Avr-Jun", "Jul-Sep", "Oct-Déc"],
        "description": "Données temporelles\narchivage facile"
    },
    {
        "titre": "LIST (liste)",
        "couleur": "#DD8452",
        "partitions": ["Europe", "Amériques", "Asie", "Autres"],
        "description": "Valeurs discrètes\nrégions, statuts"
    },
    {
        "titre": "HASH (hachage)",
        "couleur": "#55A868",
        "partitions": ["Hash 0", "Hash 1", "Hash 2", "Hash 3"],
        "description": "Distribution uniforme\nsans logique métier"
    }
]

for ax, s in zip(axes, strategies):
    couleurs = [s["couleur"]] * 4
    ax.barh(s["partitions"], [1, 1, 1, 1], color=couleurs, alpha=0.8, edgecolor="white", linewidth=2)
    ax.set_title(s["titre"], fontweight="bold", fontsize=13)
    ax.set_xlabel(s["description"], fontsize=10)
    ax.set_xlim(0, 1.5)
    ax.tick_params(axis="x", which="both", bottom=False, labelbottom=False)
    for i, p in enumerate(s["partitions"]):
        ax.text(0.5, i, p, ha="center", va="center", fontweight="bold", color="white", fontsize=11)

plt.suptitle("Stratégies de partitionnement", fontsize=15, fontweight="bold", y=1.02)
plt.tight_layout()
plt.show()
_images/ef2c26533ffce74e0f26d61251d286c95de107c92f3ddf4b2fbaaf8e6ad3dad1.png

Partition pruning#

L’élimination de partitions (partition pruning) est le mécanisme par lequel le planificateur de requêtes ignore automatiquement les partitions non pertinentes :

-- Cette requête ne scanne QUE la partition ventes_2024
SELECT SUM(montant)
FROM ventes
WHERE date_vente BETWEEN '2024-01-01' AND '2024-12-31';

-- EXPLAIN révèle le pruning
EXPLAIN SELECT SUM(montant) FROM ventes
WHERE date_vente >= '2024-01-01';
-- → Seq Scan on ventes_2024  (les autres partitions ignorées)

Réplication#

Principe et topologies#

La réplication consiste à maintenir des copies identiques d’une base sur plusieurs serveurs. Elle sert deux objectifs distincts :

  • Haute disponibilité : si le serveur principal tombe, un réplica prend le relais

  • Scalabilité en lecture : les requêtes SELECT sont distribuées sur plusieurs réplicas

Hide code cell source

# Schéma des topologies de réplication
fig, axes = plt.subplots(1, 2, figsize=(13, 6))

def dessiner_serveur(ax, x, y, label, couleur, taille=0.8):
    rect = patches.FancyBboxPatch(
        (x - taille/2, y - 0.3), taille, 0.6,
        boxstyle="round,pad=0.05",
        facecolor=couleur, edgecolor="white", linewidth=2
    )
    ax.add_patch(rect)
    ax.text(x, y, label, ha="center", va="center",
            fontweight="bold", color="white", fontsize=10)

def fleche(ax, x1, y1, x2, y2, label="", couleur="gray"):
    ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle="->", color=couleur, lw=2))
    mx, my = (x1+x2)/2, (y1+y2)/2
    if label:
        ax.text(mx + 0.1, my, label, fontsize=9, color=couleur)

# Topologie 1 : Primaire → Réplicas (streaming)
ax = axes[0]
ax.set_xlim(0, 4)
ax.set_ylim(0, 4)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("Réplication streaming\n(1 primaire → N réplicas)", fontweight="bold")

dessiner_serveur(ax, 2, 3.2, "Primaire\n(écriture)", "#C44E52")
dessiner_serveur(ax, 1, 1.5, "Réplica 1\n(lecture)", "#4C72B0")
dessiner_serveur(ax, 2, 1.5, "Réplica 2\n(lecture)", "#4C72B0")
dessiner_serveur(ax, 3, 1.5, "Réplica 3\n(lecture)", "#4C72B0")

for rx in [1, 2, 3]:
    fleche(ax, 2, 3.0, rx, 1.8, couleur="#C44E52")

ax.text(2, 0.8, "WAL stream (asynchrone)", ha="center", fontsize=9, style="italic")

# Topologie 2 : Multi-primaire
ax = axes[1]
ax.set_xlim(0, 4)
ax.set_ylim(0, 4)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("Multi-primaire\n(lecture + écriture partout)", fontweight="bold")

positions = [(2, 3.2), (0.8, 1.5), (3.2, 1.5)]
labels = ["Primaire A\n(Paris)", "Primaire B\n(New York)", "Primaire C\n(Tokyo)"]
for (x, y), lbl in zip(positions, labels):
    dessiner_serveur(ax, x, y, lbl, "#55A868")

# Flèches bidirectionnelles
for i, (x1, y1) in enumerate(positions):
    for j, (x2, y2) in enumerate(positions):
        if i < j:
            fleche(ax, x1, y1 - 0.3, x2, y2 + 0.3, couleur="#55A868")
            fleche(ax, x2, y2 - 0.3, x1, y1 + 0.3, couleur="#55A868")

ax.text(2, 0.8, "Synchronisation bidirectionnelle", ha="center", fontsize=9, style="italic")

plt.tight_layout()
plt.show()
_images/671c6ee1d4a4e422bf1456cc7b3312219060c6c36f1e6ef0eb67836c4d7c6d89.png

Réplication synchrone vs asynchrone#

Hide code cell source

# Comparaison latence / cohérence
categories = ["Latence écriture", "Cohérence données", "Risque perte données", "Disponibilité"]
sync = [2, 10, 10, 6]       # synchrone : latence haute, cohérence parfaite
async_ = [9, 5, 3, 9]       # asynchrone : rapide, léger risque

x = np.arange(len(categories))
width = 0.35

fig, ax = plt.subplots(figsize=(10, 5))
bars1 = ax.bar(x - width/2, sync, width, label="Synchrone", color="#4C72B0", alpha=0.85)
bars2 = ax.bar(x + width/2, async_, width, label="Asynchrone", color="#DD8452", alpha=0.85)

ax.set_ylabel("Score (0-10)", fontsize=12)
ax.set_title("Réplication synchrone vs asynchrone", fontsize=14, fontweight="bold")
ax.set_xticks(x)
ax.set_xticklabels(categories, fontsize=11)
ax.set_ylim(0, 12)
ax.legend(fontsize=11)
ax.bar_label(bars1, padding=3, fontsize=10)
ax.bar_label(bars2, padding=3, fontsize=10)
plt.tight_layout()
plt.show()
_images/661f2515bc6f04cfa997682cd3a6a0c038562b1f9c6c4e31bd093f350ea59caa.png

Configuration PostgreSQL pour la réplication :

-- Sur le primaire (postgresql.conf)
wal_level = replica
max_wal_senders = 10
synchronous_commit = on   -- ou 'off' pour asynchrone

-- Réplication synchrone pour un réplica critique
synchronous_standby_names = 'réplica_paris'

-- Sur le réplica (recovery.conf / postgresql.auto.conf)
primary_conninfo = 'host=primaire port=5432 user=replicateur'

Failover automatique#

Outils de haute disponibilité PostgreSQL

  • Patroni : gestionnaire de cluster HA, s’appuie sur etcd/Consul pour l’élection du leader

  • pgBouncer : pooler de connexions, transparent pour l’application lors d’un failover

  • Repmgr : gestion de la réplication et failover automatique

  • pg_auto_failover : solution intégrée Citus/Microsoft

En production, on déploie typiquement : 1 primaire + 2 réplicas + 1 pooler + 1 service de découverte.

Sharding#

Le sharding va plus loin que la réplication : il distribue les données elles-mêmes sur plusieurs serveurs independants (shards). Chaque shard est une base à part entière, responsable d’un sous-ensemble des données.

Hide code cell source

# Simulation du sharding par user_id
import hashlib

def shard_pour_user(user_id, nb_shards=4):
    """Détermine le shard d'un utilisateur par hachage cohérent."""
    h = int(hashlib.md5(str(user_id).encode()).hexdigest(), 16)
    return h % nb_shards

# Distribution de 1000 utilisateurs sur 4 shards
nb_shards = 4
distribution = {i: 0 for i in range(nb_shards)}
for uid in range(1, 1001):
    shard = shard_pour_user(uid, nb_shards)
    distribution[shard] += 1

fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Distribution
axes[0].bar([f"Shard {i}" for i in range(nb_shards)],
            list(distribution.values()),
            color=["#4C72B0", "#DD8452", "#55A868", "#C44E52"],
            alpha=0.85, edgecolor="white", linewidth=1.5)
axes[0].set_title("Distribution des utilisateurs\npar hachage (1000 users, 4 shards)", fontweight="bold")
axes[0].set_ylabel("Nombre d'utilisateurs")
for i, v in enumerate(distribution.values()):
    axes[0].text(i, v + 5, str(v), ha="center", fontweight="bold")

# Problème du resharding
anciens = [250, 250, 250, 250]
nouveaux = [200, 200, 200, 200, 200]  # passage à 5 shards
axes[1].bar([f"S{i}" for i in range(4)], anciens,
            color="#4C72B0", alpha=0.6, label="4 shards (avant)")
axes[1].bar([f"S{i}'" for i in range(5)], nouveaux,
            color="#C44E52", alpha=0.6, label="5 shards (après)")
axes[1].set_title("Resharding : déplacement de données\n(problème du hachage simple)", fontweight="bold")
axes[1].set_ylabel("Utilisateurs par shard")
axes[1].legend()

plt.tight_layout()
plt.show()
_images/06c11d50da1f3365817087a8140f76d06df86eadb48188daafe36c75ec71645a.png

Hachage cohérent (consistent hashing)#

Le hachage cohérent minimise les déplacements de données lors d’un resharding :

Hide code cell source

# Visualisation du hachage cohérent (anneau)
fig, ax = plt.subplots(figsize=(8, 8))
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("Hachage cohérent — anneau de tokens", fontsize=14, fontweight="bold")

# Anneau
cercle = plt.Circle((0, 0), 1, fill=False, color="gray", linewidth=2)
ax.add_patch(cercle)
ax.set_xlim(-1.6, 1.6)
ax.set_ylim(-1.6, 1.6)

# Positions des shards sur l'anneau
shards = [
    ("Shard A", 0, "#4C72B0"),
    ("Shard B", np.pi/2, "#DD8452"),
    ("Shard C", np.pi, "#55A868"),
    ("Shard D", 3*np.pi/2, "#C44E52"),
]

for label, angle, couleur in shards:
    x, y = np.cos(angle), np.sin(angle)
    ax.plot(x, y, "o", markersize=20, color=couleur, zorder=5)
    ax.text(x * 1.35, y * 1.35, label, ha="center", va="center",
            fontweight="bold", fontsize=11, color=couleur)

# Quelques clés distribuées
np.random.seed(42)
for i in range(12):
    angle = np.random.uniform(0, 2*np.pi)
    x, y = np.cos(angle) * 0.85, np.sin(angle) * 0.85
    ax.plot(x, y, "x", markersize=8, color="gray", markeredgewidth=2)

ax.text(0, 0, "Anneau\nde tokens\n[0, 2π]", ha="center", va="center",
        fontsize=10, color="gray", style="italic")

# Légende
ax.text(0, -1.5, "× clés de données    ● nœuds de shard\n"
        "Chaque clé appartient au premier shard rencontré dans le sens horaire",
        ha="center", fontsize=9, color="gray")
plt.tight_layout()
plt.show()
_images/b302785267f097de501cb2483196e865d39eb0fa7cdc49ceb9e5925c26c9a132.png

Le théorème CAP#

Énoncé#

Formulé par Eric Brewer (2000) et démontré par Gilbert & Lynch (2002), le théorème CAP stipule qu’un système distribué ne peut garantir simultanément que deux des trois propriétés suivantes :

  • Cohérence (Consistency) : tous les nœuds voient les mêmes données au même instant

  • Disponibilité (Availability) : chaque requête reçoit une réponse (sans garantie qu’elle soit à jour)

  • Tolérance au partitionnement (Partition tolerance) : le système continue de fonctionner malgré des pannes réseau

Hide code cell source

# Triangle CAP
fig, ax = plt.subplots(figsize=(9, 8))
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("Théorème CAP", fontsize=16, fontweight="bold")

# Triangle
sommets = np.array([[0, 0], [4, 0], [2, 3.46]])
triangle = plt.Polygon(sommets, fill=False, edgecolor="gray", linewidth=2, linestyle="--")
ax.add_patch(triangle)
ax.set_xlim(-1.5, 5.5)
ax.set_ylim(-1.5, 4.5)

# Sommets
labels_sommets = [
    ("Disponibilité\n(Availability)", 0, 0, "#DD8452"),
    ("Cohérence\n(Consistency)", 4, 0, "#4C72B0"),
    ("Tolérance réseau\n(Partition)", 2, 3.46, "#55A868"),
]
for label, x, y, couleur in labels_sommets:
    ax.plot(x, y, "o", markersize=25, color=couleur, zorder=5, alpha=0.8)
    decalage_x = -0.8 if x == 0 else (0.8 if x == 4 else 0)
    decalage_y = -0.6 if y == 0 else 0.5
    ax.text(x + decalage_x, y + decalage_y, label,
            ha="center", va="center", fontweight="bold", fontsize=11)

# Zones AP, CP, CA
systemes = [
    ("AP\nCassandra\nCouchDB\nDynamoDB", 1.0, 1.73, "#DD8452", 0.25),
    ("CP\nMongoDB\nRedis\nZooKeeper", 3.0, 1.73, "#4C72B0", 0.25),
    ("CA\nPostgreSQL\nMySQL\n(1 nœud)", 2.0, 0.3, "#55A868", 0.25),
]
for label, x, y, couleur, alpha in systemes:
    ax.text(x, y, label, ha="center", va="center",
            fontsize=9, color=couleur,
            bbox=dict(boxstyle="round,pad=0.3", facecolor=couleur, alpha=alpha))

ax.text(2, -1.2,
        "En présence d'une partition réseau (P inévitable en prod),\n"
        "il faut choisir entre Cohérence (CP) ou Disponibilité (AP).",
        ha="center", fontsize=10, style="italic", color="gray")
plt.tight_layout()
plt.show()
_images/fcb7d8e77971e5cfcde5cb5d685ef7588beac406132dba572603e182b5bf645f.png

PACELC : au-delà de CAP#

CAP raisonne en présence d’une panne. Le modèle PACELC étend ce cadre au cas normal :

  • En cas de Partition (P) → choisir entre Availability (A) et Consistency (C)

  • Else (E) (fonctionnement normal) → choisir entre Latency (L) et Consistency (C)

Hide code cell source

# Tableau PACELC
donnees = {
    "Système": ["DynamoDB", "Cassandra", "MongoDB", "PostgreSQL", "MySQL Cluster"],
    "Partition →": ["A", "A", "C", "C", "C"],
    "Normal →": ["L", "L", "L", "C", "C"],
    "Classification": ["PA/EL", "PA/EL", "PC/EL", "PC/EC", "PC/EC"],
    "Usage typique": [
        "E-commerce, IoT",
        "Logs, time-series",
        "Apps web, CMS",
        "Finance, ERP",
        "Télécom, sessions"
    ]
}
df = pd.DataFrame(donnees)

fig, ax = plt.subplots(figsize=(12, 3))
ax.axis("off")
table = ax.table(
    cellText=df.values,
    colLabels=df.columns,
    cellLoc="center",
    loc="center"
)
table.auto_set_font_size(False)
table.set_fontsize(10)
table.scale(1, 1.8)

couleurs_header = "#2C3E50"
for j in range(len(df.columns)):
    table[0, j].set_facecolor(couleurs_header)
    table[0, j].set_text_props(color="white", fontweight="bold")

for i in range(1, len(df) + 1):
    c = "#f7f7f7" if i % 2 == 0 else "white"
    for j in range(len(df.columns)):
        table[i, j].set_facecolor(c)

plt.title("Modèle PACELC : comportement en cas de partition et en fonctionnement normal",
          fontsize=12, fontweight="bold", pad=20)
plt.tight_layout()
plt.show()
_images/104281f7d77550ebbb6cd34a6c71c2b780778be67fc33c6f9abf86e90c54ac02.png

Base de données distribuée : exemple avec Citus#

Citus est une extension PostgreSQL qui transforme une instance PostgreSQL standard en base distribuée. Elle est idéale pour passer à l’échelle sans changer d’interface SQL.

-- Installation de l'extension Citus
CREATE EXTENSION citus;

-- Déclarer les nœuds workers
SELECT citus_add_node('worker1', 5432);
SELECT citus_add_node('worker2', 5432);
SELECT citus_add_node('worker3', 5432);

-- Distribuer une table existante par user_id
-- (les données sont automatiquement réparties sur les workers)
SELECT create_distributed_table('commandes', 'user_id');

-- Co-localiser les tables liées (même clé de distribution)
SELECT create_distributed_table('paiements', 'user_id');

-- Les jointures sur user_id s'exécutent localement sur chaque shard
SELECT u.nom, COUNT(c.id) AS nb_commandes
FROM utilisateurs u
JOIN commandes c ON u.id = c.user_id
GROUP BY u.nom
ORDER BY nb_commandes DESC
LIMIT 10;
-- → Citus exécute cette requête en parallèle sur tous les shards

Règles de conception pour Citus

  1. Choisir la bonne clé de distribution : toutes les tables liées doivent partager la même clé (ex: tenant_id, user_id)

  2. Éviter les jointures cross-shard : elles nécessitent des transferts réseau coûteux

  3. Tables de référence : les petites tables (pays, catégories) peuvent être répliquées sur tous les shards avec create_reference_table()

  4. Surveiller le déséquilibre : certaines clés populaires créent des « hot shards » — utiliser des clés à haute cardinalité

Récapitulatif : choisir la bonne architecture#

Hide code cell source

# Arbre de décision
fig, ax = plt.subplots(figsize=(14, 9))
ax.set_xlim(0, 14)
ax.set_ylim(0, 9)
ax.axis("off")
ax.set_title("Arbre de décision : quelle architecture choisir ?", fontsize=14, fontweight="bold")

def boite(ax, x, y, texte, couleur, w=2.2, h=0.7):
    rect = patches.FancyBboxPatch(
        (x - w/2, y - h/2), w, h,
        boxstyle="round,pad=0.1",
        facecolor=couleur, edgecolor="white", linewidth=2, alpha=0.9
    )
    ax.add_patch(rect)
    ax.text(x, y, texte, ha="center", va="center",
            fontweight="bold", color="white", fontsize=9, wrap=True)

def lien(ax, x1, y1, x2, y2, label=""):
    ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle="->", color="#555", lw=1.5))
    if label:
        ax.text((x1+x2)/2 + 0.15, (y1+y2)/2, label, fontsize=8, color="#555")

boite(ax, 7, 8.3, "Données relationnelles ?", "#2C3E50", w=3)

boite(ax, 3.5, 6.8, "Volume < 1 To\net 1 serveur ?", "#34495E", w=3)
boite(ax, 10.5, 6.8, "Documents /\nClé-valeur / Graph ?", "#34495E", w=3)

boite(ax, 2, 5.2, "PostgreSQL\n(standalone)", "#4C72B0", w=2.5)
boite(ax, 5, 5.2, "Besoin\nHA ?", "#34495E", w=2)
boite(ax, 9, 5.2, "MongoDB /\nCassandra", "#DD8452", w=2.5)
boite(ax, 12, 5.2, "Redis /\nNeo4j", "#DD8452", w=2.5)

boite(ax, 4, 3.7, "Primaire +\nRéplicas streaming", "#55A868", w=3)
boite(ax, 7, 3.7, "Patroni +\nfailover auto", "#55A868", w=3)

boite(ax, 3.5, 5.2, "Besoin\nscale-out ?", "#34495E", w=2)
boite(ax, 10.5, 3.7, "Citus /\nTimescaleDB", "#55A868", w=2.5)

lien(ax, 7, 7.95, 3.5, 7.15, "Oui")
lien(ax, 7, 7.95, 10.5, 7.15, "Non")
lien(ax, 3.5, 6.45, 2, 5.55, "Oui")
lien(ax, 3.5, 6.45, 3.5, 5.55, "Non")
lien(ax, 3.5, 4.85, 4, 4.05, "Simple")
lien(ax, 3.5, 4.85, 7, 4.05, "Critique")
lien(ax, 10.5, 6.45, 9, 5.55, "Document")
lien(ax, 10.5, 6.45, 12, 5.55, "KV/Graph")
lien(ax, 10.5, 6.45, 10.5, 4.05, "SQL distribué")

plt.tight_layout()
plt.show()
_images/5c6be5235b224755530100e52892dc0dcd7d3cfe3860ff1e8fb84a9848ef08dc.png

Résumé#

Technique

Problème résolu

Coût

Partitionnement

Volume (tables géantes)

Complexité de conception

Réplication

Disponibilité + lecture scale-out

Lag, cohérence éventuelle

Sharding

Écriture scale-out

Complexité applicative

CAP/PACELC

Comprendre les compromis

(framework de raisonnement)

Citus

Scale-out PostgreSQL transparent

Infrastructure distribuée

La règle d’or : n’ajouter de complexité que lorsqu’elle est justifiée par des mesures. Un PostgreSQL bien configuré sur un bon serveur gère des dizaines de milliers de requêtes par seconde. La distribution est une solution à des problèmes à grande échelle, pas une architecture de départ.