🌐

MQTTを使ったIoTシステム設計について考えてみる

2024/01/05に公開

はじめに

MQTTはTCPベースの常時接続型の通信方式です。

  • 軽量:ヘッダや常時接続を維持するためのkeep alive packetが軽量である
  • メッセージ: 配送の仕組みにBroker(ブローカー)がおりPublish/Subscribe型(以降pub/sub)の通信方式を取ることで、1対N N対Mなどのデバイス数が増えても効率的なメッセージを配送することができるものです。

ここでBrokerについて補足しておきますと、MQTT Brokerは

  • 接続するクライアントとの認証
  • セッションの管理
  • トピックの管理
  • メッセージのルーティングと転送

を行います。これらにより、少数のデバイスやセンサーであるPoCからスタートしたIoTプロジェクトが、成功してその後にデイバイスの数が増えてもサーバ側のリソース管理をクラウドサービス事業者へお任せできる点はかなりのメリットだと思います。
以降で、MQTTベースのシステムアーキテクチャでよくある皆様がハマりや悩み、理解されていないようなポイントを整理していきます。

アーキテクチャ上で重要なこと

MQTTとHTTPとの大きな差異としてはMQTTはBrokerまでの配送を保証しているものでありHTTPのようにバックエンドの処理結果などのようなresponseをMQTTクラアントのpublishへは返せないものとなります。
つまり、クライアントが送るメッセージ送信の成功、失敗はあくまでもBrokerまでのメッセージ到達ということしかわかりませんので、メッセージのその後の処理について(正しくDBへ保管できたのか?など)は非同期処理にならざるを得ない点はアーキテクチャを考えるうえで重要なポイントになります。まずはHTTP系のシステムとはここの差分を理解して設計を行う必要があります。

Clinet_idの設計

MQTT BrokerはMQTT client idの重複は許さない(後勝ち)仕様であるために、異なるMQTT clientで同一のclient_idを指定すると既存側のセッションが切断され、新規接続要求側のセッションが有効になります。 (そして自動再接続を指定していると、お互いのセッションを切り合う動作をしてしまいます)
皆様よく見かける動作確認ツール mosquitto client(mosquitto_pub)の動作例で -i(--id)を使われてる例がほぼ見かけませんがこの場合、mosquittoの仕様としてランダムな名前が使われています。これはpahoなどでも同様でランダムな名前が割り当てるケースが多くあります。Clientの設計上、Client_idが重複しない用にどの用に名前をアサインするのかは重要な要素です。(e.g.)デバイスが持つ単一IDな情報から取得するなど。
またクラウドサービスの機能を利用する場合、このClient_idが暗黙なThingとして使われているケースもあるので、この辺も確認したうえでClient_idを設計する必要があります。たとえばAWS IoTではThing_Nameとして使われるのは証明書ではなく、このMQTT Client_idをThing_Nameとして利用しています。
mosquitto_pubなどであれば -i hoge の様に指定、pahoであればconnect関数の中でclient_idを引数として指定するようになっています。

Topic設計

MQTTをベースにするシステムにおいて最も重要なのがtopic設計だと思います。自分たちの使い方にあわせてどの用にメッセージのルーティングをしたいのかを、設計と設定だけで管理できるようになります。(AWS IoT Coreのルールエンジンを使うなどを前提としてます)
例えば、デバイス視点でクラウドへデータを送りたいというコンテキストはuploadなのでtopic中にuploadなどを使いたくなりますが、brokerを介してsubscriberで受け取る側の立場だとuploadは非常にわかりにくいtopicになります。私自身は明確な通信方向をいれ、D2C(device to cloud) や C2D(cloud 2 device)などをつかうことをお勧めしています。topicの設計には、RESTの設計と同じ様に意味を考えること、小文字のみやキャメルケースなどの運用ルールを適用することが重要です。 topicの階層は 抽象->具体(個別) のようにドリルダウンしていくとルールエンジンのワイルドカードとの相性が良いと思います。
例えば、 system_name/factory_name/machine_name/sensor_name など
IoTではデータ収集(N->1)のパターンやメッセージ伝達(N<-1)など、その他いろいろなユースケースがありますが、AWSが出しているTopicデザインのホワイトペーパーがパターン網羅やわかり易さの観点からも一読の価値があると思います。

以下一例です。(センターから各デバイスに対してのメッセージを送信する例)

初版のホワイトペーパーを翻訳したものになります。(2018年なので最新については以下のAWSのドキュメントを見てください)
https://qiita.com/TakashiKOYANAGAWA/items/2a9e8c17cb2febce3296

最新はAWS IoTのドキュメントに移行されたようです。
https://docs.aws.amazon.com/ja_jp/whitepapers/latest/designing-mqtt-topics-aws-iot-core/designing-mqtt-topics-aws-iot-core.html

QoSレベル

QoS(Quality of Service)はメッセージ到達保証性をきめるpublish/subscribeのパラメータになります。以下3段階のレベルが定義されています。

  • QoS = 0 : At most once
    0 は確実性の無い配信でclientが publishコマンドを実行できればよく、Broker側の到達を確認しません。途中経路上のネットワークの問題などでメッセージが消失する可能性があります。
  • QoS = 1 : At least once
    1 を指定するとメッセージを受信したBroker puback という返答を行います。このpubackを受信できない場合にクライアントは再送を実施します。つまりpubackだけが受信できなった場合にsubscriber側には重複したメッセージが到達する可能性あり、そのためAt least once/最低でも一度と定義されます。
  • QoS = 2 : Exactly at once
    先に述べた通りクラウドのマネージドなIoTサービスでは利用できないことが多いQoSになります。これはclientとbrokerの間で3-wayハンドシェイクのようにメッセージ受信後に pubrec(broker->client), pubrel(client->broker), pubcomp(broker->client)がおこなれて初めてメッセージ送信を成功とみなすことで確実に一回のみを保証する仕組みとなっています。

