Back to blog

MQTT Deep Dive: Giao Thức Messaging Cho IoT

iotmqttprotocolsbackendtutorial
MQTT Deep Dive: Giao Thức Messaging Cho IoT

Chào mừng bạn đến với IOT-5 trong Lộ Trình IoT Patterns & Strategies! Trong Phase 2, chúng ta đã nói về MQTT cơ bản — mô hình pub/sub, cài đặt Mosquitto, QoS levels, và so sánh v3.1.1 vs v5.0. Giờ là lúc lên production-grade.

Chạy MQTT cho project hobby với 10 thiết bị thì đơn giản. Chạy cho 100.000+ thiết bị với guaranteed delivery, authentication, encryption, load balancing, và zero downtime — đó mới là lúc mọi thứ thú vị.

Bài viết này cover mọi thứ bạn cần để design, bảo mật, và scale MQTT cho hệ thống IoT production thực tế.

Bạn Sẽ Học Được Gì

✅ Thiết kế MQTT topic hierarchies scale được với fleet thiết bị của bạn
✅ Chọn đúng QoS level cho mỗi loại dữ liệu với phân tích trade-off thực tế
✅ Cấu hình clean vs persistent sessions cho các hành vi thiết bị khác nhau
✅ Bảo mật MQTT với TLS, mTLS, username/password, và JWT authentication
✅ Triển khai fine-grained authorization với ACLs
✅ Thiết lập broker clustering với EMQX cho high availability
✅ Scale MQTT xử lý hàng triệu concurrent connections
✅ Monitor broker health và phát hiện vấn đề trước khi chúng leo thang


Topic Design Patterns

Topic hierarchy là xương sống của hệ thống MQTT. Topic design tệ dẫn đến wildcard explosions, không thể filter, và migrations đau đớn. Topic design tốt scale đến hàng triệu thiết bị.

Best Practices Cho Topic Hierarchy

{org}/{site}/{zone}/{device-type}/{device-id}/{data-type}

Ví dụ:

acme/factory-berlin/zone-a/temperature-sensor/ts-0042/telemetry
acme/factory-berlin/zone-a/temperature-sensor/ts-0042/status
acme/factory-berlin/zone-a/temperature-sensor/ts-0042/alerts
acme/factory-berlin/zone-b/motor/motor-007/telemetry

Quy tắc:

  1. Dùng lowercase với hyphens — temperature-sensor, không phải TemperatureSensor hay temperature_sensor
  2. Cụ thể từ trái sang phải — category rộng nhất trước, cụ thể nhất cuối
  3. Không bao giờ bắt đầu với / — leading slash tạo level trống đầu tiên (/topic có 2 levels, topic có 1)
  4. Giữ mỗi level có ý nghĩa — mỗi level nên phục vụ mục đích filtering
  5. Tách biệt data types — telemetry, commands, status, và alerts nên ở topics khác nhau
  6. Bao gồm device ID — cho phép per-device subscriptions và ACLs

Wildcard Subscriptions

MQTT cung cấp hai wildcards. Hiểu chúng rất quan trọng để design topics hoạt động ở scale:

WildcardKý HiệuKhớpVí Dụ
Single-level+Chính xác một levelfactory/+/temperature khớp factory/zone-a/temperature nhưng KHÔNG khớp factory/zone-a/rack-1/temperature
Multi-level#Không hoặc nhiều levelsfactory/# khớp factory/zone-a/temperature VÀ factory/zone-a/rack-1/motor/vibration

Topic Design Patterns Cho Các Tình Huống Phổ Biến

Pattern 1: Telemetry + Commands (Hai chiều)

# Device publishes telemetry (device → cloud)
devices/{device-id}/telemetry
 
# Cloud publishes commands (cloud → device)
devices/{device-id}/commands
 
# Device publishes command responses
devices/{device-id}/commands/response

Pattern 2: Request-Response qua MQTT

MQTT vốn là pub/sub, nhưng bạn có thể triển khai request-response:

# Requester publishes với correlation ID
requests/{target-device}/rpc
  payload: { "id": "req-123", "method": "getConfig", "replyTo": "responses/requester-1" }
 
# Target publishes response đến replyTo topic
responses/requester-1
  payload: { "id": "req-123", "result": { "interval": 60, "threshold": 35 } }

MQTT v5.0 có hỗ trợ native cho pattern này với Response Topic và Correlation Data properties.

Pattern 3: Shared Subscriptions (MQTT v5.0)

Shared subscriptions phân phối messages giữa nhiều subscribers — load balancing native:

# Standard subscription — tất cả subscribers nhận mọi message
devices/+/telemetry
 
# Shared subscription — messages phân phối round-robin
$share/processing-group/devices/+/telemetry

Trước v5.0, bạn phải tự triển khai với application-level load balancing hoặc dùng broker-specific extensions (EMQX và HiveMQ đã hỗ trợ shared subscriptions trước khi spec ra).

Anti-Patterns Cần Tránh

❌ devices/telemetry          → Không có device ID — không filter được theo thiết bị
❌ ts-0042                     → Không có hierarchy — không thể dùng wildcards
❌ /devices/ts-0042/data      → Leading slash tạo level trống
❌ devices/TS_0042/Data       → Casing không nhất quán
❌ devices/ts-0042            → Một topic cho mọi thứ — không tách được telemetry khỏi commands

QoS Levels Chuyên Sâu

IOT-3 đã giới thiệu QoS 0, 1, và 2. Giờ hãy đào sâu vào trade-offs thực tế và khi nào mỗi level thực sự gặp vấn đề.

QoS 0: At Most Once (Gửi Và Quên)

Hành vi:

  • Client gửi PUBLISH, đi tiếp ngay
  • Không PUBACK, không retry
  • Message có thể mất nếu mạng chập chờn

Khi nào dùng:

  • Telemetry sensor tần suất cao (nhiệt độ mỗi giây)
  • Dữ liệu mà mất một reading không quan trọng (reading tiếp theo đến sau 1 giây)
  • Link hạn chế bandwidth mà overhead quan trọng

Khi nào gặp vấn đề: Nếu mạng của bạn thường xuyên rơi packets và bạn dùng QoS 0 cho critical alerts, bạn sẽ bỏ lỡ chúng một cách im lặng.

QoS 1: At Least Once

Hành vi:

  • Client chờ PUBACK
  • Retry nếu không có acknowledgment trong timeout
  • Broker lưu message cho đến khi xác nhận delivery
  • Có thể trùng lặp — subscriber có thể nhận cùng message hai lần

Khi nào dùng:

  • Sensor readings cần chính xác (đồng hồ điện, billing)
  • Thay đổi trạng thái (cửa mở/đóng, device online/offline)
  • Bất kỳ dữ liệu nào không thể mất nhưng xử lý được duplicates

Xử lý duplicates trong ứng dụng:

class IdempotentProcessor {
  private processed = new Set<string>();
  private maxSetSize = 100_000;
 
  process(message: { messageId: string; payload: any }): boolean {
    // Bỏ qua nếu đã xử lý
    if (this.processed.has(message.messageId)) {
      console.log(`Duplicate bỏ qua: ${message.messageId}`);
      return false;
    }
 
    // Loại bỏ entries cũ nhất nếu set quá lớn
    if (this.processed.size >= this.maxSetSize) {
      const first = this.processed.values().next().value;
      this.processed.delete(first);
    }
 
    this.processed.add(message.messageId);
    return true; // Xử lý message này
  }
}

QoS 2: Exactly Once

Hành vi:

  • Handshake bốn gói: PUBLISH → PUBREC → PUBREL → PUBCOMP
  • Đảm bảo message được deliver đúng một lần
  • Cả client và broker duy trì state cho packet ID

Khi nào dùng:

  • Giao dịch tài chính (billing events)
  • Commands quan trọng (shutdown, van mở/đóng)
  • Audit logs mà duplicates làm hỏng dữ liệu compliance

Chi phí:

  • 4x số packets so với QoS 0
  • Latency cao hơn (phải hoàn thành full handshake)
  • Nhiều broker memory hơn (lưu in-flight state)
  • Throughput thấp hơn — broker xử lý ít messages hơn mỗi giây

Ma Trận Chọn QoS

Loại Dữ LiệuQoSTại Sao
Nhiệt độ mỗi 1s0Tần suất cao, reading tiếp thay thế cái mất
Đồng hồ điện (billing)1Không thể mất, idempotent theo timestamp
Trạng thái thiết bị (online/offline)1 + retainedPhải persist cho late subscribers
Lệnh emergency shutdown2Phải thực thi đúng một lần
Trigger cập nhật firmware2Duplicate trigger có thể làm hỏng update
Heartbeat / keepalive0Mất một cái không sao, vắng mặt trigger LWT
Thay đổi config từ cloud1Retry chấp nhận được, device validate config

QoS Downgrade

Quan trọng: QoS là theo subscription, không phải theo message. Nếu publisher gửi QoS 2 nhưng subscriber subscribe QoS 1, broker hạ delivery xuống QoS 1 cho subscriber đó.

Publisher → PUBLISH QoS 2 → Broker → Subscriber (subscribed QoS 1) → delivers ở QoS 1

Nghĩa là effort QoS 2 của publisher bị lãng phí cho subscribers không cần. Design hệ thống sao cho publishers và subscribers đồng ý về QoS tối thiểu cần thiết.


Session Management

Sessions quyết định chuyện gì xảy ra khi thiết bị ngắt kết nối và kết nối lại. Làm đúng điều này rất quan trọng cho thiết bị sleep (chạy pin) hoặc kết nối không ổn định.

Clean Session vs Persistent Session

Khía CạnhClean Session (clean=true)Persistent Session (clean=false)
SubscriptionsMất khi ngắt kết nốiBroker nhớ
Queued messagesBị bỏLưu đến khi kết nối lại
In-flight QoS 1/2MấtTiếp tục khi kết nối lại
Client IDCó thể trống (v3.1.1)Phải unique và ổn định
Broker memoryThấpTăng theo mỗi thiết bị ngắt kết nối
Phù hợp nhấtClients tạm thời, dashboardsSensors chạy pin, thiết bị di động

Cải Tiến Session Trong MQTT v5.0

MQTT v5.0 thay boolean cleanSession bằng hai fields cho kiểm soát chi tiết hơn:

import mqtt from 'mqtt';
 
// MQTT v5.0 — kiểm soát session chi tiết
const client = mqtt.connect('mqtt://broker:1883', {
  protocolVersion: 5,
  clientId: 'sensor-ts-0042',
 
  // Clean Start: true = bỏ session hiện tại, false = resume
  clean: false,
 
  properties: {
    // Session Expiry Interval: broker giữ session bao lâu sau khi ngắt kết nối
    // 0 = xóa ngay (tương đương clean=true trong v3.1.1)
    // 4294967295 (0xFFFFFFFF) = không bao giờ hết hạn
    sessionExpiryInterval: 3600, // Giữ session 1 giờ sau khi ngắt kết nối
  },
});
 
client.on('connect', (connack) => {
  if (connack.sessionPresent) {
    console.log('Đã resume session hiện tại — subscriptions còn nguyên');
    // Không cần re-subscribe — broker nhớ
  } else {
    console.log('Session mới — subscribing topics');
    client.subscribe('devices/ts-0042/commands', { qos: 1 });
  }
});

Tại sao quan trọng: Trong v3.1.1, persistent sessions tồn tại mãi (hoặc đến khi broker restart). Nếu bạn có 100.000 thiết bị kết nối một lần rồi không bao giờ quay lại, broker tích lũy 100.000 dead sessions chiếm memory. sessionExpiryInterval của v5.0 cho sessions tự hết hạn.

Message Expiry (MQTT v5.0)

Queued messages cho offline devices có thể trở nên cũ. MQTT v5.0 cho publishers đặt expiry:

client.publish('devices/ts-0042/commands', JSON.stringify({
  action: 'recalibrate',
  timestamp: Date.now(),
}), {
  qos: 1,
  properties: {
    messageExpiryInterval: 300, // Hết hạn sau 5 phút
  },
});
 
// Nếu device kết nối lại sau 10 phút, command này đã biến mất
// — recalibrate với parameters cũ cũng chẳng có ý nghĩa

Bảo Mật MQTT

Một MQTT broker không bảo mật là cánh cửa mở toang vào toàn bộ fleet IoT. Bất kỳ ai cũng có thể subscribe # và xem toàn bộ dữ liệu, hoặc publish commands giả đến actuators.

Layer 1: Transport Security (TLS)

Mã hóa tất cả traffic giữa clients và broker:

Cấu hình TLS Mosquitto:

# /etc/mosquitto/conf.d/tls.conf
 
# Tắt plaintext listener
# listener 1883
 
# TLS listener
listener 8883
cafile /etc/mosquitto/certs/ca.crt
certfile /etc/mosquitto/certs/server.crt
keyfile /etc/mosquitto/certs/server.key
tls_version tlsv1.3
 
# Yêu cầu client certificates (mTLS)
require_certificate true
use_identity_as_username true

Layer 2: Authentication

Option A: Username/Password

Đơn giản nhưng hạn chế — passwords có thể bị lộ, khó rotate ở scale.

# /etc/mosquitto/conf.d/auth.conf
allow_anonymous false
password_file /etc/mosquitto/passwd
# Tạo password file
mosquitto_passwd -c /etc/mosquitto/passwd sensor-ts-0042
# Nhập password khi được hỏi

Option B: Client Certificates (mTLS)

Mỗi thiết bị có certificate riêng — authentication mạnh nhất cho IoT:

# Tạo CA (Certificate Authority)
openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
  -out ca.crt -subj "/CN=IoT CA"
 
# Tạo device certificate
openssl genrsa -out device-ts-0042.key 2048
openssl req -new -key device-ts-0042.key \
  -out device-ts-0042.csr -subj "/CN=sensor-ts-0042"
openssl x509 -req -in device-ts-0042.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out device-ts-0042.crt -days 365 -sha256

Option C: JWT Token Authentication

Cho thiết bị authenticate qua external identity provider:

import mqtt from 'mqtt';
import jwt from 'jsonwebtoken';
 
// Tạo JWT ngắn hạn cho device authentication
function getDeviceToken(deviceId: string): string {
  return jwt.sign(
    {
      sub: deviceId,
      permissions: ['publish:devices/+/telemetry', 'subscribe:devices/+/commands'],
      iat: Math.floor(Date.now() / 1000),
    },
    process.env.JWT_SECRET!,
    { expiresIn: '1h' }
  );
}
 
// Device kết nối với JWT làm password
const client = mqtt.connect('mqtts://broker:8883', {
  clientId: 'sensor-ts-0042',
  username: 'sensor-ts-0042',
  password: getDeviceToken('sensor-ts-0042'),
});

Cần broker plugin để validate JWTs. EMQX và HiveMQ đều hỗ trợ JWT auth out of the box.

Layer 3: Authorization (ACLs)

Authentication cho biết client là ai. Authorization kiểm soát họ được làm gì.

# /etc/mosquitto/conf.d/acl.conf
acl_file /etc/mosquitto/acl
 
# /etc/mosquitto/acl — Access Control List
# Sensors chỉ có thể publish đến topics của mình
user sensor-ts-0042
topic write devices/sensor-ts-0042/telemetry
topic write devices/sensor-ts-0042/status
topic read devices/sensor-ts-0042/commands
 
# Dashboard users có thể đọc mọi thứ nhưng không ghi
user dashboard-user
topic read devices/#
 
# Admin có thể làm mọi thứ
user admin
topic readwrite #
 
# ACL dựa trên pattern (dynamic theo client)
pattern write devices/%c/telemetry
pattern read devices/%c/commands

EMQX ACL với built-in database:

# EMQX hỗ trợ ACL rules qua API
curl -X POST http://localhost:18083/api/v5/authorization/sources/built_in_database/rules/users \
  -H "Content-Type: application/json" \
  -d '{
    "username": "sensor-ts-0042",
    "rules": [
      {"action": "publish", "topic": "devices/sensor-ts-0042/telemetry", "permission": "allow"},
      {"action": "publish", "topic": "devices/sensor-ts-0042/status", "permission": "allow"},
      {"action": "subscribe", "topic": "devices/sensor-ts-0042/commands", "permission": "allow"},
      {"action": "all", "topic": "#", "permission": "deny"}
    ]
  }'

Security Checklist

LayerTối ThiểuKhuyên DùngProduction
TransportTLS 1.2TLS 1.3TLS 1.3 + certificate pinning
AuthenticationUsername/passwordClient certificates (mTLS)mTLS + short-lived tokens
AuthorizationBasic ACL filePattern-based ACLsDynamic ACLs qua database/API
PayloadKhôngKhông (TLS mã hóa transport)Application-layer encryption cho dữ liệu nhạy cảm
MonitoringBroker logsFailed auth alertsReal-time anomaly detection

Broker Clustering

Một MQTT broker đơn là single point of failure. Cho production, bạn cần clustering — nhiều broker nodes chia sẻ state và phân phối tải.

Tại Sao Cần Cluster?

MetricSingle BrokerClustered (3 nodes)
Concurrent connections~100K (tùy hardware)~300K+
AvailabilityMột node fail = sập toàn bộChịu được 1 node fail
ThroughputGiới hạn bởi single CPUHorizontal scaling
Bảo trìDowntime khi updateRolling upgrades

Thiết Lập EMQX Cluster

EMQX là MQTT broker open-source phổ biến nhất cho production clustering. Hỗ trợ automatic discovery, session transfer, và horizontal scaling.

Docker Compose cho EMQX cluster:

# docker-compose.emqx-cluster.yml
version: "3.8"
 
services:
  emqx1:
    image: emqx/emqx:5.5
    container_name: emqx1
    environment:
      - EMQX_NODE_NAME=emqx@emqx1
      - EMQX_CLUSTER__DISCOVERY_STRATEGY=static
      - EMQX_CLUSTER__STATIC__SEEDS=[emqx@emqx1,emqx@emqx2,emqx@emqx3]
    ports:
      - "1883:1883"    # MQTT
      - "8083:8083"    # WebSocket
      - "8883:8883"    # MQTT/TLS
      - "18083:18083"  # Dashboard
    networks:
      - emqx-net
 
  emqx2:
    image: emqx/emqx:5.5
    container_name: emqx2
    environment:
      - EMQX_NODE_NAME=emqx@emqx2
      - EMQX_CLUSTER__DISCOVERY_STRATEGY=static
      - EMQX_CLUSTER__STATIC__SEEDS=[emqx@emqx1,emqx@emqx2,emqx@emqx3]
    ports:
      - "1884:1883"
    networks:
      - emqx-net
 
  emqx3:
    image: emqx/emqx:5.5
    container_name: emqx3
    environment:
      - EMQX_NODE_NAME=emqx@emqx3
      - EMQX_CLUSTER__DISCOVERY_STRATEGY=static
      - EMQX_CLUSTER__STATIC__SEEDS=[emqx@emqx1,emqx@emqx2,emqx@emqx3]
    ports:
      - "1885:1883"
    networks:
      - emqx-net
 
  haproxy:
    image: haproxy:2.8
    container_name: mqtt-lb
    ports:
      - "1880:1883"
    volumes:
      - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
    depends_on:
      - emqx1
      - emqx2
      - emqx3
    networks:
      - emqx-net
 
networks:
  emqx-net:
    driver: bridge

Cấu hình HAProxy cho MQTT load balancing:

# haproxy.cfg
global
    log stdout format raw local0
 
defaults
    mode tcp
    timeout connect 5s
    timeout client 120s
    timeout server 120s
 
frontend mqtt_frontend
    bind *:1883
    default_backend mqtt_backend
 
backend mqtt_backend
    balance leastconn
    # Sticky sessions theo client IP — quan trọng cho MQTT persistent connections
    stick-table type ip size 200k expire 30m
    stick on src
 
    server emqx1 emqx1:1883 check inter 5s fall 3 rise 2
    server emqx2 emqx2:1883 check inter 5s fall 3 rise 2
    server emqx3 emqx3:1883 check inter 5s fall 3 rise 2

Khởi động cluster:

docker compose -f docker-compose.emqx-cluster.yml up -d
 
# Kiểm tra trạng thái cluster
docker exec emqx1 emqx ctl cluster status
# Cluster status: running
# Node: emqx@emqx1 (running)
# Node: emqx@emqx2 (running)
# Node: emqx@emqx3 (running)

Cách Cluster Routing Hoạt Động

Khi thiết bị publish đến Node 1, nhưng subscribers kết nối ở Node 2 và Node 3:

Mỗi node duy trì một global route table — mapping từ topic subscriptions đến nodes. Khi message đến, node nhận forward đến tất cả nodes có subscribers khớp. Đây là cách MQTT clustering đạt horizontal scaling mà clients không cần biết node nào giữ dữ liệu.

HiveMQ Alternative

HiveMQ là lựa chọn enterprise — commercial với free community edition:

# docker-compose.hivemq-cluster.yml
version: "3.8"
services:
  hivemq1:
    image: hivemq/hivemq-ce:latest
    container_name: hivemq1
    environment:
      - HIVEMQ_CLUSTER_TRANSPORT_TYPE=TCP
    ports:
      - "1883:1883"
      - "8080:8080"
    networks:
      - hivemq-net
 
  hivemq2:
    image: hivemq/hivemq-ce:latest
    container_name: hivemq2
    environment:
      - HIVEMQ_CLUSTER_TRANSPORT_TYPE=TCP
    ports:
      - "1884:1883"
    networks:
      - hivemq-net
 
networks:
  hivemq-net:
    driver: bridge

So Sánh Brokers

Tính NăngEMQXHiveMQ CEHiveMQ EnterpriseMosquitto
ClusteringCó (built-in)Có (hạn chế)Có (đầy đủ)Không
Max connections100M+ (đã test)~100KHàng triệu~100K
Shared subscriptionsCóCóCóKhông (v2.0)
Rule engineCó (SQL-like)KhôngCóKhông
WebSocketCóCóCóCó
DashboardCó (web UI)KhôngCóKhông
LicenseApache 2.0Apache 2.0CommercialEPL/EDL
Phù hợp nhấtProduction IoTDev/prod nhỏEnterpriseDev/prototyping

Scaling MQTT

Connection Scaling

Mỗi kết nối MQTT tiêu thụ broker memory cho:

  • TCP socket buffer (~4 KB)
  • Session state (~1-2 KB cho clean sessions)
  • Subscription routing entries (~100 bytes mỗi subscription)
  • In-flight message buffers (QoS 1/2)

Quy tắc ngón tay cái: 1 triệu kết nối ≈ 8-16 GB RAM (tùy QoS và message rates).

Chiến Lược Scaling

Topic Aliasing (MQTT v5.0)

Topic strings dài được gửi với mọi PUBLISH. Topic aliases thay thế bằng một integer ngắn sau lần dùng đầu:

// Không có topic alias — gửi full topic string mỗi lần
client.publish('acme/factory-berlin/zone-a/temperature-sensor/ts-0042/telemetry', data);
// Overhead: 67 bytes topic mỗi message
 
// Có topic alias — gửi alias number sau message đầu tiên
const client = mqtt.connect('mqtts://broker:8883', {
  protocolVersion: 5,
  properties: {
    topicAliasMaximum: 100, // Hỗ trợ tới 100 aliases
  },
});
 
// PUBLISH đầu gửi full topic + gán alias
// Các PUBLISH sau chỉ gửi alias (2 bytes thay vì 67)

Với 10.000 messages/giây, topic aliasing tiết kiệm ~650 KB/giây bandwidth.

Connection Pooling Cho Backend Services

Backend services xử lý dữ liệu MQTT không nên tạo một connection mỗi task:

import mqtt from 'mqtt';
 
class MqttConnectionPool {
  private connections: mqtt.MqttClient[] = [];
  private roundRobinIndex = 0;
 
  constructor(brokerUrl: string, poolSize: number = 5) {
    for (let i = 0; i < poolSize; i++) {
      const client = mqtt.connect(brokerUrl, {
        clientId: `backend-worker-${i}`,
        clean: true,
      });
      this.connections.push(client);
    }
  }
 
  // Round-robin publish giữa các connections
  publish(topic: string, payload: string, opts?: mqtt.IClientPublishOptions): void {
    const client = this.connections[this.roundRobinIndex];
    client.publish(topic, payload, opts || {});
    this.roundRobinIndex = (this.roundRobinIndex + 1) % this.connections.length;
  }
 
  // Tất cả connections subscribe (cho shared subscriptions)
  subscribeAll(topic: string, opts?: mqtt.IClientSubscribeOptions): void {
    for (const client of this.connections) {
      client.subscribe(topic, opts || {});
    }
  }
}
 
// 5 connections xử lý publishing trên nhiều topics
const pool = new MqttConnectionPool('mqtts://broker:8883', 5);
 
pool.publish('factory/zone-a/aggregated', JSON.stringify(data));

Monitor Broker Health

Bạn không thể sửa thứ bạn không thấy. MQTT brokers expose metrics cho biết khi nào rắc rối đang đến — trước khi thiết bị bắt đầu disconnect.

Các Metrics Chính Cần Monitor

MetricNgưỡng WarningNgưỡng CriticalÝ Nghĩa
Connected clients> 80% capacity> 95% capacitySắp hết connection slots
Message rate (in/out)Spike bất thường2x bình thườngCó thể message storm hoặc loop
Subscription count> 1M mỗi node> 5M mỗi nodeÁp lực memory
Retained messages> 100K> 500KStorage tăng không kiểm soát
Inflight messages> 10K> 50KQoS handshakes bị tắc
Memory usage> 70%> 90%Gần OOM
Packet drop rate> 0.1%> 1%Broker quá tải

Tích Hợp EMQX Prometheus

EMQX expose Prometheus metrics out of the box:

# prometheus.yml
scrape_configs:
  - job_name: 'emqx'
    metrics_path: '/api/v5/prometheus/stats'
    static_configs:
      - targets: ['emqx1:18083', 'emqx2:18083', 'emqx3:18083']
    # Authentication cho EMQX dashboard API
    basic_auth:
      username: admin
      password: public

Ví dụ Grafana dashboard query:

# Connected clients theo thời gian
emqx_connections_count
 
# Message throughput (messages mỗi giây)
rate(emqx_messages_received_total[5m])
 
# QoS 1/2 inflight messages
emqx_messages_qos1_inflight + emqx_messages_qos2_inflight
 
# Subscription count
emqx_suboptions_count

$SYS Topics (Mosquitto)

Mosquitto expose broker stats qua các $SYS topics đặc biệt:

const monitor = mqtt.connect('mqtt://broker:1883');
 
monitor.subscribe('$SYS/#');
 
monitor.on('message', (topic, message) => {
  const value = message.toString();
 
  switch (topic) {
    case '$SYS/broker/clients/connected':
      console.log(`Connected clients: ${value}`);
      break;
    case '$SYS/broker/messages/received':
      console.log(`Tổng messages nhận: ${value}`);
      break;
    case '$SYS/broker/load/messages/received/1min':
      console.log(`Messages/phút: ${value}`);
      break;
    case '$SYS/broker/heap/current':
      console.log(`Heap memory: ${(parseInt(value) / 1024 / 1024).toFixed(1)} MB`);
      break;
  }
});

Health Check Endpoint

Xây dựng health check đơn giản validate broker connectivity:

import mqtt from 'mqtt';
 
async function checkBrokerHealth(brokerUrl: string): Promise<{
  status: 'healthy' | 'degraded' | 'down';
  latencyMs: number;
  details: string;
}> {
  return new Promise((resolve) => {
    const start = Date.now();
    const testTopic = `health-check/${Date.now()}`;
    const timeout = setTimeout(() => {
      resolve({ status: 'down', latencyMs: -1, details: 'Connection timeout' });
    }, 5000);
 
    const client = mqtt.connect(brokerUrl, {
      clientId: `health-check-${Date.now()}`,
      connectTimeout: 5000,
    });
 
    client.on('connect', () => {
      client.subscribe(testTopic, { qos: 1 });
 
      client.on('message', () => {
        const latency = Date.now() - start;
        clearTimeout(timeout);
        client.end();
 
        resolve({
          status: latency < 100 ? 'healthy' : 'degraded',
          latencyMs: latency,
          details: `Round-trip: ${latency}ms`,
        });
      });
 
      // Publish và expect nhận lại message của mình
      client.publish(testTopic, 'ping', { qos: 1 });
    });
 
    client.on('error', (err) => {
      clearTimeout(timeout);
      client.end();
      resolve({ status: 'down', latencyMs: -1, details: err.message });
    });
  });
}
 
// Sử dụng
const health = await checkBrokerHealth('mqtt://broker:1883');
console.log(`Broker: ${health.status} (${health.latencyMs}ms)`);

MQTT Với TypeScript và Python

TypeScript/Node.js (mqtt.js)

Production client đầy đủ với reconnection, error handling, và graceful shutdown:

import mqtt, { MqttClient, IClientOptions } from 'mqtt';
 
interface DeviceConfig {
  deviceId: string;
  brokerUrl: string;
  username?: string;
  password?: string;
  caCert?: Buffer;
  clientCert?: Buffer;
  clientKey?: Buffer;
}
 
class MqttDeviceClient {
  private client: MqttClient;
  private deviceId: string;
 
  constructor(config: DeviceConfig) {
    this.deviceId = config.deviceId;
 
    const options: IClientOptions = {
      clientId: config.deviceId,
      clean: false, // Persistent session
      reconnectPeriod: 5000, // Retry mỗi 5 giây
      connectTimeout: 10000,
      keepalive: 60, // Gửi PINGREQ mỗi 60 giây
 
      // Last Will and Testament
      will: {
        topic: `devices/${config.deviceId}/status`,
        payload: Buffer.from(JSON.stringify({
          status: 'offline',
          timestamp: Date.now(),
        })),
        qos: 1,
        retain: true,
      },
    };
 
    // Thêm auth
    if (config.username) {
      options.username = config.username;
      options.password = config.password;
    }
 
    // Thêm TLS
    if (config.caCert) {
      options.ca = config.caCert;
      options.cert = config.clientCert;
      options.key = config.clientKey;
      options.rejectUnauthorized = true;
    }
 
    this.client = mqtt.connect(config.brokerUrl, options);
    this.setupEventHandlers();
  }
 
  private setupEventHandlers(): void {
    this.client.on('connect', () => {
      console.log(`[${this.deviceId}] Đã kết nối broker`);
 
      // Publish online status (retained)
      this.client.publish(
        `devices/${this.deviceId}/status`,
        JSON.stringify({ status: 'online', timestamp: Date.now() }),
        { qos: 1, retain: true }
      );
 
      // Subscribe commands
      this.client.subscribe(`devices/${this.deviceId}/commands`, { qos: 1 });
    });
 
    this.client.on('message', (topic, message) => {
      try {
        const payload = JSON.parse(message.toString());
        this.handleCommand(payload);
      } catch (err) {
        console.error(`[${this.deviceId}] Message không hợp lệ trên ${topic}:`, err);
      }
    });
 
    this.client.on('reconnect', () => {
      console.log(`[${this.deviceId}] Đang kết nối lại...`);
    });
 
    this.client.on('error', (err) => {
      console.error(`[${this.deviceId}] Lỗi:`, err.message);
    });
 
    this.client.on('offline', () => {
      console.log(`[${this.deviceId}] Offline`);
    });
  }
 
  publishTelemetry(data: Record<string, number>): void {
    this.client.publish(
      `devices/${this.deviceId}/telemetry`,
      JSON.stringify({ ...data, timestamp: Date.now() }),
      { qos: 0 } // Telemetry: QoS 0 là đủ
    );
  }
 
  publishAlert(alert: { type: string; message: string; severity: string }): void {
    this.client.publish(
      `devices/${this.deviceId}/alerts`,
      JSON.stringify({ ...alert, timestamp: Date.now() }),
      { qos: 1 } // Alerts: QoS 1 — không được mất
    );
  }
 
  private handleCommand(payload: any): void {
    console.log(`[${this.deviceId}] Command nhận được:`, payload);
 
    switch (payload.action) {
      case 'recalibrate':
        // ... thực hiện recalibration
        break;
      case 'updateConfig':
        // ... áp dụng configuration mới
        break;
      case 'reboot':
        // ... lên lịch graceful reboot
        break;
    }
 
    // Acknowledge command
    this.client.publish(
      `devices/${this.deviceId}/commands/response`,
      JSON.stringify({
        commandId: payload.id,
        status: 'executed',
        timestamp: Date.now(),
      }),
      { qos: 1 }
    );
  }
 
  async shutdown(): Promise<void> {
    // Publish offline status trước khi ngắt kết nối
    this.client.publish(
      `devices/${this.deviceId}/status`,
      JSON.stringify({ status: 'offline', timestamp: Date.now() }),
      { qos: 1, retain: true }
    );
 
    return new Promise((resolve) => {
      this.client.end(false, () => {
        console.log(`[${this.deviceId}] Đã ngắt kết nối gracefully`);
        resolve();
      });
    });
  }
}
 
// Sử dụng
const device = new MqttDeviceClient({
  deviceId: 'sensor-ts-0042',
  brokerUrl: 'mqtts://broker.example.com:8883',
  username: 'sensor-ts-0042',
  password: process.env.DEVICE_TOKEN,
});
 
// Publish telemetry mỗi 5 giây
setInterval(() => {
  device.publishTelemetry({
    temperature: 23.5 + Math.random() * 2,
    humidity: 65 + Math.random() * 5,
  });
}, 5000);
 
// Graceful shutdown
process.on('SIGTERM', async () => {
  await device.shutdown();
  process.exit(0);
});

Python (paho-mqtt)

# mqtt_device.py
import json
import time
import ssl
import paho.mqtt.client as mqtt
 
class MqttDeviceClient:
    def __init__(self, device_id: str, broker_host: str, broker_port: int = 8883):
        self.device_id = device_id
        self.client = mqtt.Client(
            client_id=device_id,
            protocol=mqtt.MQTTv5,
            callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
        )
 
        # Last Will and Testament
        self.client.will_set(
            topic=f"devices/{device_id}/status",
            payload=json.dumps({"status": "offline", "timestamp": time.time()}),
            qos=1,
            retain=True,
        )
 
        # Cấu hình TLS
        self.client.tls_set(
            ca_certs="/etc/mqtt/ca.crt",
            certfile=f"/etc/mqtt/{device_id}.crt",
            keyfile=f"/etc/mqtt/{device_id}.key",
            tls_version=ssl.PROTOCOL_TLSv1_2,
        )
 
        # Callbacks
        self.client.on_connect = self._on_connect
        self.client.on_message = self._on_message
        self.client.on_disconnect = self._on_disconnect
 
        # Kết nối với auto-reconnect
        self.client.connect(broker_host, broker_port, keepalive=60)
 
    def _on_connect(self, client, userdata, flags, reason_code, properties):
        print(f"[{self.device_id}] Đã kết nối: {reason_code}")
 
        # Publish online status
        client.publish(
            f"devices/{self.device_id}/status",
            json.dumps({"status": "online", "timestamp": time.time()}),
            qos=1,
            retain=True,
        )
 
        # Subscribe commands
        client.subscribe(f"devices/{self.device_id}/commands", qos=1)
 
    def _on_message(self, client, userdata, msg):
        try:
            payload = json.loads(msg.payload.decode())
            print(f"[{self.device_id}] Command: {payload}")
            self._handle_command(payload)
        except json.JSONDecodeError:
            print(f"[{self.device_id}] JSON không hợp lệ trên {msg.topic}")
 
    def _on_disconnect(self, client, userdata, flags, reason_code, properties):
        print(f"[{self.device_id}] Ngắt kết nối: {reason_code}")
 
    def _handle_command(self, payload: dict):
        action = payload.get("action")
        if action == "recalibrate":
            pass  # Thực hiện recalibration
        elif action == "updateConfig":
            pass  # Áp dụng configuration mới
 
        # Acknowledge
        self.client.publish(
            f"devices/{self.device_id}/commands/response",
            json.dumps({
                "commandId": payload.get("id"),
                "status": "executed",
                "timestamp": time.time(),
            }),
            qos=1,
        )
 
    def publish_telemetry(self, data: dict):
        self.client.publish(
            f"devices/{self.device_id}/telemetry",
            json.dumps({**data, "timestamp": time.time()}),
            qos=0,
        )
 
    def start(self):
        self.client.loop_start()
 
    def stop(self):
        self.client.publish(
            f"devices/{self.device_id}/status",
            json.dumps({"status": "offline", "timestamp": time.time()}),
            qos=1,
            retain=True,
        )
        self.client.loop_stop()
        self.client.disconnect()
 
 
# Sử dụng
device = MqttDeviceClient("sensor-ts-0042", "broker.example.com")
device.start()
 
try:
    while True:
        device.publish_telemetry({
            "temperature": 23.5,
            "humidity": 65.0,
        })
        time.sleep(5)
except KeyboardInterrupt:
    device.stop()

Lỗi Thường Gặp Của Người Mới

Lỗi 1: Dùng Cùng Client ID Cho Nhiều Thiết Bị

❌ Tất cả thiết bị kết nối với clientId "my-device"
   → Broker ngắt kết nối client trước khi client mới kết nối cùng ID!
 
✅ Mỗi thiết bị có clientId riêng: "sensor-ts-0042", "sensor-ts-0043"

MQTT spec yêu cầu client IDs unique. Nếu hai clients kết nối cùng ID, broker ngắt cái đầu tiên. Gây ra hiện tượng "bóng bàn" khi cả hai thiết bị liên tục kết nối lại và đá nhau ra.

Lỗi 2: Subscribe # Trong Production

❌ client.subscribe('#')   → Nhận TẤT CẢ messages trên toàn bộ broker
 
✅ client.subscribe('factory/zone-a/+/telemetry')   → Chỉ những gì bạn cần

Subscribe # trên broker bận với 100.000 thiết bị sẽ ngập client với hàng triệu messages không xử lý nổi.

Lỗi 3: Không Dùng Retained Messages Cho Status

❌ Device publishes status khi connect, subscriber mới bỏ lỡ
   → Dashboard hiển thị "unknown" cho đến khi device publish status tiếp
 
✅ Device publishes status với retain=true
   → Subscriber mới lập tức nhận last known status

Lỗi 4: Bỏ Qua Keep-Alive Mechanism

❌ keepalive=0 (tắt) → Broker không phát hiện được dead connections
   → Ghost sessions tích lũy, chiếm broker memory
 
✅ keepalive=60 → Broker expect PINGREQ trong 1.5× keepalive (90s)
   → Dead connections tự động phát hiện và dọn dẹp

Lỗi 5: Không Có Chiến Lược Reconnection

❌ Một lần connect → connection fail → thiết bị im lặng mãi
 
✅ Exponential backoff reconnection:
   Lần 1: chờ 1s → Lần 2: chờ 2s → Lần 3: chờ 4s → ... → max 60s

Hầu hết MQTT client libraries (mqtt.js, paho-mqtt) tự xử lý reconnection, nhưng bạn cần cấu hình reconnectPeriod và handle event reconnect để re-subscribe nếu dùng clean sessions.


Tóm Tắt và Bài Học Chính

✅ Thiết kế topic hierarchies với wildcards trong đầu — {org}/{site}/{zone}/{device-type}/{device-id}/{data-type}
✅ Dùng shared subscriptions (v5.0) cho load balancing — phân phối messages giữa consumer workers native
✅ Khớp QoS với mức quan trọng dữ liệu — QoS 0 cho telemetry, QoS 1 cho readings, QoS 2 chỉ cho critical commands
✅ Persistent sessions + session expiry — cho thiết bị chạy pin sleep mà không mất messages
✅ Luôn dùng TLS 1.3 — MQTT plaintext là lời mời mở cho attackers
✅ mTLS là authentication IoT mạnh nhất — certificate riêng mỗi thiết bị, không passwords để lộ
✅ Triển khai ACLs — thiết bị chỉ nên truy cập topics của mình, không bao giờ #
✅ Cluster broker cho production — EMQX với 3+ nodes xử lý failover và horizontal scaling
✅ Monitor connected clients, message rates, và memory — bắt vấn đề trước khi thành outages
✅ Topic aliasing (v5.0) tiết kiệm bandwidth — thay topic strings dài bằng integers 2-byte


Tiếp Theo

Bây giờ bạn có thể design, bảo mật, và scale MQTT, đã đến lúc xem xét quản lý chính các thiết bị — provisioning, configuration, firmware updates, và fleet management ở scale.

Tiếp: IOT-6: Deep Dive: Device Management & OTA Updates — Quản lý vòng đời thiết bị, device shadow/digital twin pattern, OTA firmware updates với chiến lược rollback, remote diagnostics, và fleet management ở scale.


Bài Viết Liên Quan


Đây là Phần 5 của series IoT Patterns & Strategies. Bắt đầu từ đầu hoặc nhảy đến bất kỳ chủ đề nào bạn quan tâm.

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.