M-Bus Gateway
← Tilbage til blog
· MQTT· Mosquitto· EMQX· clustering· høj tilgængelighed· IoT· broker· TLS· failover

MQTT broker clustering og høj tilgængelighed — Mosquitto og EMQX

MQTT broker HA-design: Mosquitto bridge-konfiguration, EMQX clustering, load balancing, session persistens, failover, TLS og IoT gateway-integration.

Af M-Bus Gateway

En MQTT broker er singlepoint-of-failure i IoT-arkitekturer. Her er design-mønstrene for høj tilgængelighed.


Mosquitto bridge-konfiguration

Mosquitto har ingen native clustering.
HA-løsning: Bridge-mode — to Mosquitto-instanser synkroniserer topics.

Topologi:
  Gateway → Mosquitto Primary (Hetzner)
                  ↕ bridge
             Mosquitto Secondary (Backup VPS)
                  ↕
             Server (subscriber)
# /etc/mosquitto/conf.d/bridge.conf (på Primary)

connection backup-broker
address backup-broker.example.com:8883
bridge_cafile /etc/mosquitto/certs/ca.crt
bridge_certfile /etc/mosquitto/certs/primary.crt
bridge_keyfile /etc/mosquitto/certs/primary.key

topic meters/+/data both 1
topic meters/+/status both 1
topic meters/+/alarm both 1
# cmd og ota bridgedes IKKE — sendes direkte til aktiv broker

cleansession false
restart_timeout 5
bridge_attempt_unsubscribe false

EMQX clustering (anbefalet til produktion)

# docker-compose.yml — EMQX 5.x cluster

services:
  emqx-1:
    image: emqx:5.4
    environment:
      EMQX_NODE__NAME: "emqx@emqx-1.mbus-internal"
      EMQX_CLUSTER__DISCOVERY_STRATEGY: static
      EMQX_CLUSTER__STATIC__SEEDS: "[emqx@emqx-1.mbus-internal,emqx@emqx-2.mbus-internal]"
      EMQX_LISTENERS__SSL__DEFAULT__BIND: "0.0.0.0:8883"
      EMQX_LISTENERS__SSL__DEFAULT__SSL_OPTIONS__CACERTFILE: "/etc/emqx/certs/ca.crt"
      EMQX_LISTENERS__SSL__DEFAULT__SSL_OPTIONS__CERTFILE: "/etc/emqx/certs/server.crt"
      EMQX_LISTENERS__SSL__DEFAULT__SSL_OPTIONS__KEYFILE: "/etc/emqx/certs/server.key"
      EMQX_LISTENERS__SSL__DEFAULT__SSL_OPTIONS__VERIFY: verify_peer
    volumes:
      - emqx-1-data:/opt/emqx/data
      - ./certs:/etc/emqx/certs:ro
    networks:
      internal:
        aliases: [emqx-1.mbus-internal]

  emqx-2:
    image: emqx:5.4
    environment:
      EMQX_NODE__NAME: "emqx@emqx-2.mbus-internal"
      EMQX_CLUSTER__DISCOVERY_STRATEGY: static
      EMQX_CLUSTER__STATIC__SEEDS: "[emqx@emqx-1.mbus-internal,emqx@emqx-2.mbus-internal]"
    volumes:
      - emqx-2-data:/opt/emqx/data
      - ./certs:/etc/emqx/certs:ro
    networks:
      internal:
        aliases: [emqx-2.mbus-internal]

  haproxy:
    image: haproxy:2.9
    ports:
      - "8883:8883"
    volumes:
      - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
    depends_on: [emqx-1, emqx-2]

volumes:
  emqx-1-data:
  emqx-2-data:
# haproxy.cfg — TCP load balancing til EMQX

frontend mqtt_tls
  bind *:8883
  mode tcp
  default_backend emqx_nodes

backend emqx_nodes
  mode tcp
  balance leastconn          # Mindst connections — ikke round-robin
  option tcp-check
  timeout connect 5s
  timeout server 300s        # Lang timeout — MQTT keep-alive
  server emqx-1 emqx-1.mbus-internal:8883 check inter 10s rise 2 fall 3
  server emqx-2 emqx-2.mbus-internal:8883 check inter 10s rise 2 fall 3 backup

Session persistens ved failover

# gateway/src/mqtt/client.py

import paho.mqtt.client as mqtt

def create_ha_client(gateway_id: str, brokers: list[str]) -> mqtt.Client:
    """
    MQTT client med automatisk failover til backup broker.
    clean_session=False: Broker husker subscriptions.
    """
    client = mqtt.Client(
        client_id=f"gw-{gateway_id}",
        clean_session=False,  # Persistent session — data mistes ikke
        protocol=mqtt.MQTTv311,
    )

    client.will_set(
        topic=f"meters/{gateway_id}/status",
        payload='{"online":false}',
        qos=1,
        retain=True,
    )

    # Primary broker forsøges først
    primary = brokers[0]
    client.connect(
        host=primary,
        port=8883,
        keepalive=60,
    )

    return client

def on_disconnect(client, userdata, rc):
    """Reconnect automatisk ved disconnect."""
    if rc != 0:  # Uventet disconnect
        import time
        backoff = 1
        for broker in userdata["brokers"]:
            try:
                client.connect(broker, port=8883, keepalive=60)
                return
            except Exception:
                time.sleep(backoff)
                backoff = min(backoff * 2, 60)

ACL og isolering

# /etc/mosquitto/acl.conf — Per-gateway ACL

# Gateway kan kun publicere til egne topics:
pattern readwrite meters/%u/#

# Server subscriber kan læse alle gateways:
user server-subscriber
topic read meters/+/data
topic read meters/+/status
topic read meters/+/alarm

# Server kan skrive kommandoer til alle gateways:
user server-commander
topic write meters/+/cmd
topic write meters/+/ota

Monitoring: broker-metrik

# EMQX metrics API:
curl http://emqx-1:18083/api/v5/metrics \
  -H "Authorization: Bearer <api-key>" | jq '{
    connections: .connections.count,
    messages_received: ."messages.received",
    messages_sent: ."messages.sent",
    subscriptions: .subscriptions.count
  }'

# Eksempel output:
# {
#   "connections": 127,
#   "messages_received": 48293,
#   "messages_sent": 48291,
#   "subscriptions": 255
# }

# Mosquitto: Abonnér på $SYS topics:
mosquitto_sub -h localhost -t '$SYS/broker/#' -v
# $SYS/broker/clients/connected 127
# $SYS/broker/messages/received 48293
# $SYS/broker/load/messages/received/1min 84.23

Gateway failover-test

# Test failover: Stop primary broker, verificér gateway reconnect

# Stop primary:
docker compose stop emqx-1

# Overvåg gateway log:
sudo journalctl -u mbus-gateway -f | grep -E "connected|disconnect|reconnect"

# Forventet output:
# 2026-05-24 06:00:01 broker_disconnected broker=emqx-1.mbus-internal
# 2026-05-24 06:00:02 reconnecting_to_broker broker=emqx-2.mbus-internal
# 2026-05-24 06:00:03 broker_connected broker=emqx-2.mbus-internal

# Data tab under failover:
# QoS 0: Op til ét telegram kan mistes
# QoS 1: Ingen tab — delivery confirmed inden ACK
# Platform bruger QoS 1 for daglig datapayload

Mosquitto vs EMQX sammenligning

                    Mosquitto 2.x      EMQX 5.x
───────────────────────────────────────────────
Native clustering:  ❌ (bridge-mode)   ✅ (Mnesia cluster)
Max connections:    ~100.000           1.000.000+
Dashboard UI:       ❌                 ✅ (port 18083)
HTTP API:           ❌                 ✅ (REST management)
Rule engine:        ❌                 ✅ (stream processing)
Docker image size:  ~20MB              ~200MB
Memory (idle):      ~10MB              ~200MB
Pris:               Gratis             Gratis (open-source)

M-Bus Gateway valg: Mosquitto
  Årsag: 127 gateways × 1 connection = minimal load.
  Mosquitto er tilstrækkelig og har lavt resource-footprint.
  EMQX anbefales ved 1.000+ gateways eller behov for rule engine.

Konklusion

Mosquitto er tilstrækkelig til under 1.000 gateways og kan HA-konfigureres med bridge-mode. EMQX 5.x giver native clustering og dashboard til større deployments. clean_session=False er afgørende — det sikrer at broker husker subscriptions og queuer beskeder ved kortvarig disconnect. HAProxy med leastconn load-balancer fordeler forbindelser jævnt.

Se MQTT QoS guide eller MQTT retain/LWT guide.