Tests de brokers

Test de charge MQTT avec k6 et xk6-mqtt

Testez la charge d'un broker MQTT avec k6 et l'extension xk6-mqtt : installez ou compilez k6, écrivez des scripts publishers/subscribers, mesurez la latence.

Mis à jour · 9 min de lecture

Test de charge MQTT avec k6 et xk6-mqtt

k6 peut tester la charge d'un broker MQTT grâce à l'extension xk6-mqtt : vous écrivez le test en JavaScript avec un Client de style MQTT.js, k6 l'exécute avec des centaines ou des milliers d'utilisateurs virtuels, et les thresholds transforment le résultat en un contrôle réussi/échoué exécutable en CI. C'est idéal quand vous voulez des tests de charge sous forme de code, versionnés à côté de votre application.

Ce guide explique comment obtenir un binaire k6 avec le support MQTT, le modèle d'event loop qui piège la plupart des premiers scripts, un script de charge avec publishers, subscribers et une métrique de latence de bout en bout, ainsi que des thresholds qui font échouer l'exécution quand le broker ralentit.

Obtenir k6 avec le support MQTT

MQTT ne fait pas partie du binaire de base de k6. Il provient de l'extension xk6-mqtt maintenue par Grafana, importée via k6/x/mqtt. Deux façons de l'utiliser :

Résolution automatique des extensions (k6 v1.2.0 et suivantes). Importez le module dans votre script et lancez-le normalement ; k6 détecte l'import et provisionne l'extension :

Vérifier la version et lancer
k6 version          # nécessite v1.2.0 ou plus récent
k6 run mqtt-smoke.js

Binaire personnalisé avec xk6. Utile en CI sans accès réseau ou pour figer la version exacte de l'extension :

Compiler un binaire k6 incluant xk6-mqtt
go install go.k6.io/xk6/cmd/xk6@latest
xk6 build --with github.com/grafana/xk6-mqtt
./k6 run mqtt-smoke.js

D'abord un smoke test

Commencez avec un seul utilisateur virtuel qui se connecte, s'abonne, publie un message et se déconnecte à sa réception. Si cela échoue, un test de charge échouera simplement plus vite.

mqtt-smoke.js
import { Client } from "k6/x/mqtt";

const BROKER = __ENV.MQTT_URL || "mqtt://broker.connectmq.com:1883";

export default function () {
  const client = new Client({
    username: __ENV.MQTT_USER,
    password: __ENV.MQTT_PASS,
  });
  const topic = "testmqtt/k6/smoke";

  client.on("connect", () => {
    client.subscribe(topic, { qos: 1 });
    client.publish(topic, "bonjour depuis k6", { qos: 1 });
  });

  client.on("message", (t, payload) => {
    console.log(`reçu sur ${t} : ${new TextDecoder().decode(payload)}`);
    client.end();
  });

  client.on("error", (err) => {
    console.error(`${err.method}: ${err.message}`);
    client.end();
  });

  client.connect(BROKER);
}
Lancer le test
MQTT_USER=username MQTT_PASS=password k6 run mqtt-smoke.js

Gardez le client MQTT en ligne abonné à testmqtt/k6/# pendant l'exécution : vous verrez le message arriver, ce qui confirme le topic et les identifiants indépendamment de k6. L'endpoint partagé broker.connectmq.com convient pour ce smoke test ; lancez le script de charge ci-dessous contre votre propre instance privée de broker en définissant MQTT_URL et les identifiants.

Le modèle d'event loop

C'est ce qui diffère des tests HTTP. Avec xk6-mqtt, une itération ne se termine pas quand la fonction default retourne. Le VU continue d'exécuter son event loop — en traitant les callbacks connect, message et des timers — jusqu'à la fermeture de la connexion, soit parce que vous appelez client.end(), soit parce que le broker la coupe.

Conséquences pour vos scripts :

  • Une itération = une session MQTT. Décidez de la durée d'une session et terminez-la explicitement avec setTimeout(() => client.end(), …).
  • Utilisez setInterval dans le handler connect pour publier à rythme fixe. sleep() bloquerait l'event loop.
  • Gérez toujours error et appelez-y client.end(), sinon un VU en échec peut rester bloqué jusqu'au graceful stop du scénario.
  • Donnez à chaque connexion un client_id unique (ou laissez-le vide pour que le broker en attribue un). Les doublons provoquent des session takeovers qui ressemblent à des déconnexions aléatoires.

Un script de charge avec latence de bout en bout

Les métriques intégrées comptent connexions, publications et messages reçus, et mesurent la durée de connexion, mais elles n'indiquent pas le temps mis par un message pour aller du publisher au subscriber. Ajoutez-le vous-même : placez Date.now() dans le payload et enregistrez la différence dans une Trend personnalisée. Publisher et subscriber tournent dans le même VU et partagent donc la même horloge.

mqtt-load.js
import { Client } from "k6/x/mqtt";
import { Trend, Counter } from "k6/metrics";
import exec from "k6/execution";

const BROKER = __ENV.MQTT_URL || "mqtt://broker.connectmq.com:1883";
const SESSION_MS = 30000;   // une itération = une session de 30 s
const INTERVAL_MS = 100;    // 10 messages par seconde et par VU

const e2eLatency = new Trend("mqtt_e2e_latency", true);
const mqttErrors = new Counter("mqtt_errors");

export const options = {
  scenarios: {
    telemetry: {
      executor: "ramping-vus",
      startVUs: 0,
      stages: [
        { duration: "30s", target: 100 },
        { duration: "3m", target: 100 },
        { duration: "15s", target: 0 },
      ],
      gracefulRampDown: "35s",
    },
  },
  thresholds: {
    mqtt_e2e_latency: ["p(95)<100", "p(99)<250"],
    mqtt_connect_duration: ["p(95)<1000"],
    mqtt_errors: ["count<1"],
  },
};

export default function () {
  const vu = exec.vu.idInTest;
  const topic = `testmqtt/k6/${vu}/data`;
  const client = new Client({
    client_id: `k6-${vu}-${exec.vu.iterationInScenario}`,
    username: __ENV.MQTT_USER,
    password: __ENV.MQTT_PASS,
  });

  client.on("connect", () => {
    client.subscribe(topic, { qos: 1 });
    let seq = 0;
    const timer = setInterval(() => {
      const body = JSON.stringify({ seq: seq++, t: Date.now(), pad: "x".repeat(200) });
      client.publish(topic, body, { qos: 1 });
    }, INTERVAL_MS);
    setTimeout(() => {
      clearInterval(timer);
      client.end();
    }, SESSION_MS);
  });

  client.on("message", (t, payload) => {
    const msg = JSON.parse(new TextDecoder().decode(payload));
    e2eLatency.add(Date.now() - msg.t);
  });

  client.on("error", (err) => {
    mqttErrors.add(1, { method: err.method });
    client.end();
  });

  client.connect(BROKER, { keepalive: 30 });
}

Avec 100 VUs × 10 messages par seconde, l'objectif est de 1 000 messages publiés et 1 000 livrés par seconde. Le payload fait environ 250 octets ; ajustez pad pour correspondre à vos appareils et vérifiez la taille réelle sur le réseau avec le calculateur de taille de paquet.

gracefulRampDown est réglé un peu au-dessus de la durée de session pour que les VUs terminent leur session en cours pendant la descente au lieu d'être coupés en plein flux, ce qui apparaîtrait comme des messages manquants.

Des thresholds qui font échouer l'exécution

Les thresholds font sortir k6 avec un code non nul quand une condition est violée — exactement ce qu'il faut dans un pipeline :

ThresholdSignification
mqtt_e2e_latency: p(95)<10095 % des messages livrés en moins de 100 ms
mqtt_e2e_latency: p(99)<250La latence de queue reste sous 250 ms
mqtt_connect_duration: p(95)<1000Les connexions (TLS compris) aboutissent en moins de 1 s
mqtt_errors: count<1Toute erreur MQTT fait échouer le test

Ces valeurs sont des exemples. Fixez-les à partir de votre propre référence : lancez le test sur un broker au repos, notez p95/p99 et gardez de la marge. Comparez aussi les compteurs intégrés mqtt_messages_sent et mqtt_messages_received dans le résumé final — en QoS 1 avec un subscriber par topic, ils doivent être proches ; un écart croissant signifie des messages mis en file ou perdus. Pour aller plus loin, voir interpréter les résultats de benchmark.

Choisir un executor

Comme une itération correspond à une session MQTT, l'executor détermine combien de connexions sont ouvertes et à quelle fréquence de nouvelles arrivent :

ExecutorModéliseRemarque
constant-vusUne flotte fixe d'appareils toujours connectésConnexions simultanées = VUs
ramping-vusUne flotte qui se connecte par paliersUtilisé dans le script ci-dessus ; montez lentement pour voir où la latence décroche
constant-arrival-rateDe nouvelles sessions qui démarrent à rythme fixeFixez preAllocatedVUs ≥ débit × durée de session, sinon k6 manquera de VUs
per-vu-iterationsChaque VU exécute exactement N sessionsPratique pour les tests de reconnexion : sessions courtes, nombreuses itérations

Pour un test de tempête de reconnexions, réduisez SESSION_MS à quelques secondes et utilisez constant-arrival-rate : chaque itération effectue alors un cycle complet CONNECT, SUBSCRIBE et DISCONNECT, et mqtt_connect_duration montre comment le temps de connexion se dégrade quand le débit d'arrivée augmente. Voir les tempêtes de reconnexion pour savoir quoi surveiller.

TLS et WebSockets

Changez le schéma de l'URL pour tester d'autres transports avec le même script :

Même script, autres transports
k6 run -e MQTT_URL=mqtts://broker.connectmq.com:8883 mqtt-load.js
k6 run -e MQTT_URL=wss://broker.connectmq.com:8084/mqtt mqtt-load.js

TLS utilise la configuration TLS standard de k6 (par exemple tlsAuth pour les certificats client). Comparer le même scénario en TCP et en TLS est le moyen le plus rapide de mesurer le coût du handshake — voir tests de performance TLS MQTT.

Exécuter k6 en CI

  1. Gardez mqtt-smoke.js comme contrôle rapide sur chaque pull request et lancez mqtt-load.js chaque nuit ou avant les releases.
  2. Passez les identifiants comme secrets via -e MQTT_USER=… -e MQTT_PASS=… ou des variables d'environnement ; ne les écrivez jamais en dur.
  3. Exportez les résultats avec --summary-export=summary.json ou une sortie comme --out json=results.json pour conserver un historique.
  4. Exécutez le générateur de charge dans la même région que l'endpoint broker de vos appareils et surveillez son CPU ; un générateur saturé gonfle la latence.

Voir les tests MQTT dans GitHub Actions pour un workflow complet.

Voir le trafic k6 dans le client en ligne

k6 avec xk6-mqtt transforme un test de charge MQTT en un script court et relisible, avec des critères de réussite clairs. Maîtrisez le modèle d'event loop, mesurez vous-même la latence de bout en bout et laissez les thresholds vous signaler quand une modification du broker a tout ralenti.

Questions fréquentes

k6 prend-il en charge MQTT nativement ?

MQTT est fourni par l'extension xk6-mqtt, importée via k6/x/mqtt. Les versions récentes de k6 (v1.2.0 et suivantes) résolvent automatiquement les extensions officielles importées par un script ; sinon, compilez un binaire avec xk6 build --with github.com/grafana/xk6-mqtt.

Pourquoi les anciens exemples MQTT pour k6 utilisent-ils une autre API ?

Plusieurs projets communautaires s'appelaient aussi xk6-mqtt, avec des API basées sur le constructeur. L'extension actuelle de Grafana utilise un Client de style MQTT.js avec connect, subscribe, publish et des handlers on(event). Ne mélangez pas les exemples des deux.

Comment mesurer la latence MQTT dans k6 ?

Placez Date.now() dans chaque payload, abonnez-vous au même topic dans le même VU et ajoutez la différence à une métrique Trend personnalisée à la réception du message. Posez ensuite un threshold p(95) ou p(99) sur cette Trend.