以下の図はSoftwareToolboxさんのサイトより

publisher, SubscriberともにQoSを設定することができますが、pub/subの関係で非対称なQoSを指定した場合はPublisher側のQoSが利用されます。 つまりSubscriberが QoS=1でセッションを張っていてもpublisherがQoS=0でBrokerへメッセージを送ると、BrokerはQoS=0としてSubscriberへメッセージを送信するということになります。

バックエンドシステム側でのMQTTの良さ

AWS, AzureではマネージドなIoTサービスとして AWS IoT CoreAzure IoT Hubなどがあります。これは単純にマネージドかつサーバレスなMQTT Brokerが便利というわけだけではなく、topicコンテキストをベースにルールエンジンで後段に置くクラウドサービスへのデータ転送の連携が簡単であることがメリットになります。(データ転送のコードを作成するのではなく設定だけでデータ転送がされ、また複数サービス転送することも可能)
一方で気をつけるべき点として標準的なMQTT3.1.1などの仕様とは異なる(非サポートな機能があったりする)部分です。例えば QoS=2のサポートはAWS, Azureともにされていないようです。これらのように自分が使いたい機能がサポートされているかどうかは最初にチェックすべき点になるかと思います。

クライアント側でのMQTTの良さ

はじめに書いた通り軽量プロトコルであるところ、リアルタイムにサーバ(クラウド側)からコンテキストを遅れる点だと思います。リアルタイム性が必要ないなら他のプロトコルを使うことを考えても良いかと思います。
例えば1時間に一度HTTPでデータを送信している場合、そのレスポンスでデバイスへ伝えたいメッセージを詰めて返すや、一日数回デバイスへの指示が来ていないかのポーリング型のようなメッセージを送るような方式でも成り立つと思います。
そういった点ではデバイス観点ではお使いのデバイス、センサー上で動くMQTT SDKがあるのかや、常時接続に耐えるうる(=常時電源がとれるか)などの確認はしておいたほうが良いでしょう。

疎通テストのポイント

よくMQTT疑似クライアントとして mosquitto_client を利用している例を見かけます。たしかにmosquittoは簡単に使えるのですが、mosquitto_clientを使った疎通では以下の点がMQTT SDK(paho)などを使った場合と異なることに注意してください。

  • 常時接続型のclientではなくメッセージ送信毎にMQTTセッションの作成(ハンドシェイク)から接続を行いメッセージ送信が完了すると、接続を切断しますので、mosquitto_pubは常時接続モデルになっていません
  • clinet_idを省略しているケースが多い。
    多くのサンプルでは mosquitto_pub -t test/topic -m "testmessage" の用に使っている例が数多ありますが、前述の通りサーバ側のテストにはなってもクライアントのテストになっているのかはご自分のやりたいシステムの実装との乖離を確認してください。例えば、双方通信するためにmosquitto_pubに併せて、 mosquitto_sub -t test/topic というものをつくってメッセージの送受信をすれば pub されたものが当然、subにも流れてきます。しかしこれは -i(--id)が省略されているために同一のデバイスやPCで実施したとしても2つのmqtt_clientが動いている用に振る舞いますので、皆さんが想定されるMQTT clientの作りとは異なるものとなります。(前述のClient_idの使い方がプロセス化した場合に全く別になると思います)
    どのPCで動作しているのかが重要ではなく、どのClient_idでBrokerへ接続しているのかが重要になるためです。
    そのために簡単なデータ送信テストとしてはお手軽なmosquitto_pub,subなどのmosquitto_clientでは実際のデバイスを作成できないケースは多くあります。
    mosquitto_pubでテストをされる場合には、manページもみてやっている中身(引数の意味)を理解しておくのが重要です。
    https://mosquitto.org/man/mosquitto_pub-1.html

MQTT clientを作成する場合はpahoなどのSDKを使ったクライアント型の常駐プロセスで検討するのが良いと思います。 C/C++, Java, Javascript, Python, rust, goなど多くの言語に対応しています。
https://eclipse.dev/paho/index.php?page=downloads.php

mosquittoでクライアントを作成するのであれば、ライブラリをベースにclientを実装するようになるとおもいます。

まとめ

IoTにおいて本当にMQTTが最適かを考えてみましょう。MQTTは常時接続型のTCP通信のプロトコルです。軽量であると書かれていますが、これの比較対象は、HTTP、HTTPSなどとの比較で論じられてることが多くあります。
例えば、デバイスやセンサーがバッテリーで動作するものの場合送信頻度を下げて通信させるような通信方式がよいケースが多くあります。クライアント側視点でも書きましたがポーリング型のデバイス通知のシステムを検討するのもありです。
また、LTE-MのeDRX/PSMのような間欠通信は通信モジュールを寝かせることで消費電力を下げるという方式をとっていますので、常時接続とその接続を維持するためのkeep alive packetを必要とするMQTTとは相性が悪いと言わざるを得ません。その場合はポーリング型やデバイス送信のレスポンスでデータを伝達させることを考えてみてください。またはモジュールベンダなどにご相談しておいたほうが良いかもしれません。

免責的なこと

本記事は個人の経験などを通しての投稿となっており、筆者の属する組織の公の見解を示すものではありません。
それなりに長くIoTシステムのアーキテクト、とあるクラウドサービスのスペシャリストとしての活動をしておりその中で皆様がMQTTにとっつきにくい、わかっていないと思われる部分を個人の観点で整理してみたものです。
クラウドの進歩は著しくベストな設計は刻々と変わっていきますので、そのあたりも皆様で最終的なご確認をいただければと思います。

Discussion