Broker testleri

MQTT Last Will Testi: Ani Bağlantı Kopmalarını Simüle Edin

MQTT Last Will'i kill -9 çökmeleri, donmuş süreçler ve ağ kesintileriyle test edin; will'in ne zaman yayınlandığını ölçün, MQTT 5 will delay'i doğrulayın.

Güncellendi · 9 dk okuma

MQTT Last Will Testi: Ani Bağlantı Kopmalarını Simüle Edin

Bir MQTT Last Will'i test etmek için client'ın kötü bir şekilde ölmesini sağlamanız gerekir. Broker will'i yalnızca bir bağlantı düzgün bir DISCONNECT olmadan sona erdiğinde yayınlar — yani uygulamanızı normal şekilde durdurmak hiçbir şey kanıtlamaz. Süreci kill -9 ile öldürün, kill -STOP ile dondurun ya da ağı kesin; ardından will topic'ini ikinci bir client'tan izleyin ve will'in ne zaman geldiğini not edin. İyi bir test olumsuz durumu da içerir: düzgün bir kapanış will'i tetiklememelidir.

Bu rehber size will tanımlı küçük bir cihaz betiği, bir gözlemci, her kod yolunu tetikleyen hata enjeksiyonlarını ve MQTT 5 will delay testlerini veriyor.

Broker will'i ne zaman yayınlar

OlayWill yayınlanır mı?Tipik zamanlama
Süreç çökmesi (kill -9, segfault, OOM kill)EvetNeredeyse anında — işletim sistemi socket'i kapatır
Donmuş süreç, socket hâlâ açıkEvetTrafik olmadan 1,5 × keep-alive sonra
Ağ kesintisi, elektrik kesintisi, kablo çekilmesiEvet1,5 × keep-alive sonra (FIN veya RST gelmez)
Protokol hatası, broker bağlantıyı kapatırEvetAnında
Düzgün DISCONNECT (reason code 0)Hayır—
Reason 0x04 ile MQTT 5 DISCONNECTEvetAnında (artı will delay)
Will Delay Interval tanımlı MQTT 5Evet, client zamanında yeniden bağlanmazsaGecikmenin dolması ya da oturumun bitmesi, hangisi önceyse

Keep-alive satırı, "will'im çok geç geliyor" şikâyetinin neden bu kadar yaygın olduğunu açıklıyor: 60 saniyelik keep-alive ile ölen bir hücresel cihaz yaklaşık 90 saniye sonra bildirilir. Bu bir hata değil, doğru davranıştır — aralığı seçmek için keep alive yazısına bakın. Kavramın kendisi MQTT Last Will yazısında anlatılıyor.

Test edilen cihaz

Bu paho-mqtt 2.x betiği iyi tasarlanmış bir cihaz gibi davranır: retained bir offline will kaydeder, bağlanınca retained online yayınlar ve SIGTERM aldığında düzgünce kapanır (offline'ı kendisi yayınlar ve bağlantıyı temiz şekilde keser). Böylece tek bir süreçte bir temiz ve birkaç kirli çıkış yolu elde edersiniz.

device.py
import os, signal, sys, threading
import paho.mqtt.client as mqtt
from paho.mqtt.packettypes import PacketTypes
from paho.mqtt.properties import Properties

DEV = sys.argv[1] if len(sys.argv) > 1 else "dev1"
WILL_DELAY = int(os.environ.get("WILL_DELAY", "0"))      # seconds, MQTT 5
STATUS = f"testmqtt/lwt-test/{DEV}/status"

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
                     client_id=f"testmqtt-lwt-{DEV}", protocol=mqtt.MQTTv5)
# client.username_pw_set("username", "password")

will_props = Properties(PacketTypes.WILLMESSAGE)
will_props.WillDelayInterval = WILL_DELAY
client.will_set(STATUS, "offline", qos=1, retain=True, properties=will_props)

def on_connect(client, userdata, flags, reason_code, properties):
    print(f"connected: {reason_code}, session present: {flags.session_present}", flush=True)
    client.publish(STATUS, "online", qos=1, retain=True)

client.on_connect = on_connect
stop = threading.Event()
signal.signal(signal.SIGTERM, lambda *_: stop.set())

connect_props = Properties(PacketTypes.CONNECT)
connect_props.SessionExpiryInterval = 300                # must be >= will delay
client.connect("broker.connectmq.com", 1883, keepalive=10,
               clean_start=False, properties=connect_props)
client.loop_start()
print("pid", os.getpid(), flush=True)

while not stop.is_set():
    stop.wait(1)

# graceful shutdown: report offline ourselves, then a normal DISCONNECT (no will)
client.publish(STATUS, "offline", qos=1, retain=True).wait_for_publish(5)
client.disconnect()
client.loop_stop()

İkinci bir terminalde, her mesajla birlikte zaman damgası yazdıran bir gözlemci başlatın:

Observer
mosquitto_sub -h broker.connectmq.com -p 1883 -t 'testmqtt/lwt-test/#' -F '%I %r %t %p'

%I bir ISO-8601 zaman damgası, %r ise retain bayrağıdır: will canlı bir mesaj (0) olarak gelir; önceki bir çalıştırmadan kalan eski bir offline ise abone olur olmaz 1 ile görünür. testmqtt/lwt-test/# topic'ini online MQTT client üzerinden de izleyebilirsiniz; cihazın çalıştığı makinede ağı kestiğinizde bu çok işe yarar.

Senaryo 1: kill -9 ile çökme

Crash the device
python device.py dev1 &          # prints: pid 41237
kill -9 41237

Gözlemci yaklaşık bir saniye içinde offline yazdırır. SIGKILL yakalanamaz, bu yüzden client hiçbir zaman DISCONNECT göndermez; kernel TCP bağlantısını kapatır ve broker bunu ani bir bağlantı kopması olarak değerlendirir. Bellek yetersizliği nedeniyle öldürülme (OOM kill) ya da segfault da aynı şekilde davranır.

Senaryo 2: düzgün kapanış (negatif kontrol)

Stop the device cleanly
python device.py dev1 & PID=$!
sleep 3; kill -TERM "$PID"

Gözlemci tam olarak bir kez offline gösterir — broker tarafından değil, cihazın kendisi tarafından yayınlanmış olarak. Kısa süre sonra ikinci bir offline görürseniz client temiz bir DISCONNECT olmadan çıkmıştır (örneğin süreç, disconnect gönderilmeden sona ermiştir). Bu durumu doğru kurmak çökme durumu kadar önemlidir: her deploy'da tetiklenen bir will, dashboard'ları yanlış alarmlarla doldurur.

Senaryo 3: donma ve ağ kesintisi

Donmuş bir süreç socket'ini açık tutar; bu yüzden onu yalnızca keep-alive tespit edebilir:

Freeze, then resume
python device.py dev1 & PID=$!
sleep 3; kill -STOP "$PID"   # paho's network thread stops: no PINGREQ
# observer: "offline" after ~15 s (1.5 x keep-alive of 10 s)
kill -CONT "$PID"            # the client wakes up, reconnects and publishes "online"

Gerçek bir ağ kesintisi için cihaz makinesinde trafiği düşürün. Gözlemciyi başka bir yerde (ya da güvenli WebSocket portu üzerinden tarayıcıda) çalıştırın, çünkü bu kurallar yerel mosquitto_sub'ınızı da engeller:

Blackhole the broker (Linux, root)
BROKER_IP=$(dig +short broker.connectmq.com | head -1)
sudo iptables -I OUTPUT -p tcp -d "$BROKER_IP" --dport 1883 -j DROP
sudo iptables -I INPUT  -p tcp -s "$BROKER_IP" --sport 1883 -j DROP
# ... wait for the will, then restore:
sudo iptables -D OUTPUT -p tcp -d "$BROKER_IP" --dport 1883 -j DROP
sudo iptables -D INPUT  -p tcp -s "$BROKER_IP" --sport 1883 -j DROP

Container'larda docker pause donmayı, docker network disconnect ise kesintiyi taklit eder. CI'da tekrarlanabilir çalıştırmalar için client ile broker arasına Toxiproxy koyun ve timeout toxic'ini kullanın.

Gözlemci çıktısında son online ile offline arasındaki süreyi ölçün. Bu süre keep-alive değerinizin 1,5 katından çok daha uzunsa, client sandığınızdan farklı bir keep-alive üzerinde anlaşıyordur ya da aradaki bir proxy veya load balancer, TCP bağlantısını broker tarafında canlı tutuyordur.

Senaryo 4: MQTT 5 will delay

Will Delay Interval, bir cihazın kısa bir kesintiyi kendini offline ilan etmeden atlatmasını sağlar. Broker will'i gecikme dolduğunda ya da oturum bittiğinde (hangisi önce olursa) yayınlar; client zamanında yeniden bağlanırsa will'i tamamen atlar. Her iki sonucu da test edin:

Will delay: reconnect in time vs. stay away
# A: crash and come back within the delay -> no will
WILL_DELAY=30 python device.py dev2 & PID=$!
sleep 3; kill -9 "$PID"
sleep 10
WILL_DELAY=30 python device.py dev2 & PID=$!      # "session present: True", no offline

# B: crash and stay away -> will about 30 s after the broker notices the loss
sleep 3; kill -9 "$PID"

A testi hâlâ offline üretiyorsa üç ayarı kontrol edin: yeniden bağlantı aynı client ID'yi kullanmalı, Clean Start kapalı olmalı (clean start eski oturumu bitirir ve will'i anında tetikler) ve session expiry en az will delay kadar olmalı — MQTT 5'in varsayılan değeri olan 0 ile oturum bağlantı kesilince biter ve gecikme yok sayılır. Will delay interval rehberi bu etkileşimi ayrıntılı olarak açıklıyor.

Çökme testini otomatikleştirin

test_lwt.py
import signal, subprocess, threading, time
import paho.mqtt.client as mqtt

TOPIC = "testmqtt/lwt-test/ci/status"

def test_will_is_published_on_crash():
    got = threading.Event()

    def on_message(client, userdata, msg):
        if msg.payload == b"offline" and not msg.retain:   # live will, not stale state
            got.set()

    obs = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
    obs.on_message = on_message
    obs.connect("broker.connectmq.com", 1883)
    obs.subscribe(TOPIC, qos=1)
    obs.loop_start()

    dev = subprocess.Popen(["python", "device.py", "ci"])
    time.sleep(3)                                # connect and publish "online"
    dev.send_signal(signal.SIGKILL)
    t0 = time.monotonic()
    try:
        assert got.wait(10), "Last Will not received within 10 s"
        print(f"will arrived after {time.monotonic() - t0:.1f} s")
    finally:
        obs.publish(TOPIC, b"", qos=1, retain=True).wait_for_publish(5)   # clear status
        obs.disconnect()
        obs.loop_stop()

Düzgün kapanış durumunu, SIGTERM gönderip tam olarak bir offline geldiğini doğrulayan ikinci bir test olarak ekleyin. Çökme testi başarısız olursa Last Will gönderilmiyor yazısındaki adımları izleyin: yanlış will topic'i, will topic'ini reddeden bir ACL ya da will'i sessizce düşüren bir broker. Güvenilir bir varlık tespiti (presence detection) kurmak için will'i, burada gösterildiği gibi retained bir online mesajıyla eşleştirin.

Last Will mesajlarını canlı izleyin

Bir Last Will, ancak onun bir çökmede, bir donmada ve bir ağ kesintisinde tetiklendiğini — ve temiz bir kapanışta sessiz kaldığını — gördüğünüzde test edilmiş sayılır. Zamanlamaları keep-alive ve will delay ayarlarınızın yanına not edin; böylece dashboard'larınızın bir cihazın gittiğini fark etmesinin tam olarak ne kadar sürdüğünü bilirsiniz.

Sıkça sorulan sorular

Client'ı durdurduğumda MQTT Last Will neden gönderilmiyor?

Çünkü client düzgün şekilde durdu. Last Will yalnızca bağlantı normal bir DISCONNECT olmadan sona erdiğinde yayınlanır: çökme, ağ hatası veya keep-alive zaman aşımı. disconnect() çağıran bir uygulamada Ctrl+C'ye basmak temiz bir çıkıştır; broker will'i atar. Test etmek için kill -9 kullanın ya da ağı kesin.

Broker'ın Last Will'i yayınlaması ne kadar sürer?

Client süreci ölürse işletim sistemi socket'i kapatır ve will neredeyse anında yayınlanır. Ağ sessizce kaybolursa broker, trafik olmadan keep-alive aralığının 1,5 katı geçene kadar bekler. MQTT 5'te Will Delay Interval bunun üzerine ek bir gecikme koyar.

Client will delay süresi içinde yeniden bağlandığı hâlde will neden yayınlanıyor?

Çünkü oturum sona erdi. MQTT 5'te will, gecikme dolduğunda ya da oturum bittiğinde (hangisi önce olursa) yayınlanır. Session expiry interval 0 ise ya da client Clean Start açık olarak yeniden bağlanırsa eski oturum biter ve will gönderilir. Aynı client ID ile, Clean Start kapalı ve en az will delay kadar uzun bir session expiry ile yeniden bağlanın.