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/telemetryQuy tắc:
- Dùng lowercase với hyphens —
temperature-sensor, không phảiTemperatureSensorhaytemperature_sensor - Cụ thể từ trái sang phải — category rộng nhất trước, cụ thể nhất cuối
- Không bao giờ bắt đầu với
/— leading slash tạo level trống đầu tiên (/topiccó 2 levels,topiccó 1) - Giữ mỗi level có ý nghĩa — mỗi level nên phục vụ mục đích filtering
- Tách biệt data types — telemetry, commands, status, và alerts nên ở topics khác nhau
- 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:
| Wildcard | Ký Hiệu | Khớp | Ví Dụ |
|---|---|---|---|
| Single-level | + | Chính xác một level | factory/+/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 levels | factory/# 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/responsePattern 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/+/telemetryTrướ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 commandsQoS 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ệu | QoS | Tại Sao |
|---|---|---|
| Nhiệt độ mỗi 1s | 0 | Tần suất cao, reading tiếp thay thế cái mất |
| Đồng hồ điện (billing) | 1 | Không thể mất, idempotent theo timestamp |
| Trạng thái thiết bị (online/offline) | 1 + retained | Phải persist cho late subscribers |
| Lệnh emergency shutdown | 2 | Phải thực thi đúng một lần |
| Trigger cập nhật firmware | 2 | Duplicate trigger có thể làm hỏng update |
| Heartbeat / keepalive | 0 | Mất một cái không sao, vắng mặt trigger LWT |
| Thay đổi config từ cloud | 1 | Retry 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 1Nghĩ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ạnh | Clean Session (clean=true) | Persistent Session (clean=false) |
|---|---|---|
| Subscriptions | Mất khi ngắt kết nối | Broker nhớ |
| Queued messages | Bị bỏ | Lưu đến khi kết nối lại |
| In-flight QoS 1/2 | Mất | Tiếp tục khi kết nối lại |
| Client ID | Có thể trống (v3.1.1) | Phải unique và ổn định |
| Broker memory | Thấp | Tăng theo mỗi thiết bị ngắt kết nối |
| Phù hợp nhất | Clients tạm thời, dashboards | Sensors 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ĩaBả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 trueLayer 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ỏiOption 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 -sha256Option 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/commandsEMQX 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
| Layer | Tối Thiểu | Khuyên Dùng | Production |
|---|---|---|---|
| Transport | TLS 1.2 | TLS 1.3 | TLS 1.3 + certificate pinning |
| Authentication | Username/password | Client certificates (mTLS) | mTLS + short-lived tokens |
| Authorization | Basic ACL file | Pattern-based ACLs | Dynamic ACLs qua database/API |
| Payload | Không | Không (TLS mã hóa transport) | Application-layer encryption cho dữ liệu nhạy cảm |
| Monitoring | Broker logs | Failed auth alerts | Real-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?
| Metric | Single Broker | Clustered (3 nodes) |
|---|---|---|
| Concurrent connections | ~100K (tùy hardware) | ~300K+ |
| Availability | Một node fail = sập toàn bộ | Chịu được 1 node fail |
| Throughput | Giới hạn bởi single CPU | Horizontal scaling |
| Bảo trì | Downtime khi update | Rolling 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: bridgeCấ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 2Khở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: bridgeSo Sánh Brokers
| Tính Năng | EMQX | HiveMQ CE | HiveMQ Enterprise | Mosquitto |
|---|---|---|---|---|
| Clustering | Có (built-in) | Có (hạn chế) | Có (đầy đủ) | Không |
| Max connections | 100M+ (đã test) | ~100K | Hàng triệu | ~100K |
| Shared subscriptions | Có | Có | Có | Không (v2.0) |
| Rule engine | Có (SQL-like) | Không | Có | Không |
| WebSocket | Có | Có | Có | Có |
| Dashboard | Có (web UI) | Không | Có | Không |
| License | Apache 2.0 | Apache 2.0 | Commercial | EPL/EDL |
| Phù hợp nhất | Production IoT | Dev/prod nhỏ | Enterprise | Dev/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
| Metric | Ngưỡng Warning | Ngưỡng Critical | Ý Nghĩa |
|---|---|---|---|
| Connected clients | > 80% capacity | > 95% capacity | Sắp hết connection slots |
| Message rate (in/out) | Spike bất thường | 2x bình thường | Có thể message storm hoặc loop |
| Subscription count | > 1M mỗi node | > 5M mỗi node | Áp lực memory |
| Retained messages | > 100K | > 500K | Storage tăng không kiểm soát |
| Inflight messages | > 10K | > 50K | QoS 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: publicVí 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ầnSubscribe # 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 statusLỗ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ẹpLỗ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 60sHầ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
- Lộ Trình IoT Patterns & Strategies — Tổng quan series 12 bài và lộ trình học tập
- Giao Thức Truyền Thông IoT: MQTT, CoAP, AMQP & WebSocket — MQTT cơ bản, CoAP, AMQP, và lựa chọn giao thức
- IoT Edge Computing & Kiến Trúc Fog — Edge patterns, fog computing, offline-first design
- Lộ Trình Web Security — TLS, authentication, và authorization fundamentals
- Giải Thích Kiến Trúc Server-Client — Nền tảng để hiểu pub/sub và broker patterns
Đâ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.