我最近在整理以前讀過的書,重新翻到《物聯網系統開發:從 0 到 1 構建 IoT 平台》。

這本書對我幫助滿大,尤其是公司決定要引進 MQTT 的時候。那時候我需要的不是再看一個「五分鐘連上 broker」的範例——網路上到處都是——而是想知道大量設備真的進來之後,平台會在哪裡失去控制。
設備連上來之後
我最初的心智模型很直線:設備 connect、publish,backend subscribe,最後寫進 DB。本機 demo 收得到訊息,看起來就完成一半了。
真實設備不是這樣。它們會離線、重連、換網路,可能因為電量或訊號一天只醒來幾秒。平台面對的不是一條穩定的 request,是一群各自帶著舊 session、舊韌體、時鐘還不準的 client。「現在連得上嗎」只是入口問題,更難的是斷線期間發生了什麼、重連後哪些資料還有效、UI 上顯示的狀態到底是事實、推測,還是一次早就過期的觀察。
哪些是 MQTT 給的,哪些是 EMQX 給的
這是我讀這本書之後養成的習慣:把協議跟實作分開記。OASIS MQTT 5.0 規格定義封包、session、QoS 這些雙方必須遵守的行為;EMQX 是其中一種 broker 實作。把 MQTT 協議能力跟 EMQX 的實作設定混在一起記,架構決策看起來可攜,實際上綁死在某個產品上。
授權是最典型的例子。大量 client 一直 publish、subscribe,如果每個 hot path 都同步查一次外部 DB,授權服務自己就先變成瓶頸。EMQX authorization 文件裡的 session authorization cache 跟 node-level external resource cache,是 broker 為了少查幾次而提供的實作選擇——不是 MQTT 5 自動給你的保證。
cache 也不能只談命中率,還要談失效。EMQX 的 session cache 在斷線或重連時會清掉,也可以設 TTL 或人工清。但如果有人的權限被撤銷了呢?平台要自己定義多久內必須生效、要不要主動踢掉既有連線。這裡不想清楚,效能的改善就換來一段說不清楚的授權空窗。
收到 PUBACK 之後呢
QoS 1 的 PUBACK 是 MQTT 這一跳對 PUBLISH 的傳輸層確認,不是應用層的執行完成證明。broker 接受訊息、下游 consumer 收到訊息、設備執行完指令,是三個不同的邊界,不能用同一個「成功」概括。平台收到 PUBACK,最多只能證明協議交換往前走了一步——馬達有沒有真的轉、韌體有沒有拒絕參數、裝置是不是在執行前又斷線了,都得靠設備自己用 application event 回報。
可靠傳輸還有另一面:重送。設備端跟 backend 都要拿 command ID 做冪等或 dedupe,不然一次網路抖動,「至少送達一次」就變成「實體動作做了兩次」。
Shared Subscription 分的是流量
開發環境通常只有一個 subscriber,上線之後是好幾個 pods 同時在消化 telemetry。全部用一般 subscription 訂同一個 topic,每個 instance 都會收到一份;要用 $share/backend-workers/device/+/telemetry,同組 consumer 才會被當成一個處理池。
但 EMQX Shared Subscription 文件講的是同一群組內每次選一個 subscriber 收訊息——負載平衡,就這樣。它並非 end-to-end 的 exactly-once:斷線、session expiry、重派、consumer 做到一半掛掉,應用層照樣要面對重複跟做到一半的狀態。所以 database write、事件發布、外部 API 呼叫,還是要能重播——保留 message 或 command 的穩定識別、用唯一鍵擋重複、acknowledgment 放在 durable work 真的完成之後。
一條 command 的一生
「已送出」對操作的人來說沒有資訊量。我會把 command 拆成 accepted、running、succeeded、failed、expired 這些明確狀態,平台受理跟設備回報分開記。每次操作有一個不因 retry 改變的 command ID,狀態轉移要能拒絕晚到的倒退更新——設備都回報 succeeded 了,晚到的 running 不能把畫面改回執行中;逾期的 command 也不該在設備重新上線之後突然復活。
群組操作還多一層。如果 command 是照 tag 選設備,就要存下當時的 tag version 或實際的 target snapshot。不然過幾天有人問「這個指令當初到底打了哪一批設備」,你只能拿已經變過的標籤重新解釋歷史。
Receive Maximum 管的是方向
MQTT 5 的 Receive Maximum 是接收方宣告的方向性額度:限制對方同時送過來、還沒確認完的 QoS 1/2 PUBLISH 數量。client 在 CONNECT 宣告的管 server 到 client;server 在 CONNACK 宣告的管 client 到 server。它不是全系統共用的 throughput 旋鈕,也不管 QoS 0。
EMQX inflight 與 message queue 文件講的是另一層:broker 用每條 client connection 的 max_inflight 保存已送未確認的訊息,滿了再進 queue。這跟 MQTT 的 Receive Maximum 有關聯,但 broker 的設定還包含自身上限、舊版 client 的行為、queue 的淘汰策略——protocol 的額度跟 EMQX 實作的設定,不是同一層控制。
Topic Alias 處理的是另一種成本:同一條連線內先建立 alias 對 topic 的映射,之後省掉重複的長 topic name。它省封包,但不是跨重連永久保存的名稱表,也救不了慢 consumer 的 inflight 堆積。
最後剩下的問題:狀態可不可信
一個可信的 device state 不能只存 online: true。至少要說得出狀態來自哪個事件、設備時間跟平台接收時間、當時是哪版韌體或設定、這次觀察超過可接受的新鮮度沒有。晚到的 telemetry 不能蓋掉較新的值,連線狀態也不能當業務狀態用——設備在線不代表 command 可以執行;平台沒看到回應,也不等於設備沒收到。這些不確定性應該留在模型裡,不是被 UI 壓成一顆綠點。
協議的選擇也一樣要回到限制條件。IETF RFC 7252 把 CoAP 定位成給受限節點、低功耗、易丟包網路用的 request/response 協議,基礎訊息交換走 UDP。小記憶體、短暫喚醒、Web-style resource 的場景,它可能比 MQTT 合適——但這是條件式的選擇,不是「IoT 一律用 CoAP 比較輕」。
現在要我設計平台,我會先問:設備能保存多少狀態?網路會斷多久?指令有沒有實體副作用?失聯之後允不允許補送?這些答案才決定 session、QoS、cache、queue、command lifecycle 跟協議——不是反過來拿一組 broker 功能套所有裝置。
我讀這本書最大的收穫還是那個很樸素的轉向:MQTT 的難點不是連線,連線只是開始。真正的工作,是讓權限、訊息、指令、晚到的資料經過真實世界的干擾之後,平台還能給出一個可信的設備狀態。