MQTT-Grundlagen

Doppelte MQTT-Nachrichten bei QoS 1 richtig behandeln

QoS 1 heißt mindestens einmal – und manchmal heißt das zweimal. Erfahren Sie, warum doppelte MQTT-Nachrichten entstehen, warum das DUP-Flag allein Sie nicht rettet und wie Sie idempotente Konsumenten bauen, die sicher damit umgehen.

Dieselbe Nachricht kommt zweimal bei einem Subscriber an, als Illustration doppelter MQTT-Nachrichten bei QoS 1
Probieren Sie es auf Ihrem eigenen MQTT-Broker ausHolen Sie sich im Free-Tarif einen privaten, verwalteten MQTT-Broker und starten Sie mit fertigen Codebeispielen.
Kostenlos testen Codebeispiele Keine Kreditkarte erforderlich

Doppelte MQTT-Nachrichten sind kein Fehler Ihres Brokers, sondern eine dokumentierte Folge von QoS 1, das die Zustellung „mindestens einmal“ verspricht. Verschluckt eine Netzwerkstörung eine Bestätigung, sendet der Absender erneut, und Ihr Konsument sieht dieselbe Nachricht zweimal. In der Fertigung ist das kein theoretisches Problem: Ein doppelt gezählter Stückzähler verfälscht die OEE, ein doppelt gebuchter Energiewert die Abrechnung pro Kostenstelle. In diesem Leitfaden zur Fehlersuche erfahren Sie genau, wann Duplikate entstehen, was das DUP-Flag Ihnen sagt und was nicht, und wie Sie einen idempotenten Python-Konsumenten schreiben, der korrekt damit umgeht.

Warum QoS 1 Duplikate erzeugt

QoS 1 nutzt auf jedem Hop einen zweistufigen Handshake: Der Absender überträgt PUBLISH, der Empfänger antwortet mit PUBACK. Bis das PUBACK eintrifft, behält der Absender die Nachricht und sendet sie gegebenenfalls erneut. Der Artikel MQTT-QoS-Stufen erklärt behandelt alle drei Stufen; hier konzentrieren wir uns darauf, was bei Stufe 1 schiefgeht.

Ein Duplikat entsteht immer dann, wenn der Empfänger die Nachricht erhalten hat, der Absender aber nicht die Bestätigung:

  1. Das Gerät publiziert Messwert #42 an den Broker.
  2. Der Broker empfängt ihn, leitet ihn an Subscriber weiter und sendet PUBACK.
  3. Die Verbindung bricht ab, bevor das PUBACK das Gerät erreicht.
  4. Das Gerät verbindet sich mit seiner Session neu und sendet #42 mit gesetztem DUP-Flag erneut.
  5. Der Broker behandelt sie als neue Nachricht und leitet sie erneut weiter.

Dasselbe passiert auf dem Hop vom Broker zum Subscriber. Hat der Subscriber eine Nachricht verarbeitet, sein PUBACK ging aber verloren, sendet der Broker sie erneut, sobald die Session des Subscribers fortgesetzt wird.

Beachten Sie, dass auf jedem Hop unabhängig ein Duplikat entstehen kann, sodass eine publizierte Nachricht einen Konsumenten gelegentlich auch mehr als zweimal erreicht.

Was das DUP-Flag Ihnen sagt

Jedes PUBLISH-Paket hat ein DUP-Bit. Ein Absender setzt es, wenn er ein QoS-1- oder QoS-2-Paket erneut überträgt. Es ist verlockend, jede Nachricht mit gesetztem DUP zu verwerfen, aber das ist aus mehreren Gründen falsch:

Annahme Realität
DUP heißt, ich habe diese Nachricht schon Nicht unbedingt. Die erste Kopie kann verloren gegangen sein, sodass die Wiederholung die einzige Kopie ist, die Sie erhalten
Kein DUP heißt, sie ist neu Nein. Der Broker leitet die Wiederholung eines Publishers als frische Zustellung weiter, ohne DUP auf dem ausgehenden Hop
Packet Identifiers erkennen Duplikate Nur innerhalb einer Session und eines In-flight-Fensters. IDs werden ständig wiederverwendet
QoS 2 löst alles QoS 2 verhindert Duplikate pro Hop, Wiederholungen auf Anwendungsebene können aber trotzdem welche erzeugen

Das DUP-Flag ist nützlich für Logging und Diagnose. Ein Deduplizierungsschlüssel ist es nicht. Echte Deduplizierung braucht eine Kennung, die Sie selbst in die Nachricht schreiben.

Duplikate reproduzieren

Echte erneute Zustellung hängt von verlorenen Paketen ab und lässt sich kaum gezielt auslösen. Der zuverlässige Weg, Ihren Konsumenten zu testen, ist, Duplikate selbst einzuschleusen. Publizieren Sie gegen den öffentlichen MQTT-Broker unter broker.connectmq.com dieselbe Nachrichten-ID zweimal:

for n in 1 2; do
  mosquitto_pub -h broker.connectmq.com -p 1883 -i dup-pub-t9v4 -q 1 \
    -t connectmq/demo/t9v4/meter-3/energy \
    -m '{"msgId":"meter-3-000042","kwh":0.25}'
done

Ein Konsument, der kwh ohne Deduplizierung aufsummiert, meldet jetzt 0,5 statt 0,25. Genau diese Art von Fehler verursacht die erneute QoS-1-Zustellung in der Produktion – nur seltener und schwerer zu entdecken.

Einen idempotenten Konsumenten bauen

Ein idempotenter Konsument liefert dasselbe Ergebnis, ob er eine Nachricht einmal oder fünfmal verarbeitet. Es gibt zwei gängige Techniken:

  1. Natürliche Idempotenz. Gestalten Sie die Wirkung so, dass Wiederholung harmlos ist: „Temperatur auf 21,5 setzen“ statt „um 0,5 erhöhen“; eine Zeile per Upsert mit Gerät und Zeitstempel als Schlüssel schreiben statt einzufügen.
  2. Explizite Deduplizierung. Geben Sie jeder Nachricht eine eindeutige ID und merken Sie sich kürzlich verarbeitete IDs.

Hier ein vollständiger Konsument mit paho-mqtt 2.x, der explizite Deduplizierung mit einem begrenzten, zeitlich befristeten Cache nutzt:

import json
import time
import uuid
from collections import OrderedDict
import paho.mqtt.client as mqtt

BROKER = "broker.connectmq.com"
TOPIC = "connectmq/demo/t9v4/+/energy"
DEDUP_TTL = 600          # Sekunden, die eine ID gemerkt wird
DEDUP_MAX = 50_000       # Speicherverbrauch begrenzen

seen = OrderedDict()     # msgId -> Zeitpunkt der ersten Sichtung
totals = {}

def already_seen(msg_id):
    now = time.time()
    while seen:
        oldest_id, ts = next(iter(seen.items()))
        if now - ts > DEDUP_TTL or len(seen) > DEDUP_MAX:
            seen.popitem(last=False)
        else:
            break
    if msg_id in seen:
        return True
    seen[msg_id] = now
    return False

def on_connect(client, userdata, flags, reason_code, properties):
    client.subscribe(TOPIC, qos=1)

def on_message(client, userdata, msg):
    try:
        data = json.loads(msg.payload)
        msg_id = data["msgId"]
    except (ValueError, KeyError):
        print("rejected message without msgId")
        return
    if already_seen(msg_id):
        print(f"duplicate {msg_id} (dup flag={msg.dup}) ignored")
        return
    meter = msg.topic.split("/")[-2]
    totals[meter] = totals.get(meter, 0) + data["kwh"]
    print(f"{meter} total={totals[meter]:.2f} kWh")

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
                     client_id=f"dedup-{uuid.uuid4().hex[:8]}")
client.on_connect = on_connect
client.on_message = on_message
client.connect(BROKER, 1883, keepalive=30)
client.loop_forever()

Starten Sie ihn und führen Sie dann die Doppel-Publish-Schleife aus dem vorherigen Abschnitt aus. Die erste Nachricht aktualisiert die Summe; die zweite wird als Duplikat gemeldet und ignoriert.

Eine gute Nachrichten-ID wählen

Die ID muss vom ursprünglichen Publisher einmal erzeugt und bei jeder Wiederholung wiederverwendet werden. Optionen:

  • Geräte-ID plus Sequenznummer, etwa meter-3-000042. Kompakt und zudem nützlich für Prüfungen der MQTT-Nachrichtenreihenfolge. Persistieren Sie den Zähler oder ergänzen Sie eine Boot-ID, damit er sich nach einem Neustart nicht wiederholt.
  • UUID pro Nachricht. Einfach und global eindeutig, auf Kosten von 36 Bytes.
  • Geräte-ID plus Messzeitstempel, sofern das Gerät nie zwei Messwerte mit demselben Zeitstempel erzeugt.

Erzeugen Sie die ID nicht im Konsumenten oder in einer Zwischenschicht; dort sieht ein Duplikat bereits wie eine neue Nachricht aus.

Deduplizierung skalieren

Der obige In-Memory-Cache funktioniert für einen einzelnen Konsumentenprozess. Für größere Systeme:

  • Datenbank-Constraints. Ein eindeutiger Index auf msg_id macht Duplikate zu harmlosen Konflikten. Mit einem Upsert (INSERT ... ON CONFLICT DO NOTHING in PostgreSQL) ist das oft die einzige Deduplizierung, die Sie brauchen.
  • Gemeinsamer Cache. Speist ein Konsument ein MES oder ERP, etwa mit gemeldeten Stückzahlen pro Auftrag, gehört die Deduplizierung vor die Buchung, nicht in einen nachträglichen Abgleich. Können mehrere Konsumenten einer Shared Subscription Wiederholungen derselben Nachricht erhalten, muss die Menge gesehener IDs geteilt werden, etwa in Redis mit TTL.
  • TTL dimensionieren. Das Fenster muss nur realistische Verzögerungen bei der erneuten Zustellung abdecken: Ihr Session Expiry plus Puffer. Duplikate kommen selten Stunden auseinander an, es sei denn, Sessions leben so lange.

Sollten Sie einfach QoS 2 verwenden?

QoS 2 ergänzt einen vierstufigen Handshake, der Exactly-once-Zustellung auf jedem Hop garantiert. Das kostet mehr Round-Trips und Broker-Zustand und schützt Sie nicht vor einem Gerät, das in einen Timeout läuft und denselben Messwert auf Anwendungsebene als neue Nachricht erneut publiziert. In eingeschränkten Plänen ist es womöglich gar nicht verfügbar; so unterstützt der kostenlose ConnectMQ-Serverless-Plan QoS bis 1, während der kostenpflichtige Serverless-Plan QoS 2 ergänzt. Wie Broker QoS ankündigen und herabstufen, erklärt MQTT Maximum QoS. In den meisten Systemen ist QoS 1 plus idempotente Konsumenten das robustere und günstigere Design.

Checkliste zur Fehlersuche

  1. Treten Duplikate zusammen mit Reconnects auf? Prüfen Sie die Client-Logs auf Verbindungsabbrüche und Keepalive-Timeouts.
  2. Wiederholt Ihr Publisher zusätzlich zu QoS 1 auf Anwendungsebene? Das verdoppelt die Wiederholungspfade.
  3. Teilen sich zwei Clients eine Client ID und werfen sich gegenseitig hinaus, was ständige Session-Wiederaufnahmen auslöst? Siehe MQTT-Client trennt ständig die Verbindung.
  4. Bestätigt der Konsument langsam? Lange Verarbeitung im Callback verzögert das PUBACK und vergrößert das Fenster für erneute Zustellung.
  5. Trägt jede Nachricht eine an der Quelle erzeugte ID?

Duplikate und Ihre Broker-Wahl

ConnectMQ implementiert die Standardsemantik von QoS 1 und QoS 2, die obigen Ratschläge gelten also direkt. Da Client IDs an ausgestellte Zugangsdaten gebunden sind, lässt sich die Reconnect-Schleife durch doppelte Client IDs, die oft massenhafte erneute Zustellung verursacht, leichter vermeiden. Ein verwalteter MQTT-Broker übernimmt Session-Speicherung und Wiederholungen für Sie; die Idempotenz Ihrer Konsumenten bleibt auf jedem Broker Ihre Verantwortung. Das MQTT-Tutorial ist ein guter nächster Schritt für das Gesamtbild.

Starten Sie mit ConnectMQ

Erstellen Sie unter app.connectmq.com einen kostenlosen Broker im Serverless-Plan, ganz ohne Kreditkarte. Richten Sie den deduplizierenden Konsumenten mit Ihrem ConnectMQ-Benutzernamen, Passwort und Ihrer Client ID darauf aus, und Ihre Summen bleiben korrekt, auch wenn das Netzwerk es nicht ist.

Häufig gestellte Fragen

Warum erhalte ich dieselbe MQTT-Nachricht zweimal?

Bei QoS 1 führt eine verlorene Bestätigung dazu, dass der Absender erneut sendet. Der Empfänger erhält die Nachricht noch einmal, was die At-least-once-Garantie erlaubt.

Kann ich mich auf das DUP-Flag verlassen, um Duplikate zu verwerfen?

Nein. DUP kennzeichnet eine erneute Übertragung auf einem Hop, doch die erste Kopie ist womöglich nie angekommen, und der Broker reicht das Flag nicht an Subscriber durch.

Erzeugt QoS 0 Duplikate?

Nein, QoS 0 wird vom Protokoll nie erneut übertragen. Stattdessen können Nachrichten verloren gehen.

Wie lange sollte ich mir Nachrichten-IDs merken?

Lange genug, um Verzögerungen bei Reconnect und Session-Wiederaufnahme abzudecken, typischerweise Minuten. Bemessen Sie es anhand Ihres Session Expiry Interval plus einer Sicherheitsmarge.

Verwandte Artikel

Wer hat MQTT erfunden? Die Geschichte von Ölpipelines bis zum IoT

MQTT wurde 1999 von Andy Stanford-Clark (IBM) und Arlen Nipper (Arcom) erfunden, um Ölpipelines per Satellit zu überwachen. Verfolgen Sie den Weg zu Open Source, OASIS, ISO und den heutigen verwalteten MQTT-Brokern in der Cloud.

8 Min. Lesezeit

Alle Artikel →

Ihr privater MQTT-Broker ist nur wenige Klicks entfernt

Konto anlegen, Broker-Zugangsdaten erhalten und das erste Gerät verbinden — ohne Server, die installiert, gepatcht oder skaliert werden müssen.