Broker testleri

Python ile MQTT Cihaz Simülatörü Geliştirin

Python ve asyncio ile MQTT cihaz simülatörü: benzersiz client ID, Last Will, gerçekçi aralıklar ve elektrik kesintisi senaryolarıyla 1.000 sanal cihaz.

Güncellendi · 10 dk okuma

Python ile MQTT Cihaz Simülatörü Geliştirin

Bir MQTT cihaz simülatörü, bir broker'ı, backend'i veya dashboard'u henüz var olmayan bir filoya karşı test etmenizi sağlar. İşin püf noktası her sanal cihazın gerçek bir cihaz gibi davranmasıdır: kendi bağlantısı ve benzersiz client ID'si, onu çevrimdışı olarak işaretleyen bir Last Will, hafifçe düzensiz aralıklarla telemetri, yanıt verdiği komutlar ve ara sıra bir elektrik kesintisi.

Bu rehber böyle bir simülatörü asyncio ve aiomqtt ile yaklaşık 120 satır Python'da kuruyor, 1.000 cihaza ölçeklendiriyor ve broker'ın ne gördüğünü nasıl doğrulayacağınızı gösteriyor.

Gerçekçi bir sanal cihaz ne yapar

DavranışTest açısından neden önemli
Kendi bağlantısı, benzersiz client IDİstemci bazlı oturumları, ACL'leri ve bağlantı limitlerini çalıştırır. Aynı ID'ye sahip istemciler birbirini düşürür.
Kademeli açılışGerçek filolar aynı milisaniyede bağlanmaz — bir kesintiden sonra elektrik geri geldiğinde hariç.
Retained online durumu + Last Will offlineDashboard'ların varlık bilgisi göstermesini sağlar ve broker'ın will'leri tetiklediğini test eder.
Jitter'lı publish aralığıGerçek cihazların asla üretmediği kusursuz senkronize patlamaları önler.
Heterojen filoBazı cihazlar 5 sn'de, bazıları 30 sn'de bir raporlar; payload'lar farklıdır.
Komut topic'iAşağı yönlü yolu test eder: backend → broker → cihaz → acknowledgement.
Elektrik kesintisi arızalarıCihaz DISCONNECT göndermeden kaybolur, sonra yeniden başlar ve backoff ile tekrar bağlanır.

Topic yapısı

Her cihaz küçük bir alt ağaca sahiptir. Çalıştırma başına bir önek, paralel test çalıştırmalarını birbirinden ayırır:

TopicYönRetainedİçerik
testmqtt/sim/<id>/statuscihaz → brokerevetonline veya Last Will ile offline
testmqtt/sim/<id>/telemetrycihaz → brokerhayırJSON ölçüm
testmqtt/sim/<id>/cmdbackend → cihazhayırJSON komut, örn. {"interval": 2}
testmqtt/sim/<id>/cmd/ackcihaz → backendhayırOnay

Client ID'ler sim-<run>-<index> kalıbını izler, örneğin sim-3fa2c1-0042. Benzersizdirler, broker loglarında okunabilirler ve kolayca filtrelenirler. Client ID üretici ve client ID en iyi uygulamaları bu özelliklerin neden önemli olduğunu açıklıyor.

Simülatör

Bağımlılığı pip install aiomqtt ile kurun (paho-mqtt 2.x üzerine kurulu 2.x sürümü). Her cihaz kendi aiomqtt.Client nesnesine sahip bir coroutine'dir:

simulator.py
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())

Birkaç ayrıntı bu simülatörü güvenilir kılar:

  • Cihaz başına random.Random(i) — 42 numaralı cihaz her zaman aynı aralığa, aynı başlangıç sıcaklığına ve aynı arıza desenine sahiptir; bir kez gördüğünüz bir hata yeniden üretilebilir.
  • Eşleşen Last Will ile retained durum — cihaz bağlandıktan sonra online publish eder; kaybolursa broker retained offline will'ini publish eder. Will'lerin tam olarak ne zaman tetiklendiği için MQTT Last Will and Testament yazısını okuyun.
  • Üstel backoff ve jitter ile yeniden bağlanma — bu olmadan, broker'ı aynı anda kaybeden 1.000 cihaz yine aynı anda geri döner. Bu desen ve nasıl önleneceği MQTT yeniden bağlanma fırtınaları yazısında anlatılıyor.
  • Windows — aiomqtt orada selector tabanlı event loop'a ihtiyaç duyar; en alttaki policy satırı bu yüzden var.

1.000 cihaz çalıştırmak

Filoyu başlatın
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.py

Simülatör her on saniyede bir {'online': 1000, 'published': 48213, 'power_losses': 3, ...} gibi bir satır yazdırır. Varsayılan aralıklarla (cihaz başına 5, 10 veya 30 sn), 1.000 cihaz saniyede 100 mesaj mertebesinde trafik üretir — bir benchmark değil, gerçekçi telemetri. Daha fazla zorlamak için aralıkları kısaltın veya farklı RUN önekleriyle daha fazla süreç başlatın; saf bağlantı baskısı için MQTT bağlantı stres testi yazısına bakın.

Broker'ın ne gördüğünü doğrulayın

Broker'ın şu anda çevrimiçi olarak gösterdiği cihazları sayın — retained durum mesajları subscribe olur olmaz teslim edilir:

Çevrimiçi cihazları sayın
mosquitto_sub -h broker.connectmq.com -p 1883 -u "$MQTT_USER" -P "$MQTT_PASS" \
  -t 'testmqtt/sim/+/status' -v -W 5 | grep -c ' online$'

Aynı filtreye çevrimiçi MQTT istemcisinde subscribe olup bir elektrik kesintisini bekleyerek will'lerin gerçek zamanlı tetiklendiğini izleyin: cihazın durumu offline olur, ardından simüle edilen yeniden başlatmadan sonra tekrar online olur.

Bir cihaza komut gönderip onayı bekleyerek aşağı yönlü yolu test edin:

Komut gönderin
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}'

3fa2c1 değerini simülatörün başlangıçta yazdırdığı çalıştırma ID'siyle değiştirin. Onaydan sonra o cihazın telemetrisi iki saniyede bir gelmelidir.

Simülatörü genişletmek

  • Gerçekçi payload'lar — tek sıcaklık değerini günlük döngü, kayma ve binary kodlama içeren MQTT test verisi üreticisi sensör modeliyle değiştirin.
  • TLS — açılıştaki TLS handshake'lerinin ek maliyetini ölçmek için aiomqtt.Client'a port=8883 ve tls_params=aiomqtt.TLSParameters() verin.
  • MQTT 5 — kalıcı oturumları ve mesaj süresinin dolmasını test etmek için protocol=aiomqtt.ProtocolVersion.V5 ve session expiry özelliklerini ekleyin.
  • Cihaz tipleri — aynı main() içinden farklı topic ve hızlara sahip birkaç coroutine fonksiyonu (thermostat, meter, gateway) çalıştırın.
  • Toplu yeniden bağlanma — CRASH_PROB değerini sıfıra çekin ve bir çalıştırma sırasında broker'ı yeniden başlatarak tüm filonun nasıl toparlandığını görün.

Simüle cihazların çevrimiçi oluşunu izleyin

Bir simülatör "ölçeklendiğini düşünüyoruz" cümlesini her hafta yeniden çalıştırabileceğiniz bir deneye dönüştürür. Her sanal cihaza gerçek bir kimlik verin, onu donanımın arızalandığı gibi arızalandırın ve sonucu broker üzerinde doğrulayın — böylece ilk bin gerçek cihaz, zaten geçtiğiniz bir testin tekrarından ibaret olur.

Sıkça sorulan sorular

Tek bir makine kaç simüle MQTT cihazı çalıştırabilir?

asyncio ile tek bir Python süreci, tipik telemetri hızlarında yüzlerce ila birkaç bin cihazı rahatça çalıştırır, çünkü her cihaz bir thread değil bir coroutine'dir. Pratik sınırlar dosya tanımlayıcı limiti (ulimit -n), broker'ın bağlantı limitleri ve JSON ile TLS'e harcanan CPU'dur. Bunun ötesinde birden fazla süreç veya makine kullanın.

Her simüle cihazın kendi MQTT bağlantısı olmalı mı?

Evet, broker'ı gerçek bir filonun kullandığı şekilde test etmek istiyorsanız. Cihaz başına bir bağlantı; bağlantı yönetimini, keep-alive'ı, oturumları, Last Will'i ve istemci bazlı ACL'leri çalıştırır. Birçok cihaz adına tek bir bağlantı üzerinden publish etmek yalnızca throughput'u test eder.

Bir cihazın elektriğinin kesilmesini nasıl simüle ederim?

TCP bağlantısını DISCONNECT göndermeden kapatın. Broker bunu beklenmedik bir kopma olarak değerlendirir ve cihazın Last Will mesajını publish eder. Temiz bir disconnect ise will'i iptal eder, dolayısıyla çevrimdışı tespitinizi test etmez.