Performance et tuning#
Méthodologie de diagnostic — méthode USE#
Avant de toucher à un seul paramètre noyau, il faut mesurer. La méthode USE (Utilization, Saturation, Errors), formalisée par Brendan Gregg, fournit un cadre systématique applicable à chaque ressource physique :
Ressource |
Utilisation |
Saturation |
Erreurs |
|---|---|---|---|
CPU |
|
Load avg > nb_cœurs, runqueue |
|
Mémoire |
|
Swap actif (si/so vmstat), OOM killer |
Erreurs ECC mémoire |
Disque |
|
|
|
Réseau |
|
Drops ( |
Erreurs de trame |
L’application systématique de la méthode USE évite le cargo-cult tuning : modifier des paramètres sans mesure préalable conduit presque toujours à des régressions non anticipées.
Charge moyenne — interprétation correcte#
La charge moyenne (load average) mesure le nombre moyen de processus en état R (runnable) ou D (uninterruptible sleep I/O). Elle est souvent mal interprétée :
load average: 3.20, 2.95, 2.81
1 min 5 min 15 min
Sur un système à 4 cœurs : charge 3.2 à 1 min → 80 % de saturation, acceptable
Sur un système à 2 cœurs : charge 3.2 → saturation à 160 %, files d’attente
La tendance temporelle est aussi informative que la valeur absolue : une charge montant de 0.5 à 4.0 sur 15 minutes signale un problème émergent.
iowait dans le load average
Les processus en état D (uninterruptible I/O wait) contribuent au load average mais n’utilisent pas la CPU. Un load average élevé avec une CPU idle à 70 % et iowait à 25 % indique une saturation I/O, pas une saturation CPU — deux problèmes très différents avec des solutions différentes.
Profiling CPU#
perf stat — compteurs matériels#
perf stat interroge les Performance Monitoring Counters (PMC) du processeur, exposés par le noyau via le sous-système perf_events :
perf stat -e cycles,instructions,cache-misses,branch-misses ./mon-programme
Performance counter stats for './mon-programme':
2,345,678,901 cycles # 3.42 GHz
1,890,234,567 instructions # 0.81 insn per cycle
45,234,100 cache-misses # 2.45% of all cache refs
3,456,789 branch-misses # 1.23% of all branches
0.686321123 seconds time elapsed
Un ratio instructions per cycle (IPC) < 1 indique des bulles de pipeline fréquentes, souvent dues à des cache-misses. Un taux de branch-misses > 5 % indique des prédictions de branchement défaillantes.
perf record/report — profiling par échantillonnage#
# Enregistrer un profil CPU pendant 30 secondes
perf record -g -F 1000 -p $(pgrep mon-app) -- sleep 30
# Analyser le profil
perf report --sort=symbol,dso
-g capture les call stacks, -F 1000 échantillonne à 1000 Hz. Le rapport montre les fonctions consommant le plus de cycles CPU, avec leur call graph complet.
Flamegraphs — visualisation des call stacks#
Un flamegraph représente les call stacks de manière hiérarchique : la largeur de chaque bloc est proportionnelle au temps CPU passé dans cette fonction. Les fonctions les plus larges en haut de la flamme sont les hotspots à optimiser.
# Avec les scripts de Brendan Gregg
perf record -F 99 -ag -- sleep 30
perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg
strace et ltrace — traçage des appels système#
# Tracer les appels système d'un processus
strace -c -p 1234
# Résumé statistique
strace -c ./mon-programme 2>&1 | head -20
% time seconds usecs/call calls errors syscall
45.23 0.001234 12 100 read
23.11 0.000631 6 105 write
15.44 0.000421 421 1 execve
8.90 0.000243 4 60 mmap
ltrace fait de même pour les appels aux bibliothèques dynamiques (libssl, libc…).
I/O et stockage#
iotop — processus et I/O#
# Afficher les processus avec le plus d'I/O en temps réel
iotop -o -d 2
# Mode non-interactif pour la journalisation
iotop -b -o -n 5 -d 2
Total DISK READ: 12.34 M/s | Total DISK WRITE: 45.67 M/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
1234 be/4 mysql 10.23 M/s 2.34 M/s 0.00 % 78.23 % mysqld
5678 be/4 www 0.00 B/s 43.33 M/s 0.00 % 12.45 % php-fpm
Schedulers I/O — choisir selon le workload#
Le noyau Linux propose plusieurs ordonnanceurs d’I/O pour la file de requêtes des blocs :
Scheduler |
Description |
Workload adapté |
|---|---|---|
mq-deadline |
Garantit des délais maximaux, faible latence |
SSD, bases de données OLTP |
kyber |
Faible overhead, optimisé multi-queue NVMe |
NVMe haute performance |
bfq (Budget Fair Queueing) |
Équité entre processus, latence interactive |
Desktop, médias, HDD |
none |
Pas d’ordonnancement (FIFO) |
Hyperviseurs (l’hôte ordonnance) |
# Voir le scheduler actuel
cat /sys/block/sda/queue/scheduler
# Changer le scheduler
echo "mq-deadline" > /sys/block/nvme0n1/queue/scheduler
# Persistant via udev
cat /etc/udev/rules.d/60-scheduler.rules
# ACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/scheduler}="kyber"
fio — benchmark I/O#
# Test de lecture séquentielle
fio --name=lecture_seq --filename=/tmp/test.fio \
--rw=read --bs=1M --size=2G --numjobs=1 --runtime=30 \
--ioengine=libaio --direct=1 --iodepth=32
# Test d'écriture aléatoire (4K)
fio --name=ecriture_rand --filename=/tmp/test.fio \
--rw=randwrite --bs=4K --size=1G --numjobs=4 --runtime=30 \
--ioengine=libaio --direct=1 --iodepth=64 --group_reporting
READ: bw=1241MiB/s (1301MB/s), io=2048MiB, run=1650msec
iops : min=1200, max=1280, avg=1241.34
lat (msec) : min=0.82, max=2.34, avg=0.97
Réseau — diagnostic et tuning#
ss — états TCP et buffers#
ss remplace netstat avec de meilleures performances (il lit directement les sockets du noyau via Netlink au lieu de /proc/net/tcp) :
# Toutes les sockets TCP établies avec informations étendues
ss -tipn
# Sockets en écoute avec les buffers
ss -tlnp
# Statistiques par état TCP
ss -s
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
tcp ESTAB 0 0 10.0.0.1:443 203.0.113.5:52341 users:(("nginx",pid=1234))
cubic wscale:7,7 rto:204 rtt:3.421/1.2 ato:40 mss:1448 rcvmss:1448 advmss:1448
cwnd:10 bytes_acked:45234 bytes_received:1234 segs_out:456 segs_in:234
Les champs Recv-Q et Send-Q non nuls sur les sockets en écoute signalent que l’application ne consomme pas les connexions assez vite — un backlog saturé.
tcpdump — capture de paquets#
# Capturer le trafic HTTP sur eth0
tcpdump -i eth0 -w /tmp/capture.pcap port 80 or port 443
# Filtre BPF avancé : uniquement les SYN (nouvelles connexions)
tcpdump -i any 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0'
# Afficher le contenu HTTP
tcpdump -i eth0 -A -s 0 port 80 | grep -E "Host:|GET |POST "
ethtool — configuration de l’interface#
# Voir les offloads actifs
ethtool -k eth0 | grep -E "scatter-gather|tcp-segmentation|generic-segmentation"
# Désactiver le GRO (Generic Receive Offload) — utile pour analyser des paquets
ethtool -K eth0 gro off
# Statistiques du pilote réseau
ethtool -S eth0 | grep -E "rx_missed|tx_dropped|rx_errors"
iperf3 — mesure de bande passante#
# Serveur
iperf3 -s
# Client — test TCP bidirectionnel sur 30 secondes
iperf3 -c 192.168.1.100 -t 30 --bidir
# Test UDP (mesure du jitter)
iperf3 -c 192.168.1.100 -u -b 100M
Mémoire — pages, NUMA et OOM killer#
Transparent HugePages (THP)#
Par défaut, Linux alloue la mémoire par pages de 4 KiB. Les Transparent HugePages (THP) regroupent 512 pages consécutives en une seule page de 2 MiB, réduisant la pression sur le TLB (Translation Lookaside Buffer).
# Voir l'état THP
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never
# Désactiver pour les bases de données (Oracle, MongoDB recommandent never)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
THP et bases de données
Oracle, MongoDB, Redis et plusieurs autres bases de données recommandent de désactiver THP. Le compactage de mémoire déclenché par THP introduit des latences imprévisibles (pauses de 100ms+) incompatibles avec les SLAs OLTP. Pour les applications analytiques en mémoire (Spark, Hadoop), THP peut au contraire améliorer les performances.
NUMA — Non-Uniform Memory Access#
Sur les serveurs multi-sockets, chaque processeur a accès rapide à sa propre banque mémoire (NUMA node local) et accès plus lent à celle des autres processeurs :
# Voir la topologie NUMA
numactl --hardware
# Lier un processus à un nœud NUMA
numactl --cpunodebind=0 --membind=0 ./mon-app
# Statistiques NUMA
numastat -m
Un processus qui alloue de la mémoire sur un nœud NUMA distant peut subir des latences mémoire 2 à 3 fois supérieures.
OOM Killer — score et configuration#
Quand la mémoire physique est épuisée, le noyau active l”OOM Killer qui sélectionne et tue le processus avec le score le plus élevé :
# Voir le score OOM d'un processus
cat /proc/$(pgrep mon-app)/oom_score
# Protéger un processus critique (score ajustement -1000 = jamais tuer)
echo -1000 > /proc/$(pgrep sshd)/oom_score_adj
# Forcer le kill d'un processus par l'OOM (score +1000)
echo 1000 > /proc/$(pgrep gros-process)/oom_score_adj
# OOM killer dans les logs
dmesg | grep -i "oom\|killed process"
journalctl -k | grep "Out of memory"
[1234567.890] Out of memory: Kill process 4567 (java) score 892 or sacrifice child
[1234567.891] Killed process 4567 (java) total-vm:4096000kB, anon-rss:3800000kB
vm.swappiness#
# Valeur actuelle (défaut : 60)
sysctl vm.swappiness
# Réduire le swap pour les serveurs (10 = utiliser swap uniquement en dernier recours)
sysctl -w vm.swappiness=10
# Persistant
echo "vm.swappiness=10" >> /etc/sysctl.d/99-performance.conf
Tuning noyau — paramètres sysctl#
Paramètres réseau critiques#
# /etc/sysctl.d/99-network-tuning.conf
# Buffers TCP : [min défaut max] en octets
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# Backlog des sockets en écoute (SYN queue)
net.ipv4.tcp_max_syn_backlog = 8192
# File d'attente des connexions entrantes (SOMAXCONN)
net.core.somaxconn = 65535
# Réutilisation des sockets TIME_WAIT (serveurs à fort trafic)
net.ipv4.tcp_tw_reuse = 1
# Protection SYN flood
net.ipv4.tcp_syncookies = 1
# Nombre de ports éphémères disponibles
net.ipv4.ip_local_port_range = 1024 65535
Paramètres I/O et VM#
# Taille de la fenêtre de lecture anticipée (Ko) — adapté aux lectures séquentielles
blockdev --setra 4096 /dev/sda
# Via sysfs (persistant avec udev)
echo 4096 > /sys/block/sda/queue/read_ahead_kb
# Ratio de pages sales avant flush (défaut : 20%)
sysctl -w vm.dirty_ratio=15
# Ratio déclenchant le flush en arrière-plan (défaut : 10%)
sysctl -w vm.dirty_background_ratio=5
Appliquer les changements sysctl
sysctl -p /etc/sysctl.d/99-performance.conf applique immédiatement un fichier de configuration sans redémarrage. Pour valider sans appliquer, utiliser sysctl -n <paramètre> pour lire la valeur actuelle.
Courbe de charge simulée sur 24h#
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
np.random.seed(42)
# Simulation de charge sur 24h (mesures toutes les 5 minutes → 288 points)
t = np.linspace(0, 24, 288)
# Profil de base : creux la nuit, pics en journée
charge_base = (
0.3 + 0.5 * np.sin(np.pi * (t - 6) / 12) * (t > 6) * (t < 22) +
0.8 * np.exp(-0.5 * ((t - 10) / 1.5) ** 2) +
1.2 * np.exp(-0.5 * ((t - 15) / 2.0) ** 2) +
0.3 * np.random.normal(0, 0.1, len(t))
)
charge_base = np.clip(charge_base, 0.1, None)
# Ajouter un pic d'incident entre 16h30 et 17h30
incident_start, incident_end = int(16.5 * 12), int(17.5 * 12)
charge_base[incident_start:incident_end] += (
3.5 * np.exp(-0.5 * ((np.linspace(0, 1, incident_end - incident_start) - 0.5) / 0.2) ** 2)
)
nb_coeurs = 4
fig, ax = plt.subplots(figsize=(14, 5))
# Zones colorées
ax.axhspan(0, nb_coeurs * 0.7, alpha=0.07, color="green", label="Normal (< 70% capacité)")
ax.axhspan(nb_coeurs * 0.7, nb_coeurs, alpha=0.07, color="orange", label="Élevé (70-100%)")
ax.axhspan(nb_coeurs, nb_coeurs * 1.5, alpha=0.07, color="red", label="Critique (> 100%)")
ax.axhspan(nb_coeurs * 1.5, 10, alpha=0.07, color="darkred")
ax.plot(t, charge_base, color=sns.color_palette("muted")[0], linewidth=1.8, label="Load average")
ax.axhline(y=nb_coeurs, color="red", linestyle="--", linewidth=1.5,
label=f"Saturation CPU ({nb_coeurs} cœurs)")
ax.axhline(y=nb_coeurs * 0.7, color="orange", linestyle="--", linewidth=1.2,
label="Seuil d'alerte (70%)")
# Annoter le pic d'incident
pic_idx = int(16.9 * 12)
ax.annotate("Incident\n16h54",
xy=(t[pic_idx], charge_base[pic_idx]),
xytext=(17.5, charge_base[pic_idx] + 0.8),
arrowprops=dict(arrowstyle="->", color="red"),
fontsize=10, color="red")
ax.set_xlim(0, 24)
ax.set_ylim(0, max(charge_base) * 1.15)
ax.set_xticks(range(0, 25, 2))
ax.set_xticklabels([f"{h:02d}h" for h in range(0, 25, 2)])
ax.set_xlabel("Heure de la journée")
ax.set_ylabel("Load average (1 min)")
ax.set_title(f"Charge système simulée sur 24h — serveur {nb_coeurs} cœurs")
ax.legend(loc="upper left", fontsize=9)
plt.show()
Flamegraph simplifié#
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
# Simulation d'un flamegraph (barh empilé par frame de call stack)
# Inspiré de la structure réelle perf report
fonctions = [
# (nom, parent_offset, durée_relative, profondeur)
("main()", 0.00, 1.00, 0),
(" http_serve()", 0.00, 0.82, 1),
(" parse_request()", 0.00, 0.35, 2),
(" malloc()", 0.00, 0.08, 3),
(" memcpy()", 0.08, 0.10, 3),
(" regex_match()", 0.18, 0.17, 3),
(" db_query()", 0.35, 0.40, 2),
(" pg_exec()", 0.35, 0.28, 3),
(" read()", 0.35, 0.14, 4),
(" write()", 0.49, 0.07, 4),
(" json_encode()", 0.63, 0.12, 3),
(" log_write()", 0.82, 0.10, 1),
(" write()", 0.82, 0.10, 2),
]
palette = sns.color_palette("muted", 5)
couleurs_profondeur = {0: palette[0], 1: palette[1], 2: palette[2],
3: palette[3], 4: palette[4]}
fig, ax = plt.subplots(figsize=(13, 6))
for i, (nom, debut, duree, profondeur) in enumerate(fonctions):
barre = ax.barh(
profondeur,
duree,
left=debut,
height=0.7,
color=couleurs_profondeur[profondeur],
edgecolor="white",
linewidth=0.8,
align="center",
)
if duree > 0.05:
nom_court = nom.strip()
ax.text(debut + duree / 2, profondeur, nom_court,
ha="center", va="center", fontsize=8.5,
color="white" if profondeur < 3 else "black",
fontweight="bold")
ax.set_yticks(range(5))
ax.set_yticklabels(["Niveau 0\n(main)", "Niveau 1", "Niveau 2",
"Niveau 3", "Niveau 4"])
ax.set_xlim(0, 1)
ax.set_xlabel("Fraction du temps CPU (largeur ∝ temps passé)")
ax.set_title("Flamegraph simplifié — profil CPU d'un serveur HTTP")
# Légende des hotspots
ax.annotate("Hotspot : db_query (40%)",
xy=(0.55, 2), xytext=(0.70, 3.5),
arrowprops=dict(arrowstyle="->"),
fontsize=9, color="red")
plt.show()
Mémoire — parse de /proc/meminfo#
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
def lire_meminfo():
meminfo = {}
try:
with open("/proc/meminfo") as f:
for ligne in f:
m = re.match(r"^(\w+):\s+(\d+)", ligne)
if m:
meminfo[m.group(1)] = int(m.group(2)) # valeur en kB
except FileNotFoundError:
pass
return meminfo
mem = lire_meminfo()
if mem:
total = mem.get("MemTotal", 0)
free = mem.get("MemFree", 0)
available = mem.get("MemAvailable", 0)
buffers = mem.get("Buffers", 0)
cached = mem.get("Cached", 0)
shmem = mem.get("Shmem", 0)
slab_rec = mem.get("SReclaimable", 0)
slab_unr = mem.get("SUnreclaim", 0)
anon = mem.get("AnonPages", 0)
mapped = mem.get("Mapped", 0)
swap_total= mem.get("SwapTotal", 0)
swap_free = mem.get("SwapFree", 0)
swap_used = swap_total - swap_free
dirty = mem.get("Dirty", 0)
def ko_vers_gib(v):
return v / 1024**2
categories = {
"Anon (app)" : anon,
"Cache page" : cached,
"Buffers" : buffers,
"Slab récup." : slab_rec,
"Slab irréc." : slab_unr,
"Partagée" : shmem,
"Libre" : free,
"Autre" : max(0, total - anon - cached - buffers - slab_rec - slab_unr - shmem - free),
}
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
# Graphe en barres empilées horizontal
couleurs = sns.color_palette("muted", len(categories))
valeurs_gib = [ko_vers_gib(v) for v in categories.values()]
labels_cat = list(categories.keys())
gauche = 0
for val, nom, coul in zip(valeurs_gib, labels_cat, couleurs):
if val > 0:
barre = axes[0].barh(0, val, left=gauche, height=0.5, color=coul, label=nom)
if val > 0.2:
axes[0].text(gauche + val / 2, 0, f"{val:.1f}G",
ha="center", va="center", fontsize=8, color="white", fontweight="bold")
gauche += val
axes[0].set_xlim(0, ko_vers_gib(total))
axes[0].set_yticks([])
axes[0].set_xlabel("GiB")
axes[0].set_title(f"Répartition RAM — {ko_vers_gib(total):.1f} GiB total")
axes[0].legend(loc="upper right", bbox_to_anchor=(1, 1.3),
ncol=2, fontsize=8)
# Indicateurs clés
indicateurs = {
"Total" : ko_vers_gib(total),
"Disponible" : ko_vers_gib(available),
"Utilisée (app)" : ko_vers_gib(anon + shmem),
"Cache+Buf" : ko_vers_gib(cached + buffers + slab_rec),
"Swap utilisé" : ko_vers_gib(swap_used),
"Pages sales" : ko_vers_gib(dirty),
}
noms_ind = list(indicateurs.keys())
vals_ind = list(indicateurs.values())
barres_ind = axes[1].bar(range(len(noms_ind)), vals_ind,
color=sns.color_palette("muted", len(noms_ind)))
axes[1].set_xticks(range(len(noms_ind)))
axes[1].set_xticklabels(noms_ind, rotation=25, ha="right", fontsize=9)
axes[1].set_ylabel("GiB")
axes[1].set_title("Indicateurs mémoire clés")
for b, v in zip(barres_ind, vals_ind):
if v > 0.01:
axes[1].text(b.get_x() + b.get_width()/2, v + 0.02,
f"{v:.2f}G", ha="center", va="bottom", fontsize=8)
plt.suptitle("Hiérarchie mémoire — /proc/meminfo", fontsize=13, fontweight="bold")
plt.show()
print(f"MemAvailable : {ko_vers_gib(available):.2f} GiB | Swap utilisé : {ko_vers_gib(swap_used):.2f} GiB")
else:
print("/proc/meminfo non disponible sur ce système.")
MemAvailable : 2.46 GiB | Swap utilisé : 2.45 GiB
Modélisation — Little’s Law et schedulers I/O#
Little’s Law pour le dimensionnement#
La loi de Little est un résultat fondamental de la théorie des files d’attente :
N = λ × W
N : nombre moyen de requêtes dans le système (en cours de traitement + en attente)
λ : débit d’arrivée (requêtes par seconde)
W : temps moyen de séjour dans le système (traitement + attente)
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
# Modélisation Little's Law : N = λ × W
# Scénario : serveur web avec temps de traitement moyen W(λ)
# W augmente avec la charge (contention des ressources)
lambda_max = 1000 # requêtes/s maximales avant saturation
lambda_arr = np.linspace(1, lambda_max, 300)
# Temps de réponse W(λ) selon le modèle M/M/1 : W = 1/(μ-λ) avec μ=taux de service
mu = 1100 # capacité de service (req/s)
W_mm1 = 1 / (mu - lambda_arr) # modèle M/M/1 pur
# Modèle plus réaliste avec saturation douce
W_reel = 0.002 + 0.001 * (lambda_arr / 400) ** 2.5
N_mm1 = lambda_arr * W_mm1
N_reel = lambda_arr * W_reel
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
# Temps de réponse W vs λ
axes[0].plot(lambda_arr, W_mm1 * 1000, label="M/M/1 théorique",
color=sns.color_palette("muted")[0], linewidth=2)
axes[0].plot(lambda_arr, W_reel * 1000, label="Modèle réel (saturation douce)",
color=sns.color_palette("muted")[1], linewidth=2)
axes[0].axvline(x=lambda_max * 0.7, color="orange", linestyle="--",
label="Seuil alerte (70% capacité)")
axes[0].axvline(x=mu * 0.95, color="red", linestyle="--",
label="Saturation (95% capacité)")
axes[0].set_xlim(0, lambda_max)
axes[0].set_ylim(0, 80)
axes[0].set_xlabel("Débit d'arrivée λ (req/s)")
axes[0].set_ylabel("Temps de réponse W (ms)")
axes[0].set_title("Temps de réponse vs charge")
axes[0].legend(fontsize=8)
# N = λ × W (concurrence)
axes[1].plot(lambda_arr, N_mm1, label="M/M/1 : N = λ × W",
color=sns.color_palette("muted")[0], linewidth=2)
axes[1].plot(lambda_arr, N_reel, label="Modèle réel",
color=sns.color_palette("muted")[1], linewidth=2)
axes[1].fill_between(lambda_arr, N_reel, alpha=0.15,
color=sns.color_palette("muted")[1])
axes[1].set_xlim(0, lambda_max)
axes[1].set_ylim(0, min(200, max(N_reel) * 1.2))
axes[1].set_xlabel("Débit d'arrivée λ (req/s)")
axes[1].set_ylabel("Requêtes simultanées N")
axes[1].set_title("Little's Law : N = λ × W")
axes[1].legend(fontsize=8)
plt.suptitle("Modélisation des performances — Loi de Little", fontsize=13, fontweight="bold")
plt.show()
Comparaison des schedulers I/O#
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
schedulers = ["mq-deadline", "kyber", "bfq", "none (passthrough)"]
# Données simulées issues de benchmarks typiques (fio sur NVMe/SSD/HDD)
# Latence en µs pour requêtes 4K aléatoires
latence_nvme = [95, 80, 150, 70]
latence_ssd = [180, 220, 200, 300]
latence_hdd = [8000, 12000, 5500, 15000]
# Débit séquentiel relatif (% du max)
debit_nvme = [98, 100, 90, 100]
debit_ssd = [97, 95, 88, 100]
debit_hdd = [99, 92, 85, 100]
x = np.arange(len(schedulers))
largeur = 0.25
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
# Latence 4K random (NVMe et SSD — HDD sur axe secondaire séparé)
barres1 = axes[0].bar(x - largeur, latence_nvme, largeur,
label="NVMe (µs)", color=sns.color_palette("muted")[0])
barres2 = axes[0].bar(x, latence_ssd, largeur,
label="SSD (µs)", color=sns.color_palette("muted")[1])
axes[0].set_xticks(x)
axes[0].set_xticklabels(schedulers, rotation=15, ha="right", fontsize=9)
axes[0].set_ylabel("Latence 4K random (µs)")
axes[0].set_title("Latence I/O — requêtes 4K aléatoires")
axes[0].legend()
for b, v in zip(barres1, latence_nvme):
axes[0].text(b.get_x() + b.get_width()/2, v + 1, f"{v}", ha="center", va="bottom", fontsize=8)
for b, v in zip(barres2, latence_ssd):
axes[0].text(b.get_x() + b.get_width()/2, v + 2, f"{v}", ha="center", va="bottom", fontsize=8)
# Débit séquentiel relatif
barres3 = axes[1].bar(x - largeur, debit_nvme, largeur,
label="NVMe", color=sns.color_palette("muted")[0])
barres4 = axes[1].bar(x, debit_ssd, largeur,
label="SSD", color=sns.color_palette("muted")[1])
barres5 = axes[1].bar(x + largeur, debit_hdd, largeur,
label="HDD", color=sns.color_palette("muted")[2])
axes[1].set_xticks(x)
axes[1].set_xticklabels(schedulers, rotation=15, ha="right", fontsize=9)
axes[1].set_ylabel("Débit séquentiel (% du maximum)")
axes[1].set_ylim(0, 115)
axes[1].set_title("Débit séquentiel relatif")
axes[1].legend()
plt.suptitle("Comparaison des schedulers I/O Linux", fontsize=13, fontweight="bold")
plt.show()
# Tableau synthétique
df_sched = pd.DataFrame({
"Scheduler" : schedulers,
"Latence NVMe (µs)" : latence_nvme,
"Latence SSD (µs)" : latence_ssd,
"Latence HDD (µs)" : latence_hdd,
"Workload optimal" : [
"OLTP, bases de données",
"NVMe haute performance",
"Desktop, usage mixte",
"VM (délégation à l'hôte)",
],
})
print(df_sched.to_string(index=False))
Scheduler Latence NVMe (µs) Latence SSD (µs) Latence HDD (µs) Workload optimal
mq-deadline 95 180 8000 OLTP, bases de données
kyber 80 220 12000 NVMe haute performance
bfq 150 200 5500 Desktop, usage mixte
none (passthrough) 70 300 15000 VM (délégation à l'hôte)
Profiling applicatif et benchmarking#
/usr/bin/time -v — profil d’exécution complet#
/usr/bin/time -v ./mon-programme argument1
Command being timed: "./mon-programme argument1"
User time (seconds): 4.23
System time (seconds): 0.18
Percent of CPU this job got: 98%
Elapsed (wall clock) time: 4.52
Maximum resident set size (kbytes): 524288
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 12345
Voluntary context switches: 234
Involuntary context switches: 89
File system inputs: 0
File system outputs: 8192
Les major page faults (> 0) indiquent que le programme a dû charger des pages depuis le disque — un signe que son working set dépasse la RAM disponible.
cProfile Python vs profiling système#
# Profiler un script Python avec cProfile
python3 -m cProfile -s cumulative mon_script.py | head -20
# Avec sortie vers fichier pour analyse avec snakeviz
python3 -m cProfile -o profile.pstats mon_script.py
snakeviz profile.pstats
La différence entre cProfile (profiling au niveau Python, bytecode) et perf (profiling noyau, instructions machine) est fondamentale : cProfile ne voit pas le temps passé dans les extensions C (numpy, lxml, cryptographie) tandis que perf peut capturer le profil complet incluant les bibliothèques natives.
sysbench et stress-ng — benchmarking#
# Benchmark CPU (calcul de nombres premiers)
sysbench cpu --cpu-max-prime=20000 --threads=4 run
# Benchmark mémoire (débit de lecture/écriture)
sysbench memory --memory-block-size=1K --memory-total-size=10G run
# Benchmark I/O
sysbench fileio --file-total-size=2G prepare
sysbench fileio --file-total-size=2G --file-test-mode=rndrw --threads=4 run
sysbench fileio --file-total-size=2G cleanup
# stress-ng : générer une charge contrôlée pour tester la stabilité
stress-ng --cpu 4 --vm 2 --vm-bytes 1G --io 2 --timeout 60s --metrics
Protocole de mesure rigoureux#
Éviter les pièges de mesure
Répétitions : effectuer au minimum 5 à 10 mesures et utiliser la médiane (pas la moyenne) pour éliminer les valeurs aberrantes liées à des interruptions système.
Isolation : désactiver le scaling de fréquence CPU (cpupower frequency-set -g performance), s’assurer qu’aucun autre workload intensif ne tourne en parallèle, vider les caches disque (echo 3 > /proc/sys/vm/drop_caches) avant les benchmarks I/O.
Warmup : inclure une phase d’échauffement (1-2 itérations non mesurées) pour que les caches TLB, CPU et disque soient dans un état stable représentatif des conditions de production.
Réplicabilité : noter l’état complet du système (version noyau, paramètres sysctl, schedulers, topologie NUMA, gouverneur CPU) pour que les résultats puissent être reproduits.
Résumé#
Le tuning Linux est une discipline empirique : chaque optimisation doit être précédée d’une mesure, suivie d’une validation, et documentée. La méthode USE fournit le cadre systématique pour éviter de se concentrer sur la mauvaise ressource.
Points à retenir :
La méthode USE (Utilisation/Saturation/Erreurs) s’applique à chaque ressource physique et évite le diagnostic par intuition.
perf est l’outil de référence pour le profiling CPU à bas niveau ; les flamegraphs rendent les résultats lisibles par tous.
Les schedulers I/O doivent être sélectionnés selon le type de stockage et le profil de charge :
mq-deadlinepour les bases de données,bfqpour les usages interactifs.Les paramètres sysctl réseau (
tcp_rmem/wmem,somaxconn) sont les plus impactants sur des serveurs à fort trafic HTTP.THP améliore les performances analytiques mais dégrade les bases de données transactionnelles — configurer explicitement selon le workload.
La loi de Little (N = λ × W) permet de dimensionner correctement les ressources sans sur-provisionner.
Tout benchmark sans protocole rigoureux (répétitions, isolation, warmup) produit des résultats non reproductibles et donc inexploitables.
Axe de tuning |
Outil de mesure |
Levier principal |
|---|---|---|
CPU |
|
Affinité, priorité nice/RT |
Mémoire |
|
swappiness, NUMA binding, THP |
I/O disque |
|
Scheduler I/O, read_ahead, direct I/O |
Réseau |
|
tcp_rmem/wmem, somaxconn, offloads |
Applicatif |
|
Algorithmes, pool de connexions |