Explorer les tests de charge avec k6 et optimiser un serveur
Une application peut sembler rapide avec quelques utilisateurs et devenir instable dès qu’un trafic important arrive simultanément. Les tests de charge permettent de reproduire cette montée en activité afin d’observer le comportement réel d’une API, d’un site web ou d’un service distribué. Ils révèlent les temps de réponse, les erreurs, les limites de capacité et les ressources qui saturent en premier.
k6 s’est imposé comme un outil particulièrement pratique pour ce travail. Son approche orientée développeur repose sur des scénarios écrits en JavaScript, facilement versionnés avec le code de l’application. Les tests peuvent être exécutés localement, dans une chaîne d’intégration continue ou depuis une infrastructure cloud, avec des seuils qui rendent les régressions immédiatement visibles.
L’objectif n’est toutefois pas de produire le plus grand nombre de requêtes possible. Une campagne utile relie la charge générée à des indicateurs métier et techniques : taux de succès, latence au 95e percentile, débit, consommation mémoire, utilisation CPU et saturation des dépendances. Cette lecture globale guide ensuite les décisions d’optimisation serveur.
Définir une stratégie de charge réaliste
Avant d’écrire un script k6, il faut décrire les parcours réellement suivis par les utilisateurs. Une boutique peut charger le catalogue, consulter une fiche, ajouter un article au panier et finaliser une commande. Une API métier peut recevoir des recherches, des authentifications et des mises à jour avec des fréquences très différentes. Reproduire ces proportions rend les résultats plus représentatifs qu’un simple bombardement d’un endpoint.
Plusieurs profils sont utiles. Le test de montée en charge augmente progressivement le nombre d’utilisateurs virtuels. Le test de pointe simule une arrivée brutale de trafic, tandis que le test d’endurance maintient une activité constante pendant plusieurs heures pour révéler les fuites mémoire ou la dégradation progressive des performances. Le test de capacité cherche enfin le volume maximal acceptable avant le dépassement des seuils définis.
Les objectifs doivent être mesurables. Par exemple, une API peut exiger que 99 % des requêtes réussissent et que 95 % des réponses arrivent en moins de 400 millisecondes. Ces critères sont plus exploitables qu’une impression générale de lenteur. Ils permettent aussi de comparer deux versions d’un service avec des conditions identiques.
Écrire un scénario k6 exploitable
Un script k6 définit généralement une fonction default exécutée par chaque utilisateur virtuel. Les fonctions http.get, http.post et les contrôles check servent à envoyer des requêtes et à vérifier les réponses. Les options stages permettent de modéliser une montée progressive, tandis que thresholds transforment les objectifs de performance en règles d’échec automatisées.
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 50 },
{ duration: '5m', target: 200 },
{ duration: '2m', target: 0 },
],
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<400'],
},
};
export default function () {
const response = http.get('https://api.exemple.test/products');
check(response, {
'réponse correcte': (r) => r.status === 200,
'contenu présent': (r) => r.body.length > 0,
});
sleep(1);
}
Un scénario crédible doit gérer les données de test, les en-têtes, l’authentification et la variété des paramètres. Réutiliser toujours le même identifiant peut produire des résultats artificiels, surtout si le serveur exploite un cache. Des données générées ou distribuées entre les utilisateurs virtuels permettent de mieux représenter la diversité des requêtes et d’éviter les collisions.
Les métriques natives de k6 incluent la durée totale, la latence de connexion, le temps d’attente du serveur et le volume transféré. Des métriques personnalisées peuvent mesurer une étape fonctionnelle, comme le temps nécessaire pour obtenir un panier ou le nombre de recherches réussies. Cette granularité facilite le rapprochement entre les résultats du test et les traces applicatives.
Observer les symptômes côté serveur
Un rapport k6 ne suffit pas à expliquer une dégradation. Pendant l’exécution, il faut corréler les résultats avec les métriques de l’hôte et de la plateforme : CPU, mémoire, entrées-sorties disque, réseau, nombre de processus, connexions ouvertes et files d’attente. Des outils comme Prometheus, Grafana, OpenTelemetry ou les services de supervision cloud peuvent fournir cette visibilité.
Une latence élevée avec un CPU peu utilisé peut signaler une attente réseau, une base de données lente ou une limite de connexions. À l’inverse, un CPU constamment proche de 100 % indique souvent un calcul coûteux, une sérialisation excessive ou un nombre insuffisant de workers. Une mémoire qui augmente sans revenir à un niveau stable après la baisse du trafic mérite une investigation approfondie.
Les logs structurés et les identifiants de corrélation accélèrent le diagnostic. Ils permettent de suivre une requête de l’équilibreur jusqu’au service applicatif, puis vers la base de données ou un fournisseur externe. Les profils d’exécution, les traces distribuées et les journaux de garbage collector complètent cette analyse lorsque les indicateurs généraux ne suffisent pas.
Réduire les goulots d’étranglement applicatifs
L’optimisation commence souvent par les accès aux données. Les requêtes non indexées, les jointures coûteuses et le problème N+1 peuvent faire exploser la latence lorsque le nombre d’utilisateurs augmente. La zone dédiée aux bases de données fournit des repères utiles pour examiner les moteurs, les modèles de données et les stratégies de requêtage.
Un index pertinent réduit le volume de lignes examinées, mais chaque index ajoute un coût lors des écritures et consomme de l’espace. Il faut donc analyser les plans d’exécution plutôt que multiplier les index au hasard. La pagination limitée, la sélection explicite des colonnes et la mise en cache des réponses stables peuvent également réduire la pression sur le serveur.
Le cache doit être conçu avec une politique d’expiration et d’invalidation cohérente. Un cache local convient à certaines données propres à un processus, tandis qu’un cache partagé devient nécessaire lorsque plusieurs instances doivent accéder au même contenu. Le comparatif Redis et Memcached aide à choisir une solution adaptée aux besoins de persistance, de structures de données et de simplicité opérationnelle.
Améliorer la configuration et l’architecture
Un serveur peut perdre ses performances à cause d’une configuration trop restrictive. Les pools de connexions HTTP, les workers, les limites de fichiers ouverts et les délais d’expiration doivent correspondre au volume attendu. Augmenter chaque valeur sans mesure peut toutefois aggraver la situation en provoquant une consommation mémoire excessive ou une saturation de la base de données.
La mise en place d’un reverse proxy, de la compression et d’un réseau de diffusion de contenu réduit le travail demandé à l’application. Les fichiers statiques doivent bénéficier d’en-têtes de cache adaptés, tandis que les réponses dynamiques peuvent être compressées lorsque le coût CPU reste acceptable. La répartition de charge entre plusieurs instances améliore la capacité, à condition que l’application supporte correctement la concurrence.
L’architecture doit aussi tenir compte de la nature du produit. Les contraintes d’une API mobile, d’une application web et d’un service asynchrone ne sont pas identiques. Comprendre les différences entre web et mobile aide à ajuster les stratégies de cache, la taille des réponses et la gestion des connexions selon les clients visés.
Intégrer les tests dans le cycle de livraison
Un test de charge exécuté uniquement avant une mise en production arrive souvent trop tard. Les scénarios critiques peuvent être lancés à chaque version avec une charge réduite, puis complétés par des campagnes plus longues avant une période commerciale ou une évolution d’infrastructure. Dans une pipeline CI/CD, les seuils k6 bloquent automatiquement une livraison lorsque la latence ou le taux d’erreur dépasse la limite autorisée.
Les résultats doivent être conservés afin de suivre les tendances. Une hausse progressive du 95e percentile, même si elle reste sous le seuil, peut révéler une régression qui deviendra problématique quelques semaines plus tard. Il est également utile de comparer les résultats sur une infrastructure comparable et de documenter la version du code, les paramètres du test et les données utilisées.
La validation finale doit associer charge synthétique et observation en production. Un déploiement progressif, un mécanisme de retour arrière et une surveillance renforcée réduisent le risque lors d’une modification sensible. Cette démarche transforme la performance en propriété durable du logiciel, plutôt qu’en correction urgente après incident.
Passez d’une mesure ponctuelle à une pratique régulière : définissez vos seuils, versionnez vos scénarios k6, surveillez les ressources serveur et analysez chaque régression avec des données concrètes. Une boucle courte entre test, diagnostic et optimisation permet d’augmenter la capacité tout en préservant la fiabilité de l’application.