Broker-Tests

MQTT in langsamen, instabilen Mobilfunknetzen testen

2G-, 3G- und LTE-Latenz, Jitter und Paketverlust mit tc netem emulieren und MQTT-Keep-Alive, Payload-Größe, QoS und Sessions für mobiles IoT abstimmen.

Aktualisiert · 9 Min. Lesezeit

MQTT in langsamen, instabilen Mobilfunknetzen testen

Ein MQTT-Client, der im Büro-WLAN grundsolide läuft, kann an einem Mobilfunkmodem auseinanderfallen: Verbindungsaufbau mit Timeout, Keep-Alive-Disconnects alle paar Minuten, QoS-1-Nachrichten, die schubweise ankommen. Die Lösung beginnt damit, Mobilfunkbedingungen am Schreibtisch nachzustellen – Latenz, Jitter, Paketverlust und begrenzte Bandbreite – und dann Keep-Alive, Payload-Größe, QoS und Session-Einstellungen so lange abzustimmen, bis der Client sie übersteht.

Diese Anleitung zeigt, wie du 2G-, 3G- und LTE-Verbindungen mit tc netem emulierst, welche MQTT-Einstellungen du unter welchem Profil testen solltest, und ein Skript, das Zustellung und Latenz misst, damit du Konfigurationen vergleichen kannst.

Was Mobilfunk anders macht

Mobilfunkverbindungen sind nicht einfach „langsames WLAN“. Sie versagen auf bestimmte Arten, die MQTTs langlebige TCP-Verbindung treffen:

  • Hohe und schwankende Round-Trip-Time. Jeder MQTT-Austausch, der auf eine Antwort wartet – CONNACK, SUBACK, PUBACK, PINGRESP – zahlt die volle RTT, und Jitter macht sie unvorhersehbar.
  • Verluste in Bursts. Handover und schwaches Signal verwerfen mehrere Pakete am Stück. TCP sendet sie erneut, also sieht MQTT sekundenlange Stillstände statt verlorener Nachrichten.
  • Aufwachverzögerung des Funkmoduls. Nach einer ruhigen Phase wechselt das Modem in einen Idle-Funkzustand; das erste Paket danach wartet, bis die Funkverbindung wieder steht, und genau diese Nachricht bekommt spürbar mehr Latenz.
  • Carrier-Grade NAT. Mobilfunkbetreiber setzen Geräte hinter NAT-Gateways, die inaktive Flows vergessen. Läuft länger als der NAT-Idle-Timeout nichts über die Verbindung, wird sie halboffen: Das Gerät hält sich für verbunden, der Broker hört nie wieder von ihm. Siehe MQTT und NAT-Timeouts.
  • Volumenbasierte Tarife. Jedes Byte an Headern, Keep-Alive und TLS-Handshake kostet bei Tausenden Geräten Geld und Akku.

Mobilfunk mit tc netem emulieren

Unter Linux fügt tc mit der Queueing-Disziplin netem einer Schnittstelle Verzögerung, Jitter, Verlust und Ratenlimits hinzu. Die folgenden Profile sind grobe Ausgangswerte für typische Bedingungen, keine Messungen eines bestimmten Netzes – ersetze sie durch Werte deiner eigenen Geräte, sobald du welche hast.

ProfilVerzögerung (eine Richtung)JitterVerlustRate
Gutes LTE40 ms10 ms0,1 %10 mbit
Schwaches LTE120 ms40 ms1 %1 mbit
3G200 ms50 ms1 %384 kbit
2G / EDGE400 ms150 ms3 %64 kbit
Funkloch1500 ms500 ms15 %16 kbit

Am saubersten ist es, den MQTT-Client in einem Container laufen zu lassen und nur dessen Netzwerk zu formen, ohne den Host anzufassen:

A shaped test container
docker run --rm -it --cap-add NET_ADMIN --name cell python:3.12-slim bash

# inside the container
apt-get update && apt-get install -y iproute2 mosquitto-clients
pip install paho-mqtt
Apply and change profiles (inside the container)
# 3G-like: 200 ms ± 50 ms, 1 % loss with 25 % correlation (bursty), 384 kbit/s
tc qdisc add dev eth0 root netem delay 200ms 50ms loss 1% 25% rate 384kbit

tc qdisc change dev eth0 root netem delay 400ms 150ms loss 3% 25% rate 64kbit   # 2G
tc qdisc show dev eth0                                                         # verify
tc qdisc del dev eth0 root                                                     # back to normal

netem formt nur ausgehenden Verkehr. Auf Client-Seite reicht das für die meisten Tests: Die Verzögerung wirkt einmal pro Roundtrip, und verlorene ausgehende Segmente lösen dasselbe TCP-Retransmission-Verhalten aus. Willst du den Downlink getrennt formen, setze netem auf die Broker-Seite einer Bridge oder nutze Toxiproxy, das jede Richtung unabhängig verzögern und drosseln kann.

Prüfe das Profil, bevor du MQTT testest: ping -c 10 broker.connectmq.com sollte die zusätzliche Verzögerung zeigen, und ein mosquitto_pub sollte weiterhin durchgehen.

Zustellung pro Profil messen

Dieses Skript (paho-mqtt 2.x) verbindet einen Subscriber und einen Publisher, sendet eine Reihe von QoS-1-Nachrichten und meldet, wie viele angekommen sind, die Verbindungszeit und Perzentile der End-to-End-Latenz. Führe es im geformten Container einmal pro Profil aus.

cell_test.py
import json, sys, threading, time, uuid
import paho.mqtt.client as mqtt

HOST, PORT = "broker.connectmq.com", 1883
TOPIC = f"testmqtt/cellular/{uuid.uuid4().hex[:8]}"
N, QOS, SIZE = int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3])
latencies, done = [], threading.Event()

def on_message(client, userdata, msg):
    latencies.append((time.time() - json.loads(msg.payload)["t"]) * 1000)
    if len(latencies) == N:
        done.set()

def connect(name, keepalive=60):
    ready = threading.Event()
    c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"{name}-{uuid.uuid4().hex[:6]}")
    c.on_connect = lambda *args: ready.set()
    t0 = time.time()
    c.connect(HOST, PORT, keepalive=keepalive)
    c.loop_start()
    if not ready.wait(30):
        sys.exit(f"{name}: no CONNACK within 30 s")
    print(f"{name}: connected in {(time.time() - t0) * 1000:.0f} ms")
    return c

sub = connect("sub")
sub.on_message = on_message
sub.subscribe(TOPIC, qos=QOS)
pub = connect("pub")
time.sleep(2)

pad = "x" * SIZE
for i in range(N):
    pub.publish(TOPIC, json.dumps({"i": i, "t": time.time(), "pad": pad}), qos=QOS)
    time.sleep(0.2)

done.wait(60)
latencies.sort()
pct = lambda q: latencies[min(len(latencies) - 1, int(len(latencies) * q))]
print(f"received {len(latencies)}/{N} at QoS {QOS}, {SIZE}-byte padding")
if latencies:
    print(f"p50 {pct(0.5):.0f} ms  p95 {pct(0.95):.0f} ms  max {latencies[-1]:.0f} ms")
Run it
python cell_test.py 100 1 200    # 100 messages, QoS 1, ~200-byte payloads
python cell_test.py 100 0 200    # same at QoS 0 for comparison
python cell_test.py 20 1 20000   # few large payloads

Publisher und Subscriber teilen sich die Uhr des Containers, die Latenzwerte enthalten also keinen Clock-Skew-Fehler. Beide Verbindungen laufen über die geformte Schnittstelle – das ist pessimistisch, denn in Produktion sitzt der Subscriber meist im Rechenzentrum –, macht Unterschiede zwischen Konfigurationen aber gut sichtbar. Lass den Online-MQTT-Client über eine normale Verbindung testmqtt/cellular/# abonnieren, um die Nachrichten eintröpfeln zu sehen.

Keep-Alive auf den Provider abstimmen, nicht aufs Labor

Im Mobilfunk hat Keep-Alive zwei Aufgaben: den NAT-Eintrag am Leben halten und tote Verbindungen erkennen. Das Keep-Alive-Intervall muss kürzer sein als der NAT-Idle-Timeout des Providers, sonst stirbt die Verbindung zwischen zwei Pings unbemerkt.

Um diesen Timeout zu finden, verbinde dich, bleib komplett inaktiv (Keep-Alive größer als das Testfenster) und publishe nach immer längeren Pausen – 1, 2, 5, 10, 20 Minuten. Die erste Pause, nach der das Publish den Broker nie erreicht (und der Client es erst beim nächsten Ping oder Schreibvorgang merkt), ist dein NAT-Limit. Setze Keep-Alive deutlich darunter.

Teste auch den umgekehrten Fehler: Stelle unter den Profilen 2G und „Funkloch“ sicher, dass Keep-Alive keine Disconnects verursacht. Dauert PINGRESP regelmäßig mehrere Sekunden, kann ein Client mit aggressiven Timeouts gesunde Verbindungen kappen. MQTT erlaubt dem Broker, das Anderthalbfache des Keep-Alive abzuwarten, bevor er aufgibt; sehr kurze Werte (unter 15–20 s) lassen auf langsamen Verbindungen wenig Spielraum.

Payload-Größe und Protokoll-Overhead

Auf einer 64-kbit/s-Leitung belegt eine 20-KB-Nachricht den Uplink für einige Sekunden, und alles dahinter wartet. Vergleiche unter dem 2G-Profil kleine und große Payloads mit dem Skript, und du siehst die p95-Latenz mit der Größe steigen.

  • Payloads kompakt halten. Kurze JSON-Keys oder Binärformate wie CBOR und Protobuf helfen. Der Paketgrößen-Rechner zeigt, wie sich Topic-Länge und Properties pro Nachricht summieren.
  • Mit Bedacht bündeln. Eine Nachricht mit zehn Messwerten hat weniger Overhead als zehn Nachrichten, aber ein Verbindungsabbruch mitten im Batch tut mehr weh. Teste beides.
  • TLS einrechnen. Ein vollständiger TLS-Handshake kostet mehrere Roundtrips und einige Kilobyte. Bei 400 ms Latenz sind das Sekunden pro Verbindungsaufbau, häufige Reconnects werden also teuer. Miss die Verbindungszeit mit TLS auf Port 8883 unter jedem Profil, und lies MQTT-Bandbreiten-Overhead für die Zahlen dahinter.
  • MQTT-5-Topic-Aliases nutzen, wenn lange Topics wiederholt gesendet werden – nachdem du geprüft hast, dass dein Broker sie unterstützt.

QoS und Sessions auf verlustbehafteten Leitungen

EinstellungVerhalten im MobilfunkPassender Test
QoS 0Wenigste Bytes; was beim Abbruch unterwegs ist, geht verlorenLeitung mitten im Batch kappen, Verluste zählen
QoS 1Ein zusätzliches PUBACK pro Nachricht; Wiederholung nach Reconnect, Duplikate möglichGleicher Abbruch, auf Duplikate prüfen
QoS 2Vier Pakete pro Nachricht, jedes zahlt die RTTDurchsatz unter 2G mit QoS 1 vergleichen
Persistente SessionAbos und wartende Nachrichten überleben Reconnects2 Minuten trennen, neu verbinden, Rückstau zählen

Für die meiste Mobilfunk-Telemetrie ist QoS 1 mit idempotenter Verarbeitung beim Empfänger die pragmatische Wahl. Kombiniere sie mit einer persistenten Session – clean_start=False und einem Session Expiry Interval in MQTT 5 –, damit das Gerät nicht neu abonnieren muss und für es gepufferte Nachrichten einen kurzen Ausfall überstehen. Details unter persistente MQTT-Sessions.

Teste Reconnects explizit unter schlechten Profilen: Kappe die Leitung (tc qdisc change dev eth0 root netem loss 100%), warte, stelle sie wieder her und prüfe, dass der Client mit Backoff neu verbindet, statt das Netz zu bombardieren, und dass während des Ausfalls gepublishte QoS-1-Nachrichten ankommen, sobald die Leitung zurück ist.

Checkliste für Mobilfunktests

  1. Zuerst einen Smoke-Test im normalen Netz, dann jedes netem-Profil anwenden.
  2. Pro Profil Verbindungszeit (mit und ohne TLS), Zustellrate und p50/p95-Latenz festhalten.
  3. Den NAT-Idle-Timeout des Providers mit Leerlauf-dann-Publish-Tests finden; Keep-Alive darunter setzen.
  4. Die Leitung 30 s und 5 min kappen; Reconnect-Backoff, Session-Wiederaufnahme und QoS-1-Neuzustellung prüfen.
  5. Payload-Größen und Bündelung unter dem 2G-Profil vergleichen.
  6. Die kritischen Fälle auf echter Hardware mit der Produktions-SIM wiederholen.

Mobilfunknetze werden immer langsamer und unberechenbarer sein als das Labor. Sie bei jeder Änderung zu emulieren macht aus „läuft im Büro“ ein „läuft im fahrenden Lkw“, und die Zahlen aus diesen Läufen geben dir eine belastbare Grundlage für Entscheidungen zu Keep-Alive, QoS und Payloads.

Häufig gestellte Fragen

Welchen Keep-Alive-Wert sollte ich für MQTT über Mobilfunk verwenden?

Kurz genug, um unter dem NAT-Idle-Timeout des Providers zu bleiben, und lang genug, um weder Akku noch Datenvolumen zu verschwenden. Viele Installationen landen zwischen 60 und 300 Sekunden, aber NAT-Timeouts unterscheiden sich je nach Provider. Miss mit deiner SIM, nach welcher Leerlaufzeit die Verbindung still stirbt, und setze Keep-Alive deutlich darunter.

Führt Paketverlust zu verlorenen MQTT-Nachrichten?

Nicht direkt. MQTT läuft über TCP, das verlorene Segmente erneut sendet; Paketverlust zeigt sich daher als Latenzspitzen und Stillstände. Nachrichten gehen verloren, wenn die Verbindung abbricht: QoS-0-Nachrichten in Übertragung sind weg, QoS 1 und 2 werden nach dem Reconnect wiederholt, sofern die Session erhalten bleibt.

Kann ich Mobilfunkbedingungen unter macOS oder Windows emulieren?

Unter macOS wendet Apples Network Link Conditioner Latenz-, Verlust- und Bandbreitenprofile systemweit an. Auf jedem Betriebssystem kannst du den Client in einem Linux-Container mit tc netem ausführen oder Toxiproxy zwischen Client und Broker schalten, um Latenz und Bandbreite zu begrenzen.