Sizing et Performance : Dimensionner son Infrastructure
Le sizing (dimensionnement) est l'art de déterminer les ressources nécessaires pour qu'un système fonctionne de manière optimale. Un mauvais sizing coûte cher : trop peu = problèmes de performance, trop = gaspillage de ressources.
Méthodologie de sizing
Les questions à se poser
┌─────────────────────────────────────────────────────────────────┐
│ QUESTIONS CLÉS DU SIZING │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. CHARGE DE TRAVAIL │
│ └─ Combien d'utilisateurs/requêtes ? │
│ └─ Quelle croissance prévue ? │
│ └─ Quels pics de charge ? │
│ │
│ 2. EXIGENCES DE PERFORMANCE │
│ └─ Temps de réponse cible (P50, P95, P99) ? │
│ └─ Throughput minimum ? │
│ └─ SLA à respecter ? │
│ │
│ 3. DONNÉES │
│ └─ Volume actuel et croissance ? │
│ └─ Rétention nécessaire ? │
│ └─ Patterns d'accès (read/write ratio) ? │
│ │
│ 4. CONTRAINTES │
│ └─ Budget disponible ? │
│ └─ Contraintes réglementaires (localisation) ? │
│ └─ Compétences de l'équipe ? │
│ │
└─────────────────────────────────────────────────────────────────┘
La règle des 70%
:::tip Règle d'or : Ne jamais dépasser 70% d'utilisation en régime nominal pour garder une marge pour les pics et la croissance. :::
Capacité nécessaire = (Charge moyenne × 1.4) + Marge de croissance
Exemple:
- Charge moyenne : 1000 req/s
- Pic prévu : 1.5x = 1500 req/s
- Croissance annuelle : 30%
Capacité = 1000 × 1.4 × 1.3 = 1820 req/s minimum
→ Dimensionner pour ~2000 req/s
Sizing CPU
Comprendre l'utilisation CPU
# Voir le nombre de cœurs
nproc
cat /proc/cpuinfo | grep processor | wc -l
# Charge moyenne (load average)
uptime
# load average: 1.50, 1.20, 1.00
# = moyenne sur 1min, 5min, 15min
# Règle : load < nombre de cœurs = OK
# Analyse détaillée
mpstat -P ALL 1 5 # Stats par cœur
vmstat 1 10 # Vue d'ensemble
# Identifier les processus CPU-intensive
pidstat -u 1 5 # CPU par processus
ps aux --sort=-%cpu | head -10
Formules de calcul CPU
def calculate_cpu_requirements(
requests_per_second: float,
avg_processing_time_ms: float,
target_utilization: float = 0.7,
safety_margin: float = 1.3
) -> dict:
"""
Calcule les besoins CPU pour une application web.
Args:
requests_per_second: Nombre de requêtes par seconde
avg_processing_time_ms: Temps de traitement moyen en ms
target_utilization: Utilisation cible (défaut 70%)
safety_margin: Marge de sécurité (défaut 30%)
"""
# Temps CPU nécessaire par seconde
cpu_time_per_second = requests_per_second * (avg_processing_time_ms / 1000)
# Nombre de cœurs théoriques
theoretical_cores = cpu_time_per_second
# Avec marge de sécurité et utilisation cible
required_cores = (theoretical_cores * safety_margin) / target_utilization
return {
"requests_per_second": requests_per_second,
"avg_processing_time_ms": avg_processing_time_ms,
"cpu_seconds_per_second": round(cpu_time_per_second, 2),
"theoretical_cores": round(theoretical_cores, 2),
"required_cores": round(required_cores, 1),
"recommendation": f"{int(required_cores + 0.9)} vCPUs"
}
# Exemples
print(calculate_cpu_requirements(
requests_per_second=1000,
avg_processing_time_ms=50
))
# {'requests_per_second': 1000, 'avg_processing_time_ms': 50,
# 'cpu_seconds_per_second': 50.0, 'theoretical_cores': 50.0,
# 'required_cores': 92.9, 'recommendation': '93 vCPUs'}
# Application légère
print(calculate_cpu_requirements(
requests_per_second=500,
avg_processing_time_ms=10
))
# {'required_cores': 9.3, 'recommendation': '10 vCPUs'}
Types de charge CPU
| Type | Caractéristiques | Sizing |
|---|---|---|
| CPU-bound | Calculs intensifs, compression, crypto | Plus de cœurs, fréquence haute |
| I/O-bound | Attente disque/réseau, BDD | Moins de cœurs, plus d'I/O |
| Memory-bound | Gros datasets, caches | CPU avec gros cache L3 |
| Mixed | Applications web typiques | Balance CPU/RAM |
Sizing mémoire (RAM)
Analyse des besoins mémoire
# Vue d'ensemble
free -h
# total used free shared buff/cache available
# Mem: 32Gi 12Gi 8.0Gi 256Mi 11Gi 19Gi
# Détails
cat /proc/meminfo
# Par processus
ps aux --sort=-%mem | head -10
smem -tk # Mémoire réelle par processus (USS, PSS, RSS)
# Mémoire des conteneurs
docker stats --no-stream
# Pages et swap
vmstat 1 5
# si "si" et "so" > 0, le système swappe = manque de RAM
Calcul des besoins mémoire
def calculate_memory_requirements(
base_memory_mb: float,
memory_per_connection_mb: float,
concurrent_connections: int,
cache_size_mb: float = 0,
jvm_heap_ratio: float = None,
safety_margin: float = 1.3
) -> dict:
"""
Calcule les besoins mémoire pour une application.
Args:
base_memory_mb: Mémoire de base de l'application
memory_per_connection_mb: Mémoire par connexion active
concurrent_connections: Nombre de connexions simultanées
cache_size_mb: Taille du cache applicatif
jvm_heap_ratio: Ratio heap/total pour JVM (0.5-0.8)
safety_margin: Marge de sécurité
"""
# Mémoire applicative
app_memory = base_memory_mb + (memory_per_connection_mb * concurrent_connections)
# Avec cache
total_app = app_memory + cache_size_mb
# Mémoire totale avec marge
total_required = total_app * safety_margin
result = {
"base_memory_mb": base_memory_mb,
"connection_memory_mb": memory_per_connection_mb * concurrent_connections,
"cache_memory_mb": cache_size_mb,
"subtotal_mb": round(total_app, 0),
"required_mb": round(total_required, 0),
"required_gb": round(total_required / 1024, 1)
}
# Ajustement JVM si applicable
if jvm_heap_ratio:
jvm_total = total_required / jvm_heap_ratio
result["jvm_total_mb"] = round(jvm_total, 0)
result["jvm_total_gb"] = round(jvm_total / 1024, 1)
result["recommended"] = f"{int(jvm_total / 1024 + 0.9)} GB"
else:
result["recommended"] = f"{int(total_required / 1024 + 0.9)} GB"
return result
# Exemple : API REST Java
print(calculate_memory_requirements(
base_memory_mb=512, # JVM overhead
memory_per_connection_mb=2, # Par thread HTTP
concurrent_connections=200,
cache_size_mb=1024, # 1GB cache
jvm_heap_ratio=0.75 # Heap = 75% de la RAM allouée
))
# {'required_gb': 2.4, 'jvm_total_gb': 3.2, 'recommended': '4 GB'}
# Exemple : Application Node.js
print(calculate_memory_requirements(
base_memory_mb=256,
memory_per_connection_mb=0.5,
concurrent_connections=1000,
cache_size_mb=512
))
# {'required_gb': 1.4, 'recommended': '2 GB'}
Sizing mémoire par technologie
applications:
java_spring:
heap_min: "512m"
heap_max: "selon charge"
metaspace: "256m"
ratio_heap_total: 0.75
formula: "RAM = (Heap / 0.75) + OS overhead (500MB)"
nodejs:
heap_default: "512MB"
heap_max: "4GB (V8 limit sur 64-bit)"
per_connection: "0.5-2MB"
formula: "RAM = Base(256MB) + Connections × 1MB + Cache"
python:
base: "100-500MB"
per_worker: "150-300MB (gunicorn/uwsgi)"
formula: "RAM = Workers × 300MB + Cache + 500MB OS"
go:
base: "50-100MB"
per_goroutine: "2KB stack (grow to 1GB)"
formula: "RAM = Base + Goroutines × 8KB (average) + Heap"
databases:
postgresql:
shared_buffers: "25% of RAM"
effective_cache_size: "75% of RAM"
work_mem: "4MB-64MB (× max_connections)"
maintenance_work_mem: "256MB-1GB"
formula: "RAM = max(4GB, Data × 0.2)"
mysql:
innodb_buffer_pool: "70-80% of RAM"
formula: "RAM = Working Set × 1.2"
redis:
maxmemory: "actual data × 1.5-2"
formula: "RAM = Data + Replication buffer + OS(1GB)"
elasticsearch:
heap: "50% of RAM, max 31GB"
filesystem_cache: "remaining 50%"
formula: "RAM = min(Data, 64GB) ideally"
kafka:
heap: "4-8GB typically"
page_cache: "critical for performance"
formula: "RAM = Heap(6GB) + PageCache(segments × 2)"
Sizing stockage
Types de stockage et performance
┌─────────────────────────────────────────────────────────────────┐
│ COMPARATIF TYPES DE STOCKAGE │
├────────────┬──────────┬──────────┬──────────┬──────────────────┤
│ Type │ IOPS │ Latence │ Débit │ Cas d'usage │
├────────────┼──────────┼──────────┼──────────┼──────────────────┤
│ HDD SATA │ 80-160 │ 5-10ms │ 100MB/s │ Archivage, logs │
│ HDD SAS │ 150-200 │ 4-8ms │ 150MB/s │ BDD legacy │
│ SSD SATA │ 20-90K │ 0.1ms │ 500MB/s │ BDD, apps │
│ SSD NVMe │ 100-500K │ 0.02ms │ 3GB/s │ BDD critique │
│ Cloud gp3 │ 3-16K │ ~1ms │ 125-1000 │ Usage général │
│ Cloud io2 │ 64K │ <1ms │ 4GB/s │ BDD IOPS-heavy │
└────────────┴──────────┴──────────┴──────────┴──────────────────┘
Calcul des besoins IOPS
def calculate_storage_requirements(
read_ops_per_second: int,
write_ops_per_second: int,
avg_io_size_kb: int = 8,
read_write_ratio: float = 0.7,
raid_overhead: float = 1.0,
safety_margin: float = 1.3
) -> dict:
"""
Calcule les besoins de stockage en IOPS et débit.
Args:
read_ops_per_second: Opérations de lecture par seconde
write_ops_per_second: Opérations d'écriture par seconde
avg_io_size_kb: Taille moyenne d'une I/O en KB
read_write_ratio: Ratio lecture/total (0.7 = 70% reads)
raid_overhead: Multiplicateur RAID (1.0 = pas de RAID)
safety_margin: Marge de sécurité
"""
# IOPS totaux
total_iops = (read_ops_per_second + write_ops_per_second * raid_overhead)
# Avec marge
required_iops = total_iops * safety_margin
# Débit
throughput_mbps = (total_iops * avg_io_size_kb) / 1024
return {
"read_iops": read_ops_per_second,
"write_iops": write_ops_per_second,
"total_iops": round(total_iops, 0),
"required_iops": round(required_iops, 0),
"throughput_mbps": round(throughput_mbps, 1),
"required_throughput_mbps": round(throughput_mbps * safety_margin, 1),
"recommended_storage": recommend_storage_type(required_iops)
}
def recommend_storage_type(iops: float) -> str:
if iops < 500:
return "HDD SATA (ou gp2/gp3 minimal)"
elif iops < 5000:
return "SSD SATA ou Cloud gp3"
elif iops < 20000:
return "SSD NVMe ou Cloud gp3 provisionné"
elif iops < 64000:
return "NVMe haute perf ou Cloud io2"
else:
return "Multiple NVMe en RAID ou distributed storage"
# Exemple : Base de données transactionnelle
print(calculate_storage_requirements(
read_ops_per_second=5000,
write_ops_per_second=2000,
avg_io_size_kb=8,
raid_overhead=2.0 # RAID 10
))
# {'required_iops': 11700, 'recommended_storage': 'SSD NVMe ou Cloud gp3 provisionné'}
Sizing capacité disque
def calculate_storage_capacity(
initial_data_gb: float,
daily_growth_gb: float,
retention_days: int,
replication_factor: int = 1,
compression_ratio: float = 1.0,
filesystem_overhead: float = 0.1,
growth_buffer_percent: float = 0.3
) -> dict:
"""
Calcule la capacité de stockage nécessaire.
Args:
initial_data_gb: Données initiales en GB
daily_growth_gb: Croissance journalière en GB
retention_days: Durée de rétention en jours
replication_factor: Facteur de réplication
compression_ratio: Ratio de compression (0.5 = 50% de l'original)
filesystem_overhead: Overhead filesystem (10% par défaut)
growth_buffer_percent: Marge pour croissance imprévue
"""
# Données après rétention
retention_data = daily_growth_gb * retention_days
# Total logique
logical_data = initial_data_gb + retention_data
# Avec compression
compressed_data = logical_data * compression_ratio
# Avec réplication
replicated_data = compressed_data * replication_factor
# Avec overhead filesystem
with_overhead = replicated_data * (1 + filesystem_overhead)
# Avec buffer de croissance
total_required = with_overhead * (1 + growth_buffer_percent)
return {
"initial_data_gb": initial_data_gb,
"retention_data_gb": round(retention_data, 1),
"logical_total_gb": round(logical_data, 1),
"after_compression_gb": round(compressed_data, 1),
"after_replication_gb": round(replicated_data, 1),
"required_capacity_gb": round(total_required, 0),
"required_capacity_tb": round(total_required / 1024, 2)
}
# Exemple : Logs applicatifs
print(calculate_storage_capacity(
initial_data_gb=0,
daily_growth_gb=50, # 50 GB de logs/jour
retention_days=30, # 30 jours de rétention
replication_factor=3, # Triple réplication (ES, Kafka...)
compression_ratio=0.3 # 70% de compression
))
# {'required_capacity_tb': 1.91}
# Exemple : Base de données
print(calculate_storage_capacity(
initial_data_gb=500,
daily_growth_gb=5,
retention_days=365, # 1 an
replication_factor=2, # Primary + replica
compression_ratio=0.6 # 40% de compression
))
# {'required_capacity_tb': 3.63}
Sizing réseau
Calcul de bande passante
def calculate_bandwidth(
requests_per_second: int,
avg_request_size_kb: float,
avg_response_size_kb: float,
peak_multiplier: float = 2.0,
overhead_percent: float = 0.2
) -> dict:
"""
Calcule les besoins en bande passante.
Args:
requests_per_second: Nombre de requêtes par seconde
avg_request_size_kb: Taille moyenne des requêtes (KB)
avg_response_size_kb: Taille moyenne des réponses (KB)
peak_multiplier: Multiplicateur pour les pics
overhead_percent: Overhead réseau (headers, retransmissions)
"""
# Bande passante en KB/s
inbound_kbps = requests_per_second * avg_request_size_kb
outbound_kbps = requests_per_second * avg_response_size_kb
# Conversion en Mbps
inbound_mbps = (inbound_kbps * 8) / 1000
outbound_mbps = (outbound_kbps * 8) / 1000
# Avec overhead
inbound_with_overhead = inbound_mbps * (1 + overhead_percent)
outbound_with_overhead = outbound_mbps * (1 + overhead_percent)
# Pour les pics
peak_inbound = inbound_with_overhead * peak_multiplier
peak_outbound = outbound_with_overhead * peak_multiplier
return {
"steady_state": {
"inbound_mbps": round(inbound_with_overhead, 1),
"outbound_mbps": round(outbound_with_overhead, 1),
"total_mbps": round(inbound_with_overhead + outbound_with_overhead, 1)
},
"peak": {
"inbound_mbps": round(peak_inbound, 1),
"outbound_mbps": round(peak_outbound, 1),
"total_mbps": round(peak_inbound + peak_outbound, 1)
},
"recommended_link": recommend_network_link(peak_inbound + peak_outbound)
}
def recommend_network_link(mbps: float) -> str:
if mbps < 100:
return "100 Mbps"
elif mbps < 1000:
return "1 Gbps"
elif mbps < 10000:
return "10 Gbps"
elif mbps < 25000:
return "25 Gbps"
else:
return "40/100 Gbps ou bonding"
# Exemple : API REST
print(calculate_bandwidth(
requests_per_second=10000,
avg_request_size_kb=2, # Petites requêtes
avg_response_size_kb=10 # Réponses JSON moyennes
))
# {'peak': {'total_mbps': 2880.0}, 'recommended_link': '10 Gbps'}
# Exemple : Streaming vidéo
print(calculate_bandwidth(
requests_per_second=1000,
avg_request_size_kb=1,
avg_response_size_kb=500 # Chunks vidéo
))
# {'peak': {'total_mbps': 12009.6}, 'recommended_link': '25 Gbps'}
Benchmarking et validation
Outils de benchmark
# CPU
sysbench cpu --threads=4 run
stress-ng --cpu 4 --timeout 60s --metrics
# Mémoire
sysbench memory --threads=4 run
stress-ng --vm 2 --vm-bytes 2G --timeout 60s
# Disque
fio --name=random-rw --ioengine=libaio --iodepth=32 \
--rw=randrw --bs=4k --size=1G --numjobs=4 \
--runtime=60 --time_based --group_reporting
# Séquentiel
fio --name=seq-read --ioengine=libaio --iodepth=32 \
--rw=read --bs=1M --size=1G --numjobs=1 \
--runtime=60 --time_based
# Réseau
iperf3 -c server_ip -t 30 -P 4 # Client
iperf3 -s # Server
# HTTP
wrk -t12 -c400 -d30s http://localhost:8080/api/health
ab -n 10000 -c 100 http://localhost:8080/api/health
hey -n 10000 -c 100 http://localhost:8080/api/health
k6 run loadtest.js
Script de benchmark complet
#!/bin/bash
# Benchmark système complet
echo "=== SYSTEM BENCHMARK ==="
echo "Date: $(date)"
echo "Host: $(hostname)"
echo ""
# Info système
echo "=== SYSTEM INFO ==="
echo "CPU: $(nproc) cores"
echo "RAM: $(free -h | awk '/^Mem:/{print $2}')"
echo "Kernel: $(uname -r)"
echo ""
# Benchmark CPU
echo "=== CPU BENCHMARK ==="
sysbench cpu --threads=$(nproc) run 2>/dev/null | grep -E "(events per second|total time)"
echo ""
# Benchmark mémoire
echo "=== MEMORY BENCHMARK ==="
sysbench memory --threads=$(nproc) run 2>/dev/null | grep -E "(transferred|total time)"
echo ""
# Benchmark disque (si fio installé)
if command -v fio &> /dev/null; then
echo "=== DISK BENCHMARK ==="
echo "Random 4K Read/Write:"
fio --name=test --ioengine=libaio --iodepth=32 \
--rw=randrw --bs=4k --size=256M --numjobs=4 \
--runtime=30 --time_based --group_reporting 2>/dev/null | \
grep -E "(read:|write:|iops)"
echo ""
echo "Sequential 1M Read:"
fio --name=seq --ioengine=libaio --iodepth=32 \
--rw=read --bs=1M --size=1G --numjobs=1 \
--runtime=30 --time_based 2>/dev/null | \
grep -E "(read:|bw=)"
fi
echo ""
echo "=== BENCHMARK COMPLETE ==="
Capacity Planning
Processus de capacity planning
┌─────────────────────────────────────────────────────────────────┐
│ CYCLE DE CAPACITY PLANNING │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. MESURER 2. ANALYSER │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Collecter │───────────────▶│ Identifier │ │
│ │ métriques │ │ tendances │ │
│ │ actuelles │ │ et patterns │ │
│ └─────────────┘ └──────┬──────┘ │
│ ▲ │ │
│ │ ▼ │
│ ┌─────┴─────┐ ┌─────────────┐ │
│ │ Implémenter│ │ Prévoir │ │
│ │ changements│◀────────────────│ besoins │ │
│ └───────────┘ │ futurs │ │
│ 4. AGIR └─────────────┘ │
│ 3. PROJETER │
│ │
└─────────────────────────────────────────────────────────────────┘
Modèle de projection
import math
from datetime import datetime, timedelta
def project_resource_needs(
current_value: float,
monthly_growth_rate: float,
projection_months: int,
threshold_percent: float = 0.8
) -> list:
"""
Projette les besoins futurs et identifie quand le seuil sera atteint.
Args:
current_value: Valeur actuelle (ex: utilisation CPU %)
monthly_growth_rate: Taux de croissance mensuel (0.1 = 10%)
projection_months: Nombre de mois à projeter
threshold_percent: Seuil d'alerte (0.8 = 80%)
"""
projections = []
today = datetime.now()
for month in range(projection_months + 1):
date = today + timedelta(days=30 * month)
# Croissance composée
projected = current_value * ((1 + monthly_growth_rate) ** month)
status = "OK"
if projected >= threshold_percent * 100:
status = "WARNING"
if projected >= 100:
status = "CRITICAL"
projections.append({
"month": month,
"date": date.strftime("%Y-%m"),
"projected_percent": round(min(projected, 100), 1),
"status": status
})
# Trouver le mois où le seuil est atteint
threshold_month = None
for p in projections:
if p["status"] != "OK" and threshold_month is None:
threshold_month = p["month"]
break
return {
"projections": projections,
"threshold_reached_month": threshold_month,
"action_required": threshold_month is not None and threshold_month <= 6
}
# Exemple
result = project_resource_needs(
current_value=45, # 45% d'utilisation actuelle
monthly_growth_rate=0.08, # 8% de croissance/mois
projection_months=12,
threshold_percent=0.8
)
print("Projection CPU:")
for p in result["projections"]:
print(f" {p['date']}: {p['projected_percent']}% [{p['status']}]")
if result["action_required"]:
print(f"\n⚠️ Action requise : seuil atteint dans {result['threshold_reached_month']} mois")
Checklist sizing
## Avant le sizing
- [ ] Définir les SLA/SLO cibles
- [ ] Identifier les pics de charge
- [ ] Estimer la croissance sur 12-24 mois
- [ ] Benchmarker l'application existante
- [ ] Documenter les contraintes budget
## CPU
- [ ] Profiler l'application (CPU-bound vs I/O-bound)
- [ ] Calculer les besoins en régime nominal
- [ ] Prévoir 30% de marge pour les pics
- [ ] Considérer le burst capacity (cloud)
## Mémoire
- [ ] Identifier les consumers (app, cache, OS)
- [ ] Calculer la mémoire par connexion
- [ ] Ajouter la marge pour GC (JVM)
- [ ] Éviter le swap à tout prix
## Stockage
- [ ] Calculer les IOPS nécessaires
- [ ] Projeter la croissance des données
- [ ] Choisir le type adapté (SSD/NVMe)
- [ ] Prévoir la rétention et backups
## Réseau
- [ ] Calculer la bande passante
- [ ] Prévoir les pics (×2 à ×3)
- [ ] Considérer la latence inter-zone
- [ ] Dimensionner les load balancers
## Validation
- [ ] Tests de charge avant mise en prod
- [ ] Monitoring post-déploiement
- [ ] Review trimestrielle du capacity plan
Conclusion
Un bon sizing repose sur :
- Des données réelles : Benchmarks, métriques, historique
- Des marges de sécurité : 30% minimum pour les imprévus
- Une vision long terme : Projeter sur 12-24 mois
- Une validation continue : Monitoring et ajustements
:::warning Erreur courante : Dimensionner pour la charge moyenne au lieu de la charge de pointe. Toujours prévoir les pics ! :::