System Design pour Développeurs : Architecture à Grande Échelle
System Design
Architecture
Scalabilité
DevOps

System Design pour Développeurs : Architecture à Grande Échelle

FS
Fernand SOUALO
·
9 min read

System Design pour Développeurs : Architecture à Grande Échelle

Table of Contents#

  1. Pourquoi le System Design ?
  2. Fondamentaux
  3. Load Balancing
  4. Caching
  5. Bases de Données Distribuées
  6. Message Queues
  7. Microservices vs Monolithe
  8. CDN et Edge Computing
  9. Cas Pratiques
  10. Conclusion

Pourquoi le System Design ?#

Le code parfait ne suffit pas si l'architecture ne tient pas la charge. Le system design est l'art de concevoir des systèmes qui restent performants, fiables et maintenables à mesure qu'ils grandissent.

Génération du diagramme…
Architecture type haute disponibilité : CDN, Load Balancer, Cache, DB répliquée, Message Queue.

Fondamentaux#

Les 3 piliers du system design#

PilierDéfinitionMétriques
ScalabilitéCapacité à gérer la croissanceRequêtes/seconde, latence p99
DisponibilitéUptime du systèmeSLA (99.9% = 8.7h down/an)
ConsistanceCohérence des donnéesÉventuelle vs forte

Théorème CAP#

Dans un système distribué, vous ne pouvez garantir que 2 sur 3 :

  • Consistency : Toutes les lectures retournent la dernière écriture
  • Availability : Chaque requête reçoit une réponse
  • Partition Tolerance : Le système fonctionne malgré des pannes réseau

En pratique, les partitions réseau arrivent toujours. Le choix est donc entre CP (consistance forte, ex: PostgreSQL) et AP (haute disponibilité, ex: Cassandra).

Estimation de capacité#

// Exercice : dimensionner un système de messagerie

Utilisateurs actifs quotidiens : 10M
Messages par utilisateur/jour : 50
Taille moyenne d'un message : 200 bytes

Calculs :
- Messages/jour : 10M × 50 = 500M messages/jour
- Messages/seconde : 500M / 86400 ≈ 5800 msg/s
- Stockage/jour : 500M × 200B = 100 GB/jour
- Stockage/an : 100 GB × 365 = 36.5 TB/an
- Bandwidth : 5800 × 200B = 1.16 MB/s (écriture)

Load Balancing#

Algorithmes de répartition#

Round Robin      : Requêtes distribuées en cercle
Weighted Round   : Poids basé sur la capacité du serveur
Least Connections: Vers le serveur le moins chargé
IP Hash          : Même client → même serveur (stickiness)

Configuration Nginx#

Nginx
# nginx.conf
upstream app_servers {
    least_conn;  # Algorithme least connections

    server app1:3000 weight=3;  # 3x plus de trafic
    server app2:3000 weight=2;
    server app3:3000 weight=1;

    # Health checks
    server app4:3000 backup;  # Utilisé seulement si les autres tombent
}

server {
    listen 80;

    location / {
        proxy_pass http://app_servers;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # Timeouts
        proxy_connect_timeout 5s;
        proxy_read_timeout 30s;
    }

    # Health check endpoint
    location /health {
        proxy_pass http://app_servers;
        proxy_connect_timeout 1s;
        proxy_read_timeout 1s;
    }
}

Caching#

Stratégies de cache#

StratégieDescriptionQuand l'utiliser
Cache-AsideL'app vérifie le cache, sinon requête DBLecture intensive, données rarement modifiées
Write-ThroughÉcriture dans cache + DB simultanémentConsistance forte nécessaire
Write-BehindÉcriture dans cache, DB en arrière-planPerformance d'écriture critique
Read-ThroughLe cache requête la DB si cache missSimplification du code applicatif

Implémentation Redis#

TypeScript
// lib/cache.ts
import Redis from "ioredis";

const redis = new Redis(process.env.REDIS_URL!);

// Cache-Aside pattern
export async function cachedQuery<T>(
  key: string,
  queryFn: () => Promise<T>,
  ttl: number = 3600,
): Promise<T> {
  // 1. Vérifier le cache
  const cached = await redis.get(key);
  if (cached) {
    return JSON.parse(cached) as T;
  }

  // 2. Requête DB
  const data = await queryFn();

  // 3. Stocker dans le cache
  await redis.setex(key, ttl, JSON.stringify(data));

  return data;
}

// Invalidation ciblée
export async function invalidateCache(pattern: string) {
  const keys = await redis.keys(pattern);
  if (keys.length > 0) {
    await redis.del(...keys);
  }
}

// Utilisation
const posts = await cachedQuery(
  "posts:published:page:1",
  () =>
    prisma.post.findMany({
      where: { published: true },
      take: 20,
    }),
  1800, // 30 minutes
);

Cache multi-couches#

TypeScript
// L1: In-Memory (process) → L2: Redis → L3: Database

const memoryCache = new Map<string, { data: any; expires: number }>();

export async function multiLayerCache<T>(
  key: string,
  queryFn: () => Promise<T>,
  ttl: number = 3600,
): Promise<T> {
  // L1: Mémoire locale (ultra-rapide, ~1ms)
  const inMemory = memoryCache.get(key);
  if (inMemory && inMemory.expires > Date.now()) {
    return inMemory.data as T;
  }

  // L2: Redis (rapide, ~5ms)
  const cached = await redis.get(key);
  if (cached) {
    const data = JSON.parse(cached) as T;
    memoryCache.set(key, {
      data,
      expires: Date.now() + 60000, // L1 = 1 min
    });
    return data;
  }

  // L3: Database (lent, ~50-200ms)
  const data = await queryFn();
  await redis.setex(key, ttl, JSON.stringify(data));
  memoryCache.set(key, {
    data,
    expires: Date.now() + 60000,
  });

  return data;
}

Bases de Données Distribuées#

Réplication#

Primary-Replica Architecture :

┌──────────┐     ┌──────────────┐
│ Primary  │────→│  Replica 1   │  (Lecture)
│ (Write)  │     └──────────────┘
│          │     ┌──────────────┐
│          │────→│  Replica 2   │  (Lecture)
│          │     └──────────────┘
│          │     ┌──────────────┐
│          │────→│  Replica 3   │  (Lecture)
└──────────┘     └──────────────┘

Avantages :
- Lectures distribuées (scalabilité)
- Haute disponibilité (failover)
- Backups sans impact

Sharding#

TypeScript
// Sharding par user_id — chaque shard contient un sous-ensemble

function getShardKey(userId: string): number {
  // Hash consistant pour déterminer le shard
  const hash = createHash("md5")
    .update(userId)
    .digest("hex");
  const numericHash = parseInt(hash.substring(0, 8), 16);
  return numericHash % NUM_SHARDS;
}

// Routing vers le bon shard
const shardConnections = [
  new PrismaClient({ datasources: { db: { url: SHARD_0_URL } } }),
  new PrismaClient({ datasources: { db: { url: SHARD_1_URL } } }),
  new PrismaClient({ datasources: { db: { url: SHARD_2_URL } } }),
];

async function getUserData(userId: string) {
  const shardIndex = getShardKey(userId);
  const db = shardConnections[shardIndex];
  return db.user.findUnique({ where: { id: userId } });
}

Message Queues#

Pourquoi les queues ?#

Sans Queue :
Client → API → [Opération lente 10s] → Réponse (10s d'attente)

Avec Queue :
Client → API → Queue → Réponse immédiate (200ms)
                ↓
              Worker → [Opération lente 10s] → Résultat asynchrone

Implémentation avec BullMQ#

TypeScript
// lib/queue.ts
import { Queue, Worker } from "bullmq";
import Redis from "ioredis";

const connection = new Redis(process.env.REDIS_URL!);

// Créer la queue
export const emailQueue = new Queue("emails", { connection });

// Ajouter un job
export async function sendEmail(to: string, template: string) {
  await emailQueue.add(
    "send-email",
    { to, template },
    {
      attempts: 3,
      backoff: { type: "exponential", delay: 1000 },
      removeOnComplete: true,
      removeOnFail: false,
    },
  );
}

// Worker qui traite les jobs
const worker = new Worker(
  "emails",
  async (job) => {
    const { to, template } = job.data;
    await sendEmailViaProvider(to, template);
    console.log(`Email sent to ${to}`);
  },
  {
    connection,
    concurrency: 5,
    limiter: { max: 100, duration: 60000 }, // 100/minute
  },
);

worker.on("failed", (job, err) => {
  console.error(`Job ${job?.id} failed:`, err);
});

Microservices vs Monolithe#

AspectMonolitheMicroservices
DéploiementSimple, un seul artefactComplexe, orchestration nécessaire
ScalabilitéVerticale principalementHorizontale par service
DéveloppementRapide au débutRapide à grande échelle
DebuggingStack trace uniqueTracing distribué nécessaire
ConsistanceTransactions DBSaga pattern, eventual consistency
Équipe~10 développeursMultiple équipes autonomes

Quand basculer ?#

Monolithe d'abord (< 10 devs, < 1M users)
    ↓ Quand la douleur dépasse le bénéfice
Monolithe modulaire (services internes)
    ↓ Quand il faut scaler différemment certaines parties
Microservices (> 50 devs, > 10M users)

Règle de Martin Fowler : Si vous ne pouvez pas construire un monolithe correctement, vous ne pouvez pas construire des microservices correctement.

CDN et Edge Computing#

Architecture CDN#

Utilisateur (Paris) → Edge PoP (Paris) → [Hit] → Réponse 20ms
                           ↓ [Miss]
                      Origin (US-East) → Réponse 200ms
TypeScript
// next.config.mjs — caching pour CDN
async headers() {
  return [
    {
      source: "/api/posts",
      headers: [
        {
          key: "Cache-Control",
          value: "public, s-maxage=3600, stale-while-revalidate=86400",
        },
      ],
    },
    {
      source: "/_next/static/(.*)",
      headers: [
        {
          key: "Cache-Control",
          value: "public, max-age=31536000, immutable",
        },
      ],
    },
  ];
}

Edge Functions#

TypeScript
// middleware.ts — exécuté sur l'Edge (< 5ms)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export const config = {
  matcher: "/api/:path*",
};

export function middleware(req: NextRequest) {
  // Géo-routing
  const country = req.geo?.country ?? "US";

  // A/B testing côté Edge
  const bucket = req.cookies.get("ab-bucket")?.value
    ?? (Math.random() < 0.5 ? "A" : "B");

  const response = NextResponse.next();
  response.cookies.set("ab-bucket", bucket);
  response.headers.set("x-country", country);

  return response;
}

Cas Pratiques#

Design d'un URL Shortener#

Exigences :
- 100M URLs créées/mois
- 10B redirections/mois
- Latence < 10ms pour les redirections

Architecture :
1. API Service → Génération de short URL (Base62 encoding)
2. Redis Cache → Hot URLs (top 20% = 80% du trafic)
3. PostgreSQL → Stockage persistant
4. CDN → Redirections cachées en edge
5. Analytics Queue → Comptage asynchrone des clics

Design d'un Feed Social#

Exigences :
- 100M utilisateurs actifs
- Chaque utilisateur suit ~200 personnes
- Feed personnalisé, ordonné par pertinence

Architecture :
1. Write Path : Post → Fan-out Queue → Pre-compute feeds
2. Read Path : User → Cache (pre-computed feed) → Serve
3. Célébrités : Fan-out on read (hybride)
4. Ranking : Score = recency × engagement × relationship
5. Storage : Redis (hot feeds) + Cassandra (cold storage)

Conclusion#

Le system design est un art d'équilibre :

  • Load Balancing pour distribuer la charge
  • Caching multi-couches pour la performance
  • Réplication et Sharding pour la scalabilité des données
  • Message Queues pour le découplage et la résilience
  • CDN et Edge pour la latence minimale
  • Monolithe d'abord, microservices ensuite

Chaque décision architecturale est un compromis. Comprenez les trade-offs, mesurez avant d'optimiser, et évoluez l'architecture avec les besoins réels — pas les besoins imaginés.

¿Te resultó útil este artículo?

9 min read
0 vistas
0 me gusta
0 compartidos