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
| Avantages | Inconvénients |
|---|---|
| Scalabilité fine | Complexité distribuée |
| Déploiement indépendant | Debug difficile |
| Résilience | Latence réseau |
| Teams autonomes | Transactions distribuées |
| Technologie adaptée | Consistency |
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écanisme | Description | Exemple |
|---|---|---|
| DNS | Round-robin DNS | standard |
| Client-side | Le client découvre | Eureka, Consul |
| Server-side | Le proxy découvre | K8s Service, Nginx |
| Service Mesh | Sidecar proxy | Istio, 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ère | REST | gRPC | Events |
|---|---|---|---|
| Sync/Async | Sync | Sync/Stream | Async |
| Performances | Moyen | Élevé | Élevé |
| Typage | Non (sauf OpenAPI) | Oui (protobuf) | Non (sauf schéma) |
| Découplage | Faible | Faible | Fort |
| Debug | Facile | Moyen | Difficile |
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-pattern | Problème | Solution |
|---|---|---|
| Distributed monolith | Services fortement couplés | Bounded contexts stricts |
| Chatty services | Trop d'appels interservice | Aggregation, events |
| Shared database | Couplage fort | Database per service |
| No monitoring | Aveugle | Distributed tracing |
| Sync overload | Cascade failures | Async events |
| God service | Service trop gros | Split |
| Wrong boundaries | Mauvais découpage | DDD, event storming |