Modern Backend EngineeringUtilisation de
Chapitre 11
Chapitre 11 — Performance Backend
Chapitre 11 — Performance Backend
Cours — Performance Backend
1. Profiling et Instrumentation
Pourquoi profiler ?
Avant d'optimiser, il faut mesurer. Le profiling identifie les goulots d'étranglement.
Node.js — Profiling avec Clinic.js
# Installation
npm install -g clinic
# Profiling
clinic doctor -- node server.js
# Flamegraph
clinic flame -- node server.js
# Heap profiler
clinic heapprofiler -- node server.js
Utilisation de --prof (V8)
node --prof server.js
# Analyse
node --prof-process isolate-*.log > profiled.txt
Python — cProfile et py-spy
import cProfile
import pstats
profiler = cProfile.Profile()
profiler.enable()
# code à profiler
profiler.disable()
stats = pstats.Stats(profiler)
stats.sort_stats('cumtime').print_stats(20)
py-spy (sampling profiler, sans modification du code) :
py-spy record -o profile.svg --pid 12345
py-spy top --pid 12345
Go — pprof
import _ "net/http/pprof"
// Dans le code
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Analyse
go tool pprof http://localhost:6060/debug/pprof/heap
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
2. Caching : Stratégies
Types de cache
| Type | Latence | Capacité | Persistance |
|---|---|---|---|
| L1 (CPU) | ~1ns | KB | Non |
| In-memory (RAM) | ~100ns | GB | Non |
| Redis | ~1ms | GB | Optionnelle |
| CDN | ~10ms | TB | Oui |
| Database | ~10ms | TB | Oui |
Cache-Aside Pattern
async function getUser(id) {
// 1. Vérifier le cache
const cached = await redis.get(`user:${id}`);
if (cached) return JSON.parse(cached);
// 2. Charger depuis la BDD
const user = await db.users.findById(id);
// 3. Stocker dans le cache
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
return user;
}
Write-Through Cache
async function updateUser(id, data) {
// 1. Écrire dans la BDD
const user = await db.users.update(id, data);
// 2. Mettre à jour le cache
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
return user;
}
Invalidation strategies
- TTL : expiration automatique (le plus simple)
- Event-driven : invalidation sur modification
- Write-through : mise à jour synchrone du cache
- Lazy invalidation : invalidation à la lecture si stale
Redis Cache Patterns
// Rate limiting avec sliding window
const WINDOW = 60; // secondes
const MAX = 100; // requêtes
async function checkRateLimit(userId) {
const key = `ratelimit:${userId}:${Math.floor(Date.now() / 1000 / WINDOW)}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, WINDOW);
return count <= MAX;
}
CDN Caching
Cache-Control: public, max-age=31536000, immutable
CDN-Cache-Control: public, max-age=86400
Cloudflare-CDN-Cache-Control: public, max-age=86400
3. Database Optimization
Indexing
-- Index simple
CREATE INDEX idx_users_email ON users(email);
-- Index composé (ordre des colonnes important !)
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at DESC);
-- Index partiel
CREATE INDEX idx_active_users ON users(is_active) WHERE is_active = true;
-- Index hash pour égalité
CREATE INDEX idx_users_email_hash ON users USING HASH(email);
Query Optimization
-- Mauvais : pas d'index, full scan
SELECT * FROM orders WHERE YEAR(created_at) = 2026;
-- Bon : index sur created_at
SELECT * FROM orders WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';
N+1 Query Problem
// MAUVAIS : N+1 queries
const users = await db.users.findAll();
for (const user of users) {
const posts = await db.posts.findAll({ where: { userId: user.id } });
}
// BON : eager loading (SQL JOIN)
const users = await db.users.findAll({
include: [{ model: db.posts }]
});
// OU : batch loading (DataLoader)
const userLoader = new DataLoader(ids =>
db.users.findAll({ where: { id: ids } })
);
Pagination efficace
-- Offset-based (lent pour les grandes pages)
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 10000;
-- Cursor-based (rapide)
SELECT * FROM users WHERE id > 10000 ORDER BY id LIMIT 20;
4. Connection Pooling
Principe
Réutiliser les connexions à la base de données plutôt que d'en créer une nouvelle à chaque requête.
PostgreSQL (pg-pool)
import { Pool } from 'pg';
const pool = new Pool({
max: 20, // max connexions
idleTimeoutMillis: 30000, // temps avant fermeture idle
connectionTimeoutMillis: 2000, // timeout création
maxUses: 7500, // recycle après X requêtes
});
// Toujours libérer la connexion
async function query(text, params) {
const client = await pool.connect();
try {
return await client.query(text, params);
} finally {
client.release();
}
}
Monitoring du pool
setInterval(() => {
console.log({
totalCount: pool.totalCount,
idleCount: pool.idleCount,
waitingCount: pool.waitingCount,
});
}, 5000);
5. Lazy Loading et Pagination
Lazy Loading (Cursors)
async function* getUserEvents(userId, batchSize = 100) {
let cursor = null;
while (true) {
const [events, nextCursor] = await db.events.findByCursor({
userId,
limit: batchSize,
cursor
});
if (events.length === 0) break;
yield events;
cursor = nextCursor;
}
}
// Usage
for await (const batch of getUserEvents(userId)) {
processBatch(batch);
}
Offset vs Cursor
| Critère | Offset | Cursor |
|---|---|---|
| Performance (grandes pages) | Dégradé | Constant |
| Données en temps réel | Résultats dupliqués | Stable |
| Pagination aléatoire | Oui | Non |
| Index requis | Non | Oui |
6. Benchmarking
wrk
wrk -t12 -c400 -d30s http://localhost:3000/api/users
# Résultat :
# Requests/sec: 15243.67
# Transfer/sec: 3.45MB
# Latency (ms): avg=26.15 max=542.37
autocannon (Node.js)
npx autocannon -c 100 -d 30 http://localhost:3000/api/users
Go Benchmark
// bench_test.go
func BenchmarkCalculateHash(b *testing.B) {
for i := 0; i < b.N; i++ {
CalculateHash("test-input")
}
}
go test -bench=. -benchmem
# Result: 12345 ns/op 512 B/op 6 allocs/op
7. Memory Leaks et GC
Causes communes de memory leaks
- Variables globales : accumulées dans le temps
- Closures : capture de références non libérées
- Timers/Callbacks : non nettoyés
- Event listeners : attachés sans détachement
- Caches sans limite : accumulation infinie
- Streams non drainés : backpressure ignoré
Détection (Node.js)
// Heap dump
import heapdump from 'heapdump';
heapdump.writeSnapshot('/tmp/heap-1.heapsnapshot');
// Comparaison
heapdump.writeSnapshot('/tmp/heap-2.heapsnapshot');
// Analyser avec Chrome DevTools → Memory → Load snapshot
WeakRef et FinalizationRegistry
// WeakRef pour cache sans fuite mémoire
const cache = new Map();
function getCached(key) {
const ref = cache.get(key);
if (ref) {
const value = ref.deref();
if (value !== undefined) return value;
}
const value = computeExpensive(key);
cache.set(key, new WeakRef(value));
return value;
}
GC Tuning (Node.js)
# Voir le GC
node --trace-gc server.js
# Augmenter la mémoire
node --max-old-space-size=4096 server.js
# GC explicite (déconseillé en prod)
global.gc();
8. Network Optimization
Compression
import compression from 'express-compression';
app.use(compression({
brotli: { enabled: true, quality: 11 },
gzip: { level: 6 }
}));
HTTP/2 Multiplexing
import http2 from 'node:http2';
const server = http2.createSecureServer({ key, cert }, app);
server.listen(3000);
Keep-Alive
const agent = new http.Agent({
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 256,
maxFreeSockets: 64,
});
9. Async et Concurrence
Worker Threads (Node.js)
import { Worker } from 'node:worker_threads';
function runInWorker(data) {
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js', { workerData: data });
worker.on('message', resolve);
worker.on('error', reject);
});
}
CPU-bound tasks
// Ne PAS bloquer l'event loop
function heavyComputation() {
// MAUVAIS - bloque l'event loop
for (let i = 0; i < 1e9; i++) { /* ... */ }
// BON - découper en chunks
const chunkSize = 1000;
for (let i = 0; i < 1e9; i += chunkSize) {
setImmediate(() => processChunk(i, chunkSize));
}
}
10. Monitoring des Performances
APM Tools
- Datadog APM : tracing distribué
- New Relic : monitoring applicatif
- OpenTelemetry : standard open-source
- Prometheus + Grafana : métriques personnalisées
Métriques clés
import prometheus from 'prom-client';
const httpRequestDuration = new prometheus.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests',
labelNames: ['method', 'route', 'status'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
});
app.use((req, res, next) => {
const end = httpRequestDuration.startTimer();
res.on('finish', () => {
end({ method: req.method, route: req.route?.path, status: res.statusCode });
});
next();
});
Database Query Monitoring
const dbQueryDuration = new prometheus.Histogram({
name: 'db_query_duration_seconds',
help: 'Database query duration',
labelNames: ['query', 'table'],
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1],
});
async function monitoredQuery(text, params) {
const end = dbQueryDuration.startTimer();
try {
return await pool.query(text, params);
} finally {
end({ query: text.split(' ')[0], table: extractTable(text) });
}
}