Tests de brokers

MQTT garantit-il l'ordre des messages ? Testez-le vous-même

Ce que MQTT garantit vraiment sur l'ordre des messages par topic et QoS, où il se casse, et des scripts Python qui détectent désordre et doublons.

Mis à jour · 8 min de lecture

MQTT garantit-il l'ordre des messages ? Testez-le vous-même

MQTT ne garantit l'ordre des messages que dans un périmètre étroit : les messages d'un publisher, sur un topic, avec un niveau de QoS sont livrés à chaque subscriber dans l'ordre où le broker les a reçus. Entre topics, entre publishers et au sein des abonnements partagés, il n'y a aucune garantie d'ordre — et même dans le périmètre garanti, les retransmissions QoS 1 peuvent produire des doublons tardifs.

Cet article explique précisément ce que promet la spécification, où l'ordre se casse en pratique, et vous donne deux courts scripts Python qui mesurent les messages désordonnés et les doublons sur votre propre broker.

Ce que dit la spécification

MQTT 3.1.1 comme MQTT 5 (section 4.6, « Message ordering ») définissent par défaut chaque topic comme un Ordered Topic. Lorsqu'un broker transmet des messages sur un topic ordonné, il doit envoyer les paquets PUBLISH aux subscribers « pour le même topic et le même QoS » dans l'ordre où il les a reçus de chaque client. Les clients, de leur côté, doivent renvoyer les messages non acquittés et envoyer les acquittements dans l'ordre.

La spécification signale aussi le piège. En QoS 1, un publisher qui envoie 1, 2, 3, 4 peut amener un subscriber à voir 1, 2, 3, 2, 3, 4 : après une reconnexion, les copies retransmises de 2 et 3 arrivent après les originaux. La copie finale de chaque message est dans l'ordre, mais un doublon peut apparaître après son successeur. S'il n'y a jamais plus d'un message en vol à la fois, cela ne peut pas se produire.

ScénarioOrdre garanti ?
Un publisher, un topic, même QoSOui, par subscriber
Un publisher, plusieurs topicsNon — l'entrelacement entre topics n'est pas défini
Plusieurs publishers, un topicSeulement par publisher ; leur entrelacement est arbitraire
Même publisher, QoS mélangés sur un topicNon — la règle s'applique par QoS
QoS 1 après une reconnexionCopies finales dans l'ordre ; des doublons peuvent arriver en retard
QoS 0 sous chargeL'ordre de ce qui arrive est conservé, mais des messages peuvent manquer
Groupe d'abonnement partagéNon — les messages sont répartis entre les membres

Les niveaux de QoS MQTT détaillent les échanges d'acquittement derrière ces règles.

Où l'ordre se casse en pratique

  • Plusieurs messages en vol et reconnexions. Clients et brokers autorisent généralement de nombreux messages QoS 1/2 non acquittés à la fois (en MQTT 5, la limite est négociée via Receive Maximum). Si la connexion tombe avec plusieurs messages en vol, le renvoi peut arriver après des messages plus récents.
  • Abonnements partagés. $share/group/topic répartit les messages entre consommateurs. Chacun peut voir sa part dans l'ordre, mais le traitement est parallèle, donc le résultat global est désordonné. Voir les abonnements partagés.
  • Plusieurs publishers. Deux passerelles qui remontent le même appareil produisent deux flux ordonnés indépendamment. Le broker peut les entrelacer n'importe comment.
  • Bridges et clusters. Un bridge n'est qu'un client de plus : une connexion de bridge conserve l'ordre, plusieurs chemins entre brokers non. Les brokers en cluster préservent généralement l'ordre par publisher sur un topic, mais consultez la documentation de votre broker.
  • Votre propre consommateur. La cause la plus fréquente de toutes : les messages arrivent dans l'ordre, puis sont traités en parallèle (pools de threads, handlers async, files de workers).

Test 1 : mesurer l'ordre sur votre broker

Le publisher numérote les messages par topic et par identifiant de publisher. Le subscriber retient la plus haute séquence vue par (topic, publisher) et signale tout retour en arrière, chaque doublon et tout ce qui manque.

order_sub.py
import os, sys, time
from collections import Counter, defaultdict
import paho.mqtt.client as mqtt

HOST = os.getenv("MQTT_HOST", "broker.connectmq.com")
RUN = sys.argv[1]                      # même identifiant de run que le publisher
QOS = int(os.getenv("MQTT_QOS", "1"))
last = defaultdict(lambda: -1)         # (topic, publisher) -> séquence la plus haute
seen = defaultdict(set)
stats = Counter()

def on_connect(client, userdata, flags, reason_code, properties):
    client.subscribe(f"testmqtt/order/{RUN}/#", qos=QOS)

def on_message(client, userdata, msg):
    pub_id, seq = msg.payload.decode().split(":")
    key, seq = (msg.topic, pub_id), int(seq)
    stats["received"] += 1
    stats["dup_flag"] += msg.dup
    if seq in seen[key]:
        stats["duplicates"] += 1
        return
    seen[key].add(seq)
    if seq < last[key]:
        stats["out_of_order"] += 1
        print(f"OUT OF ORDER {key}: {seq} after {last[key]}")
    last[key] = max(last[key], seq)

def missing():
    return sum(last[k] + 1 - len(seen[k]) for k in seen)

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
if os.getenv("MQTT_USER"):
    client.username_pw_set(os.getenv("MQTT_USER"), os.getenv("MQTT_PASS"))
client.on_connect, client.on_message = on_connect, on_message
client.connect(HOST, 1883)
client.loop_start()
try:
    while True:
        time.sleep(5)
        print(dict(stats), "missing:", missing())
except KeyboardInterrupt:
    print("FINAL", dict(stats), "missing:", missing())
order_pub.py
import os, sys
import paho.mqtt.client as mqtt

HOST = os.getenv("MQTT_HOST", "broker.connectmq.com")
RUN, N = sys.argv[1], int(sys.argv[2]) if len(sys.argv) > 2 else 10000
PUB_ID = os.getenv("PUB_ID", "p1")
TOPICS = int(os.getenv("TOPICS", "4"))
QOS = int(os.getenv("MQTT_QOS", "1"))

pub = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
if os.getenv("MQTT_USER"):
    pub.username_pw_set(os.getenv("MQTT_USER"), os.getenv("MQTT_PASS"))
pub.max_inflight_messages_set(int(os.getenv("INFLIGHT", "20")))
pub.connect(HOST, 1883)
pub.loop_start()

seq, infos = [0] * TOPICS, []
for i in range(N):
    t = i % TOPICS
    infos.append(pub.publish(f"testmqtt/order/{RUN}/t{t}", f"{PUB_ID}:{seq[t]}", qos=QOS))
    seq[t] += 1
for info in infos:
    info.wait_for_publish(timeout=60)
pub.disconnect()
pub.loop_stop()
print(f"published {N} messages on {TOPICS} topics as {PUB_ID}")
Lancer la référence
pip install "paho-mqtt>=2"
python order_sub.py run42            # terminal 1, à lancer en premier
python order_pub.py run42 10000      # terminal 2

Sur un broker sain et un réseau stable, la référence doit se terminer avec out_of_order: 0, duplicates: 0 et missing: 0. Avant les scripts, vous pouvez suivre le flux en direct en vous abonnant à testmqtt/order/# dans le client MQTT en ligne pendant que le publisher tourne.

Test 2 : provoquer les cas limites

Changez une seule variable à la fois et comparez avec la référence :

ExpérienceCommentCe que vous devriez voir
Deux publishersLancez order_pub.py deux fois en parallèle avec PUB_ID=p1 et PUB_ID=p2Le flux de chaque publisher reste ordonné ; ils s'entrelacent
QoS 0 sous chargeMQTT_QOS=0, N plus grand, plusieurs publishersÉventuellement missing > 0, mais aucun désordre
Coupure réseauBloquez le port 1883 du publisher quelques secondes en cours de run (voir le QoS face à une coupure réseau)Doublons et compteur dup_flag ; renvois tardifs possibles avec une session persistante
Un seul en volINFLIGHT=1 plus une coupure réseauDoublons possibles, mais aucun message après son successeur
Abonnement partagéAbonnez deux copies de order_sub.py à $share/g/testmqtt/order/run42/#Chaque copie voit des trous — l'autre membre a reçu ces messages

Pour l'essai avec abonnement partagé, remplacez l'appel subscribe par le filtre $share/... ; vous pouvez vérifier qu'un filtre correspond à vos topics avec le topic matcher. Les doublons ne sont pas un bug du broker — c'est le QoS 1 qui fait son travail ; les messages en double expliquent comment rendre les consommateurs idempotents.

Ne perdez pas l'ordre dans votre propre code

La plupart des signalements « MQTT a mélangé mes messages » viennent du consommateur. En Node.js, un handler de messages async n'attend pas le message précédent :

Désordonné : les handlers se chevauchent
client.on("message", async (topic, payload) => {
  await saveToDb(topic, payload); // le message 2 peut finir avant le message 1
});

Chaînez le travail par topic pour conserver l'ordre par topic tout en traitant les différents topics en parallèle :

Ordonné par topic
const chains = new Map();
client.on("message", (topic, payload) => {
  const prev = chains.get(topic) ?? Promise.resolve();
  const next = prev.then(() => saveToDb(topic, payload)).catch(console.error);
  chains.set(topic, next);
});

Il en va de même pour les pools de threads en Python ou Java et pour les files de workers en aval du broker : partitionnez le travail par topic ou par identifiant d'appareil, jamais en round-robin.

Concevoir en tenant compte de l'ordre

  • Placez les événements qui doivent rester ordonnés sur un seul topic issu d'un seul publisher — par exemple site/device42/events, pas un topic par type d'événement.
  • Ajoutez un numéro de séquence ou un horodatage source au payload, pour que les consommateurs puissent écarter les mises à jour périmées (« le dernier qui écrit gagne ») ou réordonner de petites fenêtres.
  • Rendez les handlers idempotents, car le QoS 1 livrera des doublons.
  • Considérez les abonnements partagés comme désordonnés par nature ; partitionnez par appareil si l'ordre compte.
  • Utilisez les messages retenus pour l'« état courant », où seule la dernière valeur compte et où l'ordre importe moins.

La garantie d'ordre de MQTT est réelle mais étroite : un publisher, un topic, un QoS. Dix minutes avec un test de numéros de séquence vous montrent comment se comportent votre broker et votre réseau, et les variantes vous montrent exactement où votre conception doit prendre le relais — généralement avec un numéro de séquence dans le payload et un consommateur qui traite chaque topic dans l'ordre.

Questions fréquentes

MQTT garantit-il l'ordre des messages ?

En partie. La spécification impose au broker de livrer les messages d'un même publisher, sur le même topic et avec le même QoS, dans l'ordre où il les a reçus. Il n'y a aucune garantie d'ordre entre topics, entre publishers différents ni entre membres d'un abonnement partagé.

Des messages QoS 1 peuvent-ils arriver dans le désordre ?

Oui, d'une manière précise : après une reconnexion, un message QoS 1 retransmis peut arriver après des messages plus récents, par exemple 1, 2, 3, 2, 3, 4. Les copies finales sont dans l'ordre, mais des doublons peuvent arriver en retard. Garder un seul message en vol à la fois l'évite.

Comment conserver l'ordre des messages MQTT entre plusieurs topics ?

MQTT ne peut pas le faire pour vous. Placez les événements liés sur un seul topic issu d'un seul publisher, ou ajoutez un numéro de séquence ou un horodatage au payload et laissez le consommateur réordonner ou écarter les messages périmés.