Maximize Page
Tech & DevOps HubEspace Tech & DevOps: Explorez le monde du Dev, du Cloud et des outils DevOps à travers nos articles et discussions Explore the world of development, the cloud and DevOps tools

Mise en place du test de charge (Grafana K6)

Date de l'article:20-11-2025
Grafana CI/CD Github-Actions
Écriture et mise en place d'un script k6 simulant un flux utilisateur complet sur l'API, intégration dans le pipeline staging de GitHub Actions. Nous verrons la configuration du job, l'interprétation des métriques ainsi que les contraintes du plan gratuit de Grafana Cloud
Nous abordons ici le rôle des tests de charge, la comparaison entre Postman/Newman et k6 dans une approche fonctionnelle versus performance, la création d'un script k6 permettant de simuler un flux utilisateur complet, l'intégration de l'étape Grafana k6 dans le workflow [staging] avec visualisation des résultats, ainsi que l'interprétation des métriques en fonction de la charge générée et de la latence
Rôle du test de charge: Valider la performance de l'API
Le test de charge est crucial dans le développement logiciel pour simuler une utilisation réelle (normale ou pic) d'une application.
Ce qui permet de mesurer sa réactivité, sa stabilité et sa fiabilité sous pression.

→ Il sert à identifier les goulets d'étranglement, garantir une bonne expérience utilisateur et assurer la scalabilité avant la mise en production


Script k6: simulation d'un flux utilisateur complet

Le script se trouve dans load-test/flashcards.js, vous pouvez le visualiser en entier dans l'application Flashcards

Le script simule un scénario réaliste :
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
    stages: [
        { duration: '30s', target: 10 },
        { duration: '1m', target: 30 },
        { duration: '30s', target: 0 },
    ],
    thresholds: {
        http_req_failed: ['rate<0.01'],
        http_req_duration: ['p(95)<500'],
    },
};

const BASE_URL = __ENV.BASE_URL || 'http://localhost:8081';


Stages:
  • Fait une montée progressive à 10 VUs, puis 30 VUs, puis retour à 0
         Cela simule un petit trafic qui monte, se stabilise, puis redescend

Thresholds:

http_req_failed: ['rate < 0.01']: défini à moins de 1% d'erreurs HTTP, sinon le test est considéré comme en échec

http_req_duration: ['p(95) < 500']: 95% des requêtes doivent répondre en moins de 500 ms, sinon c'est un échec de performance

Création d'une catégorie:
export default function () {
const params = {
    headers: {
        'Content-Type': 'application/json',
    },
};

const categoryPayload = JSON.stringify({
    name: `cat-${Math.random()}`,
});

const resCat = http.post(
    `${BASE_URL}/api/categories`,
    categoryPayload,
    params
);

check(resCat, {
    'create category OK': (r) => r.status === 200 || r.status === 201,
});

if (!(resCat.status === 200 || resCat.status === 201)) {
    return;
}

let catId;
try {
    catId = resCat.json('id');
} catch (e) {
    console.log(`Category JSON parse error: ${e}`);
    return;
  }
Création d'une flashcard et lectures globales:
const flashcardPayload = JSON.stringify({
  categoryId: catId,
  front: 'question test k6',
  back: 'réponse test k6',
});

sleep(0.05);

const resFlash = http.post(
  `${BASE_URL}/api/flashcards`,
  flashcardPayload,
  params
);

check(resFlash, {
 'create flashcard OK': (r) => r.status === 200 || r.status === 201,
});

// READ OPERATIONS
const resGetCats = http.get(`${BASE_URL}/api/categories`);
check(resGetCats, {
  'get categories OK': (r) => r.status === 200,
});

const resGetFlashcards = http.get(`${BASE_URL}/api/flashcards`);
check(resGetFlashcards, {
 'get flashcards OK': (r) => r.status === 200,
});

sleep(1);
}

Flux fonctionnel
Création de catégorie → création de flashcard liée → lecture des listes
Chaque constante est vérifier par un check, on vérifie ainsi la base de données, la logique métier et le mapping JSON sous charge

Intégration k6 dans GitHub Actions (pipeline staging)
Place dans le pipeline
La branche staging joue le rôle d'environnement de validation réaliste avant le passage vers la production
Ainsi,
Les tests Postman/Newman valident le fonctionnel et le contrat de l'API
     : statuts HTTP, structure JSON, cas d'erreur, chaînes CRUD complètes
k6 ajoute une brique performance/charge sur l'API
     : Il se concentre sur la performance, le temps de réponse, le taux d'erreur,
      le comportement de l'application sous une charge soutenue
Dans le pipeline :
Les collections Postman tournent en premier
       pour s'assurer que l'API est correcte
Le job k6 tourne ensuite sur staging
       pour vérifier que cette API tient la charge

L'objectif est de prouver que l'API supporte une charge concurrente raisonnable avant d'envisager un déploiement sur la branche finale

Voir le pipeline complet dans le repository Flashcard
Le job "load-test"
Steps:
Checkout
Download app JAR
Set up JDK 17
Start Spring Boot

k6 tourne dans le même pipeline que les tests fonctionnels ce qui donne une vue "qualité + performance"

- name: Setup k6
  uses: grafana/setup-k6-action@v1.1.0

- name: Run k6 LOCAL (staging)
  env:
    BASE_URL: http://localhost:8081
  run: k6 run load-test/flashcards.js 
       --vus 50 --duration 2m --out json=k6-report.json
Upload Spring Boot logs
Stop Spring Boot
LEGEND:
CI de base
Ajout (staging/main)
Artefacts (upload/download)

Le job load-test tourne sur ubuntu-24.04
Il s'exécute après les tests fonctionnels (needs: functional-tests)
et, uniquement sur push pour éviter de surcharger les PR

Il utilise un environnement staging réel: Spring Boot + Postgres et profil "staging"

Il réutilise l'artifact, téléchargé dans le job précédent ("functional-tests") pour faire tourner les tests dedans


Les logs applicatifs sont uploadés en artifact, ce qui facilite l'analyse post-mortem en cas de problème


Le résultat

Output du job dans les Actions Github: Workflow "staging" et regarder le job "load-test"

 Run k6 run load-test/flashcards.js --vus 50 --duration 2m --out json=k6-report.json

         /\      Grafana   /‾‾/  
    /\  /  \     |\  __   /  /   
   /  \/    \    | |/ /  /   ‾‾\ 
  /          \   |   (  |  (‾)  |
 / __________ \  |_|\_\  \_____/ 

     execution: local
        script: load-test/flashcards.js
        output: json (k6-report.json)

     scenarios: (100.00%) 1 scenario, 50 max VUs, 2m30s max duration (incl. graceful stop):
              * default: 50 looping VUs for 2m0s (gracefulStop: 30s)

running (0m01.0s), 50/50 VUs, 0 complete and 0 interrupted iterations
default   [   1% ] 50 VUs  0m01.0s/2m0s

running (0m02.0s), 50/50 VUs, 0 complete and 0 interrupted iterations
default   [   2% ] 50 VUs  0m02.0s/2m0s
//...
running (2m01.0s), 05/50 VUs, 5524 complete and 0 interrupted iterations
default ↓ [ 100% ] 50 VUs  2m0s

  █ THRESHOLDS 
    http_req_duration
    ✓ 'p(95)<500' p(95)=21.01ms

    http_req_failed
    ✓ 'rate<0.01' rate=0.00%

  █ TOTAL RESULTS 
    checks_total.......: 22116   182.666242/s
    checks_succeeded...: 100.00% 22116 out of 22116
    checks_failed......: 0.00%   0 out of 22116

    ✓ create category OK
    ✓ create flashcard OK
    ✓ get categories OK
    ✓ get flashcards OK

    HTTP
    http_req_duration..............: avg=9.53ms min=851.92µs med=4.53ms max=448.36ms p(90)=16.28ms p(95)=21.01ms
      { expected_response:true }...: avg=9.53ms min=851.92µs med=4.53ms max=448.36ms p(90)=16.28ms p(95)=21.01ms
    http_req_failed................: 0.00%  0 out of 22116
    http_reqs......................: 22116  182.666242/s

    EXECUTION
    iteration_duration.............: avg=1.08s  min=1.05s    med=1.07s  max=2.11s    p(90)=1.09s   p(95)=1.1s   
    iterations.....................: 5529   45.66656/s
    vus............................: 4      min=4          max=50
    vus_max........................: 50     min=50         max=50

    NETWORK
    data_received..................: 1.6 GB 13 MB/s
    data_sent......................: 3.0 MB 25 kB/s

running (2m01.1s), 00/50 VUs, 5529 complete and 0 interrupted iterations
default ✓ [ 100% ] 50 VUs  2m0s

Interpréter les métriques k6
Charge générée
load test
50 VUs pendant 2 minutes = ~5 500 itérations complètes
≈ 183 requêtes HTTP / seconde (4 requêtes par itération x ~46 itérations/s)

Pour un petit service CRUD Spring Boot, c'est déjà une charge significative


Qualité de service (latence)
Métrique Valeur typique
Médiane (p50) ~4-5 ms
p90 ~16 ms
p95 ~21 ms
Max ~400-450 ms

La p95 < 500 ms satisfait le seuil défini dans le thresholds
Les pics max restent ponctuels, typiquement liés au GC ou à des IO,
     ce qui est acceptable

On a, à présent, un test de charge automatisé sur la branche staging
ce qui prouve que l'API supporte la création concurrente de catégories et de flashcards à ~180 req/s, tout en restant sous 500 ms pour 95% des réponses

Dans l'article suivant, nous passerons à l'intégration de tous les tests (postman/newman, load tests) dans le pipeline [staging]

Laissez-moi un commentaire

En postant un commentaire anonyme, vous adhérez automatiquement aux conditions d'utilisation du site.