.NET MQTT 用戶端訂閱方法使用時的陷阱,關於 MaximumQualityOfService
我有個用 .NET Core 開發的案子,需要透過 MQTT 取得設備狀態後顯示在螢幕上。 最近在新增可用設備後,遭客戶回報主程式顯示設備未回應,但指定設備實際上還在運作的狀況。 我仔細分析了程式工作期間的 log 內容,發現是主程式訂閱的訊息中途失聯了。 主程式剛開始都有收到設備發佈的狀態,一段時間後就收不到了。而執行緒仍然活著好好的,顯然不是程式錯誤。
在解決過程中,我才發現被 System.Net.Mqtt 套件坑了。 它有一個不知該說是臭蟲、或是陷阱的特性。 若沒有正確設置 MqttConfiguration 的 MaximumQualityOfService 屬性,則訂閱者收到 20 次訂閱訊息後,就不再通知訂閱者新的訊息。
最初我的程式使用 .NET MQTT 套件時,沒有指定訊息的 qos ,採用預設組態。
- 配置 MqttClient 時,其
MqttConfiguration
省略MaximumQualityOfService
屬性;此時預設值是AtMostOne
(0)。 - 調用
SubscribeSync()
訂閱主題時省略 qos 參數;此時 qos 預設值也是AtMostOne
(0)。 - 設備發佈的訊息,它則明確指定 qos 為
AtLeastOne
(1)。
在這種條件下,程式跑起來都很正常,訂閱者擺著幾小時仍然持續收到訊息。
但是我更新程式時,手癢把 SubscribeSync()
加上 qos 參數,指定為 ExactlyOne
。
跑幾次單元測試都沒問題,就部署到客戶端。第二天,客戶就回報狀況了。
為了重現狀況,我寫了兩個測試程式,分別負責訂閱者和發佈者的工作。
這兩個程式是用 .NET 6 寫的。取回程式碼後,執行 dotnet run
就能跑了。
發佈者的參數
發佈者程式碼使用 MQTT 參數節錄於下,這部份在測試過程中不動。
var configuration = new MqttConfiguration
{
MaximumQualityOfService = MqttQualityOfService.AtMostOnce
};
mqClient.PublishAsync(msg, MqttQualityOfService.AtLeastOnce);
注意 MqttConfiguration 的 MaximumQualityOfService
為 AtMostOne
,而發佈時的 qos 是 AtLeastOnce
。
儘管兩者不匹配,但對發佈工作沒有任何影響。
訂閱者的 qos 與 MaximumQualityOfService 符合時
訂閱者先測試 MQTT 套件預設值的情形。其程式碼的 MQTT 參數節錄於下。
var configuration = new MqttConfiguration
{
MaximumQualityOfService = MqttQualityOfService.AtMostOnce
};
mqClient.SubscribeAsync(topic);
// 省略 qos 參數時採用預設值 AtMostOne
我分別開兩個視窗,一個跑訂閱者,一個跑執行者。觀察執行結果一小時都很正常。
訂閱者的 qos 與 MaximumQualityOfService 不相符時
接著,來看看只有 SubscribeAsync()
的 qos 參數指定為 ExactlyOne
的情形。
MQTT 參數節錄於下。
var configuration = new MqttConfiguration
{
MaximumQualityOfService = MqttQualityOfService.AtMostOnce
};
mqClient.SubscribeAsync(topic, MqttQualityOfService.ExactlyOnce);
然後我就看到訂閱者收到 20 次訊息後,就不再收到新訊息了。如圖:
我本來猜測是不是跟連線時間有關。 接著調整了發佈者發佈訊息的時間間隔,看看時間有無影響。 而結果不論是隔 10 秒、 20 秒、甚至隔 1 分鐘。訂閱者都是收到 20 次訊息。
面對這個結果,我心中只想著這是不是在惡整人啊?
但只要把 MaximumQualityOfService
也配合調整為 ExactlyOnce
,那麼訂閱者又正常了。
結論
首先,對發佈者來說, PublishAsync()
的 qos 參數不受 MaximumQualityOfService
影響。
而對訂閱者來說, SubscribeAsync()
的 qos 參數必須小於或等於 MaximumQualityOfService
。
否則的話,訂閱者最多可收到 20 次訊息。超過就會失效。
因為我的 MQTT 服務端是用 mosquitto,所以後來查了一下 issue ,找到固定 20 次失效的原因。 20 次是 mosquitto 組態項目 max_inflight_messages 的預設值。 mosquitto 預設服務端和訂閱者之間最多允許 20 筆處於遞送過程的訊息,超過這數目就不再發送訊息給訂閱者。 但在本文的案例,訂閱者已經收到訊息了,為什麼訊息仍然被認定為「遞送中」? 那是因為訂閱者沒有按照 qos 傳送回執 (PUBREL) 給服務端。 參考「Mosquitto broker stop publishing on subscribed topics」。(2022-09-06補充)
儘管找到固定 20 次失效的原因,但問題還是在 System.Net.Mqtt 。
它不按照訊息的 qos 完成回執,反而受到 MaximumQualityOfService
參數限制,累積大量「收到訊息卻不回報」的欠條,最後就失去訂閱內容了。
我不知道 System.Net.Mqtt 為何如此設計,也不知道為何不擲出例外。 這真是令人困惱的特性。難以歸類是臭蟲還是陷阱。
我的實際案例使用的設備,回報狀態的週期最少是 2 分鐘以上。這表示至少前 40 分鐘都會正常運作。 而我在做單元測試時,可不會測到 40 分鐘後才出現的錯誤狀況啊。著實被它耍了一把。