MQTT là kênh phổ biến cho telemetry và điều khiển thiết bị IoT, nhưng payload JSON thô, không ký, không version hoá là nguồn lỗi âm thầm — từ tốn băng thông cellular đến thiết bị cũ parse sai khi server đổi schema. Bài này dành cho kỹ sư firmware và backend đang thiết kế lớp envelope ứng dụng trên MQTT, cần quyết định định dạng payload, cơ chế xác thực, QoS, và đặc biệt là retain flag — nơi nhiều hệ thống thật đã trả giá.
#Bối cảnh và phạm vi
Thiết bị dùng MQTT làm kênh song song cho hai luồng: telemetry (thiết bị → server, tần suất cao) và cmd/status (hai chiều, tần suất thấp nhưng cần tin cậy). Payload cần gọn để tiết kiệm băng thông trên mạng cellular hoặc gateway LoRa-MQTT bridge, và cần cơ chế xác thực nguồn gốc vì broker trung gian không đảm bảo publisher là thiết bị hợp lệ.
Phạm vi bài giới hạn ở thiết kế envelope ứng dụng — cấu trúc bản tin, versioning, ký số, QoS, LWT, retain. Không bàn broker clustering, high-availability, hay TLS mutual-auth (xác thực kết nối ở tầng transport là chủ đề khác).
#Envelope versioned: vì sao cần version ngay từ đầu
Trường ver trong envelope cho phép thay đổi schema ở phiên bản server mới mà không làm vỡ thiết bị đang chạy firmware cũ ngoài field — vốn không thể cập nhật đồng loạt. Chiến lược an toàn: bên nhận bỏ qua trường lạ không biết (forward-compatible), nhưng từ chối xử lý nếu giá trị ver nằm ngoài danh sách hỗ trợ, thay vì cố parse sai và tạo ra hành vi không xác định.
| Trường | Ý nghĩa | Ghi chú |
|---|---|---|
ver | Phiên bản schema envelope | số nguyên tăng dần, không tái sử dụng |
ts | Thời điểm tạo bản tin | dùng chống replay cùng seq |
seq | Số thứ tự bản tin theo thiết bị | chính sách reset khi reconnect/reboot do từng dự án tự định nghĩa, cần nhất quán giữa thiết bị và server |
payload | Dữ liệu nghiệp vụ | thay đổi theo loại bản tin (telemetry/cmd/status) |
sig | Chữ ký xác thực | tính trên payload+ts+seq, xem mục HMAC |
Việc đưa ver vào từ bản phát hành đầu tiên — dù lúc đó chỉ có một schema — rẻ hơn nhiều so với vá thêm sau khi đã có hàng nghìn thiết bị ngoài field không hiểu field mới.
#Vì sao chọn msgpack thay JSON
msgpack mã hoá nhị phân, giữ cấu trúc key-value tương tự JSON nhưng không cần dấu ngoặc, dấu phẩy, hay chuỗi ký tự cho số — payload thường nhỏ hơn JSON cho cùng nội dung, mức chênh lệch cụ thể phụ thuộc cấu trúc bản tin và cần đo trực tiếp trên dự án. Với kết nối cellular tính theo dung lượng hoặc gateway LoRa có giới hạn payload chặt, chênh lệch này tích lũy đáng kể qua số lượng bản tin lớn.
Trade-off: msgpack không đọc được bằng mắt khi log hoặc debug qua mosquitto_sub thô — cần tool decode riêng (CLI hoặc plugin cho broker) để xem nội dung khi điều tra sự cố. Với team không chuẩn bị tool này trước, chi phí debug tăng đáng kể trong giai đoạn đầu triển khai.
| JSON | msgpack | |
|---|---|---|
| Dung lượng bản tin mẫu | lớn hơn | nhỏ hơn (mức cụ thể cần đo trên bản tin mẫu thực tế của dự án) |
| Đọc trực tiếp qua log | được | không, cần decode |
| Tooling broker/CLI hỗ trợ sẵn | phổ biến | cần bổ sung |
| Overhead parse trên MCU | thường cao hơn (string parsing) | thường thấp hơn (binary), mức cụ thể phụ thuộc thư viện parse dùng trên firmware |
#Ký HMAC cho bản tin
Mục đích của HMAC trong envelope là xác thực nguồn gốc bản tin và chống sửa đổi trên đường truyền hoặc tại broker — không phải mã hoá nội dung. Nếu cần giữ bí mật nội dung payload, đó là yêu cầu riêng (mã hoá), không nên nhầm với ký số.
Trường sig nằm ở cuối envelope, được tính trên tổ hợp payload + ts + seq. Đưa ts và seq vào phạm vi ký — không chỉ payload — là điểm quan trọng để chống replay: nếu kẻ tấn công bắt lại một bản tin cũ hợp lệ và gửi lại nguyên vẹn, bên nhận có thể phát hiện qua seq đã dùng hoặc ts quá cũ, vì chữ ký gắn chặt với cả hai giá trị này.
Bài này không nêu chi tiết thuật toán băm hay cách quản lý khoá — đó là quyết định bảo mật riêng của từng dự án, cần đánh giá theo mô hình đe doạ cụ thể, không nên sao chép nguyên trạng từ bài viết kỹ thuật.
#QoS và khi nào dùng QoS nào
QoS trong MQTT là một trade-off giữa độ tin cậy giao bản tin và chi phí bộ nhớ/băng thông trên thiết bị nhúng. Chọn sai QoS theo hai hướng đều có hại: QoS thấp cho lệnh điều khiển có thể làm mất lệnh quan trọng; QoS cao cho telemetry tần suất cao làm broker và thiết bị tốn tài nguyên không cần thiết.
| Loại bản tin | QoS khuyến nghị | Lý do |
|---|---|---|
| Telemetry tần suất cao | 0 | chấp nhận mất gói lẻ tẻ, bản tin kế tiếp sẽ đến, không cần overhead xác nhận |
| Lệnh điều khiển (cmd) | 1 | cần đảm bảo đến, có thể nhận trùng — thiết kế idempotent ở phía xử lý lệnh |
| Status/presence | 1 | cần đến đúng, trùng không gây hại nếu xử lý theo giá trị mới nhất |
| Giao dịch tài chính/an toàn nghiêm ngặt | 2 hiếm dùng trên thiết bị nhúng | overhead bộ nhớ cho dedup theo message-id vượt khả năng RAM hạn chế của MCU |
QoS 2 đảm bảo "exactly once" nhưng yêu cầu broker và client giữ trạng thái handshake 4 bước cho mỗi bản tin — với RAM hạn chế trên MCU, chi phí này thường không đáng so với thiết kế idempotent ở QoS 1.
#LWT (Last Will and Testament) thiết kế đúng
LWT là bản tin broker tự phát khi phát hiện kết nối thiết bị bị cắt bất thường (mất điện, mất mạng, crash) — không phải khi thiết bị tự disconnect có chủ ý. Hai tình huống này cần được phân biệt rõ ở phía server: trước khi disconnect chủ động (ví dụ để OTA reboot), thiết bị nên tự publish một bản tin status "offline" rõ ràng, rồi mới disconnect — khi đó server nhận "offline" chủ động, không phải LWT.
Payload LWT nên tối giản — chỉ đủ để server biết thiết bị mất kết nối bất thường, không cần mang theo state nghiệp vụ phức tạp. Và theo nguyên tắc ở mục kế tiếp, LWT không nên retain nếu không có lý do rõ ràng, vì bản chất LWT là một sự kiện ("vừa mất kết nối"), không phải trạng thái cần tồn tại qua restart broker.
#Bẫy retained message — khi retain phá hệ thống
Retain flag làm broker giữ lại bản tin cuối cùng của một topic và gửi ngay cho subscriber mới join — hữu ích cho dữ liệu có tính "trạng thái cấu hình" cần tồn tại qua restart (config hiện tại, token provisioning). Nhưng retain sai cho dữ liệu có tính "phiên" (session đang chạy, đang truyền dở) tạo ra hệ quả nghiêm trọng: khi broker hoặc server restart, bản tin retained cũ được gửi lại như thể vừa mới xảy ra, khiến server suy luận sai trạng thái hiện tại.
Quy tắc kiểm tra nhanh trước khi quyết định retain cho một topic: tự hỏi "nếu server restart và nhận lại đúng bản tin này ngay bây giờ, điều đó có còn hợp lý về mặt nghiệp vụ không?" Nếu câu trả lời là không — ví dụ bản tin báo "đang truyền dữ liệu phiên X" — thì không nên retain.
#Chúng tôi đã gặp gì trên thiết bị thật
Ngược lại, bản tin token provisioning (dùng để thiết bị mới join nhận cấu hình khởi tạo) lại cần retain đúng, vì ý nghĩa của nó là trạng thái cấu hình cần tồn tại qua restart — thiết bị mới join bất kỳ lúc nào vẫn phải nhận được ngay, không cần chờ server publish lại.
Hai trường hợp này có cấu trúc envelope giống nhau, cùng đi qua một pipeline publish, nhưng cần chính sách retain ngược nhau hoàn toàn. Bài học rút ra: quyết định retain phải đi theo review riêng cho từng loại payload theo ý nghĩa nghiệp vụ, không áp dụng một quy tắc chung cho mọi topic trong hệ thống.
#Checklist khi thiết kế
- Envelope có đủ
ver,ts,seq,sigchưa? - Mỗi topic đã có quyết định rõ: retain hay không, kèm một câu lý do bằng ý nghĩa nghiệp vụ?
- QoS từng loại bản tin đã map theo mức độ chấp nhận mất gói (0/1), tránh dùng QoS 2 trên thiết bị nhúng nếu không có lý do bắt buộc?
- LWT đã tách biệt rõ với luồng disconnect chủ động (publish "offline" trước khi disconnect)?
- Đã test case "broker/server restart" để phát hiện retained message sai ý nghĩa chưa?
- Chữ ký HMAC đã bao gồm
tsvàseq, không chỉpayload, để chống replay? - Có tool decode msgpack sẵn sàng cho log/debug trước khi triển khai production?
#Đọc tiếp
Bài kế tiếp trong series: "Kết nối mạng cho thiết bị: reconnect, backoff, và phát hiện mất kết nối giả".