Tests de brokers

Comment tester les garanties de livraison QoS 1 et QoS 2

Prouvez la livraison at-least-once et exactly-once de MQTT : coupez les connexions en plein envoi, comptez doublons et trous, inspectez PUBACK et PUBREC.

Mis à jour · 9 min de lecture

Comment tester les garanties de livraison QoS 1 et QoS 2

Pour tester les garanties de livraison de MQTT, publiez des messages numérotés, coupez la connexion pendant que des messages sont en vol, et comptez ce qui arrive. La QoS 1 réussit quand aucun numéro de séquence ne manque (les doublons sont acceptés). La QoS 2 réussit quand rien ne manque et que rien n'est en double. Tout le reste — sessions persistantes, paquets d'acquittement, limites de file du broker — sert à rendre cette expérience honnête.

Ce guide montre comment observer le flux d'acquittements, un harnais Python qui compte trous et doublons, les injections de pannes qui valent la peine et comment lire les résultats.

Ce que promet chaque niveau

QoS 0QoS 1QoS 2
GarantieAu plus une foisAu moins une foisExactement une fois
Paquets par sautPUBLISHPUBLISH → PUBACKPUBLISH → PUBREC → PUBREL → PUBCOMP
Après une coupure de connexionLe message peut être perduRenvoyé avec DUP=1, doublons possiblesReprise à la dernière étape, pas de doublon
Le test réussit si— (la perte est autorisée)trous = 0trous = 0 et doublons = 0

Deux détails décident de la plupart des résultats. D'abord, la QoS est par saut : la QoS du publisher couvre publisher → broker, la QoS de l'abonnement couvre broker → abonné, et la QoS de livraison effective est la plus basse des deux. Un publish en QoS 2 vers un abonnement en QoS 1 est une livraison en QoS 1. Ensuite, les garanties ne survivent à une déconnexion que si la session survit — sinon le broker jette l'état en vol. Le guide des niveaux de QoS couvre la théorie ; ici, on la teste.

Étape 1 : observer les acquittements

Avant d'automatiser quoi que ce soit, regardez les paquets. Les clients Mosquitto affichent chaque paquet de contrôle avec -d :

Two terminals
# Terminal 1: subscribe at QoS 2 with debug output
mosquitto_sub -h broker.connectmq.com -p 1883 -i testmqtt-qos-sub -t 'testmqtt/qos/demo' -q 2 -d

# Terminal 2: publish one QoS 2 message with debug output
mosquitto_pub -h broker.connectmq.com -p 1883 -i testmqtt-qos-pub -t 'testmqtt/qos/demo' -m 'hello' -q 2 -d

Le publisher affiche un flux semblable à celui-ci :

mosquitto_pub -d, QoS 2
Client testmqtt-qos-pub sending CONNECT
Client testmqtt-qos-pub received CONNACK (0)
Client testmqtt-qos-pub sending PUBLISH (d0, q2, r0, m1, 'testmqtt/qos/demo', ... (5 bytes))
Client testmqtt-qos-pub received PUBREC (Mid: 1)
Client testmqtt-qos-pub sending PUBREL (m1)
Client testmqtt-qos-pub received PUBCOMP (Mid: 1)
Client testmqtt-qos-pub sending DISCONNECT

d0 est le flag DUP, q2 la QoS, m1 le packet identifier. Recommencez avec -q 1 : vous devriez voir un seul PUBACK au lieu de la poignée de main en trois étapes. Côté abonné, vérifiez la valeur q du PUBLISH reçu : si vous vous êtes abonné en QoS 1, elle indique q1 même si le publisher a utilisé la QoS 2.

Pour une inspection plus fine, capturez le trafic et filtrez dans Wireshark avec mqtt.msgtype >= 3 && mqtt.msgtype <= 7 (de PUBLISH à PUBCOMP) ou mqtt.dupflag == 1 pour ne voir que les retransmissions. En MQTT 5, les acquittements portent aussi un reason code : un PUBACK avec 0x10 (No matching subscribers) est un succès, pas une erreur — consultez les codes dans la référence des reason codes.

Étape 2 : un compteur de trous et de doublons

Le harnais comporte trois parties : un abonné avec session persistante qui journalise chaque numéro de séquence reçu, un publisher qui envoie une plage connue et compte les acquittements, et un court script d'analyse.

sub.py — run first, keep running
import sys
import paho.mqtt.client as mqtt

QOS = int(sys.argv[1]) if len(sys.argv) > 1 else 1
TOPIC = "testmqtt/qos-test/seq"
log = open(f"received-q{QOS}.log", "a", buffering=1)  # line-buffered: survives kill -9

def on_connect(client, userdata, flags, reason_code, properties):
    print(f"connected: {reason_code}, session present: {flags.session_present}")
    client.subscribe(TOPIC, qos=QOS)

def on_message(client, userdata, msg):
    log.write(f"{msg.payload.decode()} {int(msg.dup)}\n")

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
                     client_id=f"testmqtt-qos-sub-q{QOS}", clean_session=False)
# client.username_pw_set("username", "password")
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=10)
client.loop_forever()   # reconnects automatically after a network error
pub.py
import sys, time
import paho.mqtt.client as mqtt

QOS = int(sys.argv[1]) if len(sys.argv) > 1 else 1
N = int(sys.argv[2]) if len(sys.argv) > 2 else 2000
TOPIC = "testmqtt/qos-test/seq"
acked = 0

def on_publish(client, userdata, mid, reason_code, properties):
    global acked
    acked += 1          # QoS 1: on PUBACK, QoS 2: on PUBCOMP

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
                     client_id=f"testmqtt-qos-pub-q{QOS}", clean_session=False)
client.on_publish = on_publish
client.connect("broker.connectmq.com", 1883, keepalive=10)
client.loop_start()

for seq in range(N):
    client.publish(TOPIC, str(seq), qos=QOS)   # queued by paho while disconnected
    time.sleep(0.005)

deadline = time.time() + 60
while acked < N and time.time() < deadline:
    time.sleep(0.2)
print(f"published {N}, acknowledged {acked}")
client.disconnect()
client.loop_stop()
analyze.sh
#!/usr/bin/env bash
LOG=${1:-received-q1.log}; N=${2:-2000}
echo "received lines : $(wc -l < "$LOG")"
echo "unique         : $(cut -d' ' -f1 "$LOG" | sort -u | wc -l)"
echo "duplicates     : $(cut -d' ' -f1 "$LOG" | sort | uniq -d | wc -l)"
echo "with DUP flag  : $(awk '$2 == 1' "$LOG" | wc -l)"
echo "missing        : $(comm -23 <(seq 0 $((N - 1)) | sort) <(cut -d' ' -f1 "$LOG" | sort -u) | wc -l)"

Lancez python sub.py 1, puis python pub.py 1, puis ./analyze.sh received-q1.log. Commencez par une exécution sans panne : elle doit afficher 2 000 uniques et 0 manquant. Si la référence échoue déjà, corrigez d'abord ce problème (mauvaise QoS sur l'abonnement, une ACL, une limite de taille de message) avant d'injecter des pannes.

Étape 3 : casser des choses en plein vol

Exécutez chaque scénario pendant que pub.py envoie, puis analysez. Utilisez un nouveau fichier de log à chaque exécution.

ScénarioCommentLa QoS 1 doit montrerLa QoS 2 doit montrer
RéférenceAucune panne0 manquant0 manquant, 0 doublon
Réinitialiser les connexionssudo ss -K dst <broker-ip> dport = 1883 (Linux)0 manquant, quelques doublons0 manquant, 0 doublon
Trou noir de 20 sRègle DROP iptables, puis la supprimer0 manquant, doublons probables0 manquant, 0 doublon
Latence et resetsToxics latency et reset_peer de Toxiproxy0 manquant0 manquant, 0 doublon
Crash de l'abonnékill -9 sur sub.py, redémarrage en quelques secondes0 manquant, doublons probablesVoir plus bas
Blackhole the broker for 20 seconds (Linux, root)
BROKER_IP=$(dig +short broker.connectmq.com | head -1)
sudo iptables -I OUTPUT -p tcp -d "$BROKER_IP" --dport 1883 -j DROP
sleep 20
sudo iptables -D OUTPUT -p tcp -d "$BROKER_IP" --dport 1883 -j DROP

Le trou noir est la panne la plus réaliste : aucun RST n'arrive, le client ne s'en aperçoit donc que via le keep-alive (environ 1,5 × les 10 secondes de keep-alive côté broker), puis il se reconnecte. À la reconnexion, l'abonné doit afficher session present: True, et le broker doit renvoyer les messages non acquittés avec DUP=1, suivis de l'arriéré en file d'attente. D'autres techniques de coupure sont décrites dans tester la QoS avec une coupure réseau.

Le crash du processus est le cas intéressant pour la QoS 2. paho-mqtt garde son état de session en mémoire : un kill -9 efface donc la moitié client de la poignée de main QoS 2. Selon qu'une bibliothèque remet le message à votre code à la réception du PUBLISH ou du PUBREL, un crash au mauvais moment peut produire un doublon ou un message manquant. L'exactly-once est une propriété du protocole entre deux extrémités qui conservent leur état — si votre application ne doit pas traiter deux fois après un crash, dédupliquez sur un ID de message dans votre propre stockage.

Lire les résultats

  • Messages manquants en QoS 1/2 après une coupure réseau — la session n'a pas persisté : vérifiez session present dans le log, le client ID (il doit être identique à la reconnexion) et le session expiry en MQTT 5.
  • Messages manquants uniquement après de longues coupures — la file par client du broker s'est remplie et il a jeté des messages selon sa politique (par exemple max_queued_messages dans Mosquitto). C'est une limite de capacité, pas un bug de QoS ; voir file de messages pleine.
  • Doublons en QoS 2 — vérifiez la QoS effective côté abonné (q2 dans mosquitto_sub -d), puis cherchez des crashs ou un second client partageant le client ID. D'autres causes dans messages en double.
  • Doublons sans flag DUP en QoS 1 — en général, le publisher a renvoyé après sa reconnexion, ce qui constitue un nouveau message du point de vue du broker. Normal en at-least-once.
  • acknowledged inférieur à published — le publisher a abandonné avant que l'arriéré soit écoulé ; augmentez le délai ou vérifiez une limite d'inflight côté broker.

Une vérification manuelle rapide

Pour un contrôle rapide sans script, abonnez-vous en QoS 1 dans le client MQTT en ligne, publiez depuis un terminal avec mosquitto_pub -q 1 -d et comparez la QoS affichée des deux côtés. Déconnectez ensuite le client du navigateur, publiez encore quelques messages et reconnectez-vous avec le même client ID et clean session désactivé : les messages en file d'attente doivent arriver.

Observez les livraisons QoS dans votre navigateur

La QoS 1 se prouve par zéro trou, la QoS 2 par zéro trou et zéro doublon — mesurés avec des numéros de séquence à travers de vraies déconnexions, et non supposés d'après la spécification. Gardez le harnais dans votre dépôt et relancez-le à chaque changement de broker, de bibliothèque cliente ou de paramètres de session.

Questions fréquentes

Comment prouver que MQTT QoS 1 ne perd pas de messages ?

Publiez des messages numérotés en QoS 1 vers un abonné avec session persistante, coupez la connexion au milieu du test, puis comparez les numéros de séquence reçus à la plage envoyée. Zéro trou prouve l'at-least-once ; les doublons sont autorisés et attendus.

Pourquoi ai-je des doublons en QoS 2 ?

Généralement parce qu'un saut n'est pas vraiment en QoS 2 (l'abonnement a été fait en QoS 1, la livraison est donc rétrogradée), parce que la session n'était pas persistante, ou parce que le processus client a planté et perdu son état de session en mémoire. L'exactly-once ne tient qu'entre des extrémités qui conservent leur état de session.

Ai-je besoin d'une session persistante pour tester la QoS ?

Oui, pour tout test qui implique une déconnexion. Sans session persistante (clean session, ou MQTT 5 avec un session expiry à 0), le broker jette les messages en vol et en file d'attente quand la connexion tombe : vous testez alors la gestion des sessions, pas la QoS.