Mit einem MQTT-Gerätesimulator testest du einen Broker, ein Backend oder ein Dashboard gegen eine Flotte, die es noch nicht gibt. Der Trick: Jedes virtuelle Gerät muss sich wie ein echtes verhalten – eigene Verbindung und eindeutige Client-ID, ein Last Will, das es als offline markiert, Telemetrie in leicht unregelmäßigen Abständen, Befehle, auf die es reagiert, und ab und zu ein Stromausfall.
Dieser Leitfaden baut einen solchen Simulator in rund 120 Zeilen Python mit asyncio und aiomqtt, skaliert ihn auf 1.000 Geräte und zeigt, wie du prüfst, was der Broker sieht.
Was ein realistisches virtuelles Gerät tut
| Verhalten | Warum es beim Testen wichtig ist |
|---|---|
| Eigene Verbindung, eindeutige Client-ID | Testet Sessions pro Client, ACLs und Verbindungslimits. Doppelte IDs werfen sich gegenseitig raus. |
| Gestaffelter Start | Echte Flotten verbinden sich nicht in derselben Millisekunde – außer wenn nach einem Ausfall der Strom zurückkommt. |
Retained-Status online + Last Will offline | Dashboards können Präsenz anzeigen, und du testest, ob der Broker Wills auslöst. |
| Publish-Intervall mit Jitter | Vermeidet perfekt synchrone Bursts, die echte Geräte nie erzeugen. |
| Heterogene Flotte | Manche Geräte melden alle 5 s, andere alle 30 s; die Payloads unterscheiden sich. |
| Befehls-Topic | Testet den Rückkanal: Backend → Broker → Gerät → Acknowledgement. |
| Stromausfälle | Das Gerät verschwindet ohne DISCONNECT, startet neu und verbindet sich mit Backoff wieder. |
Topic-Struktur
Jedes Gerät besitzt einen kleinen Teilbaum. Ein Präfix pro Lauf hält parallele Testläufe auseinander:
| Topic | Richtung | Retained | Inhalt |
|---|---|---|---|
testmqtt/sim/<id>/status | Gerät → Broker | ja | online, oder offline per Last Will |
testmqtt/sim/<id>/telemetry | Gerät → Broker | nein | JSON-Messwert |
testmqtt/sim/<id>/cmd | Backend → Gerät | nein | JSON-Befehl, z. B. {"interval": 2} |
testmqtt/sim/<id>/cmd/ack | Gerät → Backend | nein | Bestätigung |
Client-IDs folgen dem Muster sim-<run>-<index>, zum Beispiel sim-3fa2c1-0042. Sie sind eindeutig, in Broker-Logs lesbar und leicht zu filtern. Der Client-ID-Generator und die Best Practices für Client-IDs erklären, warum diese Eigenschaften wichtig sind.
Der Simulator
Installiere die Abhängigkeit mit pip install aiomqtt (Version 2.x, basiert auf paho-mqtt 2.x). Jedes Gerät ist eine Coroutine mit eigenem aiomqtt.Client:
import asyncio, json, os, random, socket, sys, time, uuid
from collections import Counter
import aiomqtt
HOST, PORT = os.environ.get("MQTT_HOST", "broker.connectmq.com"), 1883
USER, PASS = os.environ.get("MQTT_USER"), os.environ.get("MQTT_PASS")
N_DEVICES = int(os.environ.get("DEVICES", 100))
RAMP_S = 60 # spread device boot over one minute
CRASH_PROB = 0.002 # chance of a power loss after each publish
RUN = uuid.uuid4().hex[:6]
stats = Counter()
class PowerLoss(Exception):
pass
async def commands(client, base, state):
try:
async for msg in client.messages:
try:
cmd = dict(json.loads(msg.payload))
if "interval" in cmd:
state["interval"] = max(1.0, float(cmd["interval"]))
except (ValueError, TypeError):
continue # ignore malformed commands
await client.publish(f"{base}/cmd/ack", json.dumps({"ok": True, **cmd}), qos=1)
stats["commands"] += 1
except aiomqtt.MqttError:
pass # connection lost; device() reconnects
async def device(i: int):
dev_id = f"sim-{RUN}-{i:04d}"
base = f"testmqtt/sim/{dev_id}"
rng = random.Random(i)
state = {"interval": rng.choice([5, 10, 30])}
temp, boots, attempt = rng.uniform(19, 24), 0, 0
await asyncio.sleep(rng.uniform(0, RAMP_S)) # staggered boot
while True:
try:
will = aiomqtt.Will(f"{base}/status", b"offline", qos=1, retain=True)
async with aiomqtt.Client(HOST, PORT, identifier=dev_id, username=USER,
password=PASS, keepalive=30, will=will) as client:
attempt, boots, started = 0, boots + 1, time.monotonic()
stats["online"] += 1
try:
await client.publish(f"{base}/status", b"online", qos=1, retain=True)
await client.subscribe(f"{base}/cmd", qos=1)
listener = asyncio.create_task(commands(client, base, state))
try:
while True:
temp += rng.gauss(0, 0.05)
reading = {"temp": round(temp, 2), "boots": boots,
"uptime": int(time.monotonic() - started), "fw": "1.4.2"}
await client.publish(f"{base}/telemetry", json.dumps(reading), qos=1)
stats["published"] += 1
if rng.random() < CRASH_PROB:
# power loss: drop TCP without DISCONNECT so the broker fires the will
# (aiomqtt has no public API for this, so reach the paho client)
client._client.socket().shutdown(socket.SHUT_RDWR)
await asyncio.sleep(1)
raise PowerLoss
await asyncio.sleep(state["interval"] * rng.uniform(0.9, 1.1))
finally:
listener.cancel()
finally:
stats["online"] -= 1
except PowerLoss:
stats["power_losses"] += 1
await asyncio.sleep(rng.uniform(5, 30)) # time to reboot
except aiomqtt.MqttError:
stats["conn_errors"] += 1
attempt += 1
await asyncio.sleep(min(60, 2 ** attempt) * rng.uniform(0.5, 1.0))
async def report():
while True:
await asyncio.sleep(10)
print(time.strftime("%H:%M:%S"), dict(stats), flush=True)
async def main():
print(f"run {RUN}: starting {N_DEVICES} devices under testmqtt/sim/sim-{RUN}-*")
await asyncio.gather(report(), *(device(i) for i in range(N_DEVICES)))
if __name__ == "__main__":
if sys.platform == "win32":
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
asyncio.run(main())Einige Details machen diesen Simulator vertrauenswürdig:
random.Random(i)pro Gerät – Gerät 42 hat immer dasselbe Intervall, dieselbe Starttemperatur und dasselbe Ausfallmuster; ein Bug, den du einmal siehst, lässt sich also reproduzieren.- Retained-Status mit passendem Last Will – das Gerät publiziert nach dem Verbinden
online; verschwindet es, publiziert der Broker das retained Willoffline. In MQTT Last Will and Testament steht genau, wann Wills ausgelöst werden. - Reconnect mit exponentiellem Backoff und Jitter – ohne das kommen 1.000 Geräte, die den Broker im selben Moment verlieren, auch im selben Moment zurück. Dieses Muster und wie du es vermeidest, beschreibt MQTT-Reconnect-Stürme.
- Windows – dort braucht aiomqtt die Selector-Event-Loop, deshalb die Policy-Zeile ganz unten.
1.000 Geräte betreiben
ulimit -n 4096 # one socket per device, plus headroom
export MQTT_HOST=your-broker-host # your own broker instance for the full fleet
export MQTT_USER=username MQTT_PASS=password
DEVICES=1000 python simulator.pyAlle zehn Sekunden gibt der Simulator eine Zeile wie {'online': 1000, 'published': 48213, 'power_losses': 3, ...} aus. Mit den Standardintervallen (5, 10 oder 30 s pro Gerät) erzeugen 1.000 Geräte größenordnungsmäßig 100 Nachrichten pro Sekunde – realistische Telemetrie, kein Benchmark. Für mehr Druck verkürzt du die Intervalle oder startest weitere Prozesse mit anderem RUN-Präfix; für reinen Verbindungsdruck siehe den MQTT-Verbindungs-Stresstest.
Prüfen, was der Broker sieht
Zähle die Geräte, die der Broker aktuell als online meldet – die Retained-Statusnachrichten werden direkt beim Abonnieren zugestellt:
mosquitto_sub -h broker.connectmq.com -p 1883 -u "$MQTT_USER" -P "$MQTT_PASS" \
-t 'testmqtt/sim/+/status' -v -W 5 | grep -c ' online$'Beobachte das Auslösen der Wills in Echtzeit, indem du denselben Filter im Online-MQTT-Client abonnierst und auf einen Stromausfall wartest: Der Status des Geräts springt auf offline und nach dem simulierten Neustart wieder auf online.
Teste den Rückkanal, indem du einem Gerät einen Befehl schickst und auf die Bestätigung wartest:
mosquitto_sub -h broker.connectmq.com -u "$MQTT_USER" -P "$MQTT_PASS" \
-t 'testmqtt/sim/sim-3fa2c1-0042/cmd/ack' -C 1 -v &
mosquitto_pub -h broker.connectmq.com -u "$MQTT_USER" -P "$MQTT_PASS" \
-t 'testmqtt/sim/sim-3fa2c1-0042/cmd' -q 1 -m '{"interval": 2}'Ersetze 3fa2c1 durch die Run-ID, die der Simulator beim Start ausgegeben hat. Nach der Bestätigung sollte die Telemetrie dieses Geräts alle zwei Sekunden eintreffen.
Den Simulator erweitern
- Realistische Payloads – ersetze den einzelnen Temperaturwert durch das Sensormodell aus dem MQTT-Testdatengenerator, inklusive Tageszyklus, Drift und Binärkodierung.
- TLS – übergib
port=8883undtls_params=aiomqtt.TLSParameters()anaiomqtt.Client, um die Zusatzkosten der TLS-Handshakes beim Start zu messen. - MQTT 5 – ergänze
protocol=aiomqtt.ProtocolVersion.V5und Session-Expiry-Properties, um persistente Sessions und Message Expiry zu testen. - Gerätetypen – starte aus demselben
main()mehrere Coroutine-Funktionen (thermostat,meter,gateway) mit unterschiedlichen Topics und Raten. - Massen-Reconnect – setze
CRASH_PROBauf null und starte den Broker während eines Laufs neu, um zu sehen, wie sich die ganze Flotte erholt.
Simulierte Geräte online gehen sehen
Ein Simulator macht aus „wir glauben, es skaliert" ein Experiment, das du jede Woche wiederholen kannst. Gib jedem virtuellen Gerät eine echte Identität, lass es so ausfallen, wie Hardware ausfällt, und prüfe das Ergebnis am Broker – dann sind die ersten tausend echten Geräte nur die Wiederholung eines Tests, den du schon bestanden hast.
Häufig gestellte Fragen
Wie viele simulierte MQTT-Geräte schafft eine Maschine?
Mit asyncio betreibt ein Python-Prozess problemlos Hunderte bis einige Tausend Geräte bei typischen Telemetrieraten, weil jedes Gerät eine Coroutine und kein Thread ist. Praktische Grenzen sind das Dateideskriptor-Limit (ulimit -n), die Verbindungslimits des Brokers und die CPU für JSON und TLS. Darüber hinaus nutzt du mehrere Prozesse oder Maschinen.
Sollte jedes simulierte Gerät eine eigene MQTT-Verbindung haben?
Ja, wenn du den Broker so testen willst, wie eine echte Flotte ihn nutzt. Eine Verbindung pro Gerät testet Verbindungsverwaltung, Keep-Alive, Sessions, Last Will und ACLs pro Client. Für viele Geräte über eine einzige Verbindung zu publizieren testet nur den Durchsatz.
Wie simuliere ich einen Stromausfall am Gerät?
Schließe die TCP-Verbindung, ohne DISCONNECT zu senden. Der Broker wertet das als unerwarteten Verbindungsabbruch und publiziert das Last Will des Geräts. Ein sauberer Disconnect verwirft das Will dagegen und testet deine Offline-Erkennung deshalb nicht.



