MFormations
Modern Backend Engineering

Chapitre 14

Chapitre 14 — Microservices

Chapitre 14 — Microservices

Cours — Microservices

1. Principes des Microservices

Définition

Architecture où l'application est composée de petits services indépendants, chacun responsable d'un domaine métier spécifique.

Caractéristiques

  • Déploiement indépendant : chaque service peut être déployé sans impact sur les autres
  • Scalabilité individuelle : on ne scale que les services qui en ont besoin
  • Stack hétérogène : différents langages/BDD selon les besoins
  • Équipes autonomes : une équipe par service (ownership)
  • Isolation des pannes : un service qui tombe n'emporte pas tout le système

Bounded Context (DDD)

Chaque service correspond à un Bounded Context du Domain-Driven Design.

[User Context] [Order Context] [Payment Context] [Inventory Context]

Avantages vs Inconvénients

AvantagesInconvénients
Scalabilité fineComplexité distribuée
Déploiement indépendantDebug difficile
RésilienceLatence réseau
Teams autonomesTransactions distribuées
Technologie adaptéeConsistency

2. API Gateway

Rôle

Point d'entrée unique pour tous les clients.

Client → [API Gateway] → User Service
                       → Order Service
                       → Payment Service
                       → Inventory Service

Fonctionnalités

  • Routing : redirige vers le bon service
  • Authentication : vérifie les tokens
  • Rate limiting : protège les services
  • Load balancing : distribue la charge
  • Aggregation : combine plusieurs réponses
  • Caching : réduit la charge
  • Transformation : adapte les réponses

Implémentation (Express Gateway / custom)

import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';

const app = express();

// Auth middleware (global)
app.use('/api/*', authenticate);

// Routes
app.use('/api/users', createProxyMiddleware({
  target: 'http://user-service:3001',
  changeOrigin: true,
}));

app.use('/api/orders', createProxyMiddleware({
  target: 'http://order-service:3002',
  changeOrigin: true,
}));

// Rate limiting
app.use('/api', rateLimit({ windowMs: 60000, max: 1000 }));

3. Circuit Breaker

Principe

Évite les appels à un service défaillant pour ne pas aggraver la situation.

États

Closed (normal) → Open (défaillant) → Half-Open (test)

Implémentation (opossum — Node.js)

import CircuitBreaker from 'opossum';

async function callPaymentService(amount) {
  const response = await axios.post('http://payment-service/charge', { amount });
  return response.data;
}

const breaker = new CircuitBreaker(callPaymentService, {
  timeout: 3000,          // 3s timeout
  errorThresholdPercentage: 50,  // 50% d'erreurs → open
  resetTimeout: 30000,    // 30s avant half-open
  volumeThreshold: 10,    // minimum 10 requêtes pour décider
});

breaker.on('open', () => console.log('Circuit opened!'));
breaker.on('halfOpen', () => console.log('Circuit half-open'));
breaker.on('close', () => console.log('Circuit closed'));

// Fallback
breaker.fallback(() => ({ status: 'service_unavailable', queued: true }));

// Usage
app.post('/api/charge', async (req, res) => {
  try {
    const result = await breaker.fire(req.body.amount);
    res.json(result);
  } catch (error) {
    res.status(503).json({ error: 'Payment service unavailable' });
  }
});

4. Bulkhead et Retry

Bulkhead

Isoler les ressources (connexions, threads) pour qu'un service lent n'épuise pas tout le pool.

import { Bulkhead } from 'cockatiel';

const bulkhead = new Bulkhead(10, 100); // max 10 concurrent, queue 100

async function makeRequest() {
  return bulkhead.execute(() =>
    fetch('http://payment-service/charge', { method: 'POST', body: data })
  );
}

Retry avec backoff

import { retry, exponentialBackoff } from 'cockatiel';

const policy = retry(3) // 3 tentatives
  .delay(exponentialBackoff({ initialDelay: 100, maxDelay: 10000 }))
  .breakWhen((err) => err.status === 400); // ne pas retry sur 400

async function chargeWithRetry(amount) {
  return policy.execute(() => paymentService.charge(amount));
}

Combinaison des patterns

import { CircuitBreaker, Bulkhead, retry, exponentialBackoff } from 'cockatiel';

const circuitBreaker = new CircuitBreaker({
  halfOpenAfter: 30000,
  threshold: 0.5,
  duration: 60000,
});

const bulkhead = new Bulkhead(20);

const retryPolicy = retry(3)
  .delay(exponentialBackoff({ initialDelay: 200, maxDelay: 5000 }));

// Pipeline
const pipeline = policy.wrap(circuitBreaker, bulkhead, retryPolicy);

async function chargeUser(amount) {
  return pipeline.execute(() => paymentService.charge(amount));
}

5. Service Discovery

Pourquoi ?

Dans un environnement dynamique (K8s), les services changent d'adresse IP.

Mécanismes

MécanismeDescriptionExemple
DNSRound-robin DNSstandard
Client-sideLe client découvreEureka, Consul
Server-sideLe proxy découvreK8s Service, Nginx
Service MeshSidecar proxyIstio, Linkerd

Consul (Client-side)

import Consul from 'consul';

const consul = new Consul({ host: 'consul.service.consul' });

// Register service
await consul.agent.service.register({
  name: 'user-service',
  address: '10.0.0.1',
  port: 3001,
  check: {
    http: 'http://10.0.0.1:3001/health',
    interval: '10s',
  },
});

// Discover service
async function discover(serviceName) {
  const services = await consul.catalog.service.nodes(serviceName);
  const node = services[Math.floor(Math.random() * services.length)];
  return `http://${node.Address}:${node.ServicePort}`;
}

Kubernetes (Server-side)

apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: user-service
  ports:
    - port: 80
      targetPort: 3001
---
# Accès : http://user-service.namespace.svc.cluster.local

6. Communication Interservice

REST

// Synchronous, simple, évolutif
const user = await axios.get('http://user-service/users/123');

gRPC

Appels typés, performants, streaming bidirectionnel.

// user.proto
service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc ListUsers (ListUsersRequest) returns (stream User);
}

message GetUserRequest {
  string user_id = 1;
}

message User {
  string id = 1;
  string name = 2;
  string email = 3;
}
// Client gRPC
const client = new UserServiceClient('user-service:50051', grpc.credentials.createInsecure());
client.getUser({ userId: '123' }, (error, user) => {
  console.log(user.name);
});

Events (asynchrone)

// Publié par Order Service
await producer.send({
  topic: 'order.created',
  messages: [{ key: '123', value: JSON.stringify(order) }],
});

// Consommé par Payment Service + Inventory Service

Comparaison

CritèreRESTgRPCEvents
Sync/AsyncSyncSync/StreamAsync
PerformancesMoyenÉlevéÉlevé
TypageNon (sauf OpenAPI)Oui (protobuf)Non (sauf schéma)
DécouplageFaibleFaibleFort
DebugFacileMoyenDifficile

7. Distributed Tracing

Pourquoi ?

Dans une architecture microservices, une requête traverse N services. Le tracing distribué permet de suivre le parcours.

OpenTelemetry

import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
import { Resource } from '@opentelemetry/resources';

const provider = new NodeTracerProvider({
  resource: Resource.default().merge(new Resource({
    'service.name': 'order-service',
  })),
});

provider.addSpanProcessor(new SimpleSpanProcessor(new JaegerExporter()));
provider.register();

// Automatic instrumentation
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';

registerInstrumentations({
  instrumentations: [new HttpInstrumentation(), new ExpressInstrumentation()],
});

Headers de propagation

traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01

8. Saga Pattern

Pourquoi ?

Les transactions ACID ne fonctionnent pas en environnement distribué. La saga est une séquence de transactions locales avec compensation.

Choreography Saga

Chaque service publie des événements et réagit aux événements des autres.

Order Service → 'Order Created' → Payment Service → 'Payment Completed'
                                                       ↓
                                              Inventory Service → 'Inventory Reserved'

Orchestration Saga

Un orchestrateur central coordonne les étapes.

class OrderSagaOrchestrator {
  async execute(orderData) {
    try {
      await this.createOrder(orderData);
      await this.processPayment(orderData);
      await this.reserveInventory(orderData);
      await this.confirmOrder(orderData);
    } catch (error) {
      await this.compensate(error.step, orderData);
    }
  }

  async compensate(failedStep, data) {
    const compensations = {
      'createOrder': () => this.cancelOrder(data),
      'processPayment': () => this.refundPayment(data),
      'reserveInventory': () => this.releaseInventory(data),
    };
    // Exécuter les compensations en ordre inverse
    for (const step of reversedSteps) {
      await compensations[step]();
    }
  }
}

9. Déploiement et Versioning

Stratégies de déploiement

  • Blue/Green : deux environnements, bascule DNS
  • Canary : % du trafic vers la nouvelle version
  • Rolling : pods remplacés un par un

Versioning des APIs

GET /api/v1/users
GET /api/v2/users
Content-Type: application/vnd.api+json;version=2

Backward Compatibility

  • Ne jamais casser les consumers existants
  • Ajouter plutôt que modifier
  • Déprécier les endpoints avec header Sunset

10. Anti-patterns et Pièges

Anti-patternProblèmeSolution
Distributed monolithServices fortement couplésBounded contexts stricts
Chatty servicesTrop d'appels interserviceAggregation, events
Shared databaseCouplage fortDatabase per service
No monitoringAveugleDistributed tracing
Sync overloadCascade failuresAsync events
God serviceService trop grosSplit
Wrong boundariesMauvais découpageDDD, event storming