IIJのIoTプラットフォームサービスを支える設計技術
2019年12月22日 日曜日
CONTENTS
Twitterフォロー&条件付きツイートで「バリーくんぬいぐるみ」を抽選で20名にプレゼント!
応募期間は2019/11/29~2019/12/31まで。詳細はこちらをご覧ください。
今すぐツイートするならこちら→ フォローもお忘れなく!
【IIJ 2019 TECHアドベントカレンダー 12/22(日)の記事です】
こんにちは。IIJ IoTサービスを開発している近藤です。
これまでのエンジニアブログの記事をご覧いただくとお分かりの通り、IIJはIoT事業にも積極的に取り組んでいます。
IIJ IoTサービスは、その取り組みの一つである、エンタープライズ向けのIoTプラットフォームサービスです。
プラットフォーマーとして、高トラフィック・高負荷に耐えうるシステム作りに日々検討・改良を加えておりますが、この記事では、そんな私達が取り入れている設計手法 メッセージングアーキテクチャ(イベントドリブンアーキテクチャ) をご紹介します。
高トラフィック、そして高負荷
まず、IIJ IoTサービスとは何なのかを説明せねばなりません。
IoTのシステムというものを少しだけ想像すると、「取得したセンサーデータを、クラウドサービスに送る」といった具合でしょうか。
この“クラウドサービスに送る”の部分、さらに考えを進めると、結構面倒なんです。
「クラウドサービスまでのネットワークはモバイルを使うか?じゃあどの会社のSIMカードを調達しよう?SIMカードって料金以外に違いはあるのか?データを送信するデバイスが直接インターネットから繋げられる状態はセキュリティ的にありえないから、モバイルのプライベートネットワークを作らなきゃいけないかな… 1,000台もデバイスがあると電池交換だけでも大変な作業だから、いっそ軽量なUDPなんかでクラウドサービスにデータを送って、電池の節約を図れないだろうか?でもインターネットに出るのに暗号化しなくてもいいはずないよな… そもそもクラウドサービス使うのに認証が必要なのか。げ、これ認証キーを変更したら、1,000台のデバイス一つ一つにログインして認証キーの変更しなきゃならないのか…」
などなど。IoTのシステムを検討されたことのある管理者の方はうんうんと頷いていただけるのではと思います。
こんなつなぐ部分の煩わしさを一挙に引き受けるのが、IIJ IoTサービスです。
「IIJ IoTサービスで提供しているSIMをデバイスにさして、センサーデータをとにかくプラットフォームに送信していただくだけで完了です。SIMとプラットフォームの間はプライベートネットワークになっているため、デバイス(の情報)が直接インターネットに晒される状態にはなりません。また同じ理由で通信の暗号化を考える必要がないため、HTTPでもUDPでも軽量なプロトコルで送っていただいて結構です。送信先のクラウドサービスの認証などの設定はWeb上で管理し、そこで設定を一度変更するだけで、1,000台全てのデバイスに対してその変更が適用され、適切にクラウドサービスへセンサーデータが送信されます。」
他にも、プライベートネットワーク上にあるデバイスの管理画面に、Web上でアクセスできるといったリモートメンテナンス管理機能や、センサーデータ群をファイルにして半永久的にオブジェクトストレージに保存する機能など沢山ありますが、閑話休題、サービスの説明は終わりです。
このように、IIJ IoTサービスは、大量のデバイスからプラットフォームを目掛けてセンサーデータが送信されてきます。それはもう、休み無く、絶え間無く。また、PoC目的である場合を除き、一人のお客様がご利用になるデバイス数は、10〜1,000台と、非常に数が多いです。それら全てのお客様のデバイスから受け取るセンサーデータを、プラットフォームは高速に、損失することなく、適切に処理をしなければなりません。
プラットフォームの負荷は高い。そして、それに耐えねばなりません。
システムを一から考える
こうした高負荷に耐えられるシステムを、一度頭をまっさらにして、一から考えてみます。
ここでは、「デバイスからHTTPやUDPで送信されたセンサーデータを、お客様が指定したサーバへHTTPSで暗号化して転送する」という、IIJ IoTサービスの最もベーシックな機能 データハブ を例に考えます。データハブには、受け取ったセンサーデータが本当にお客様の契約したSIM経由で送信されているかを確かめる認証機能、またセンサーデータをお客様のサーバへ届ける転送機能が必要です。
まず、一番素朴な例を考えます。以下はその模式図です。
シンプルですね。一つのアプリケーションが、受け取ったセンサーデータに対して認証を行い、その処理の後に転送を行なっています。ここでは話を簡単にするために、センサーデータの送信はHTTPプロトコルのPOSTメソッドを使って行われているとします。
さて、デバイスから送信されるセンターデータが多くなり、とうとう一つのアプリケーションでは処理できなくなりました。処理すべきセンサーデータの量が多くなったのならば、こちらもアプリケーションの数を多くしましょう。次に考えられる構成は以下です。
前段にある「LB」は「LoadBalancer」の略で、役割は字のまま「負荷分散」です。具体的にはNginxやApache HTTP Serverといったソフトウェアが有名です。こいつは賢くて、デバイスから送信されたセンサーデータを、後ろに控えているアプリケーションに対して均等に割り振ってくれます。また、後ろに控えているアプリケーションの一つが死んでいたら、そのアプリケーションを避けて、生きているアプリケーションだけに割り振る機能も持っています。
今、後ろに3つのアプリケーションがあるため、単純計算でこのプラットフォームが処理できるセンサーデータの量は、以前の3倍になりました。この後ろに控えているアプリケーションを増やせば増やすほど、プラットフォーム全体の処理性能は理論的には何倍にもなりそうです。
ここまではよくあるシステム構成です。それに一つ観点を加えます。アプリケーションの保守性です。一般的に、一つのアプリケーションに沢山の機能を詰め込むほど、そのアプリケーションの改修には時間がかかります。というのも、機能のコードを一部変更した際に、改修予定になかった機能にまで、予期せぬ形でその変更が影響してしまいバグに繋がることがあるからです。この例であれば、転送機能のコードを改修した際に、その変更が認証機能にまで波及していないかと心配になります。こうした心配から、転送機能の改修なのに、認証機能が正常に動くことを確認するテストにも時間を費やしてしまいます。
そこで、この一つのアプリケーションを二つに分解し、認証機能だけを持ったアプリケーション(認証アプリケーション)と、転送機能だけを持ったアプリケーション(転送アプリケーション)を作ってそれぞれ動かします。これらアプリケーション間の通信は、話しを簡単にするためにHTTP REST APIで行われているものとします。(microservice!?)
だいぶ良くなりました。認証機能と転送機能がアプリケーション単位で異なるため、それぞれのアプリケーションは自分の役割に専念すればOKです。つまり、認証アプリケーションは「認証を確認して、OKなら次へ渡す、NGならドロップする」、転送アプリケーションは「受け取ったセンサーデータをお客様のサーバへ転送する」ということに集中すればよく、これは開発者にとって、改修するハードルを下げてくれます。まさにUNIXの哲学にある”Do One Thing and Do It Well“を体現しているようです。
またアプリケーションの個数を増やす単位も、より最適なものになりました。例えば転送アプリケーションの負荷が高い一方で、認証アプリケーションの負荷がそうでもない場合、転送アプリケーションの個数のみを増やす、という戦略をとることができます。これにより、限りあるリソースをより効率的に利用できるようになりました。
「デバイスは、お客様のサーバへセンサーデータを送信した時のレスポンスを受け取らなければならない」といった、デバイスとお客様サーバ間の同期的な協調が要件としてあるならば、これは一つの完成形だと思います。もっとも、LBをNginxなどで自前で構築すると、アプリケーション数に増減がある度にLBに設定の変更を加えねばならず、だいぶ手間がかかります。Kubernetesやその周辺の技術は、こうしたLBの設定の煩わしさを吸収し、またアプリケーションのロケーションすらも意識させない仕組みを提供してくれます。アプリケーションが乱立し、大量のLBが存在し得るような上記の構成を取る際は、Kubernetesの導入を見据えたほうがよいでしょう。
さて、ここで各アプリケーションが同期して動いているとします。
デバイスがHTTP POSTでデータを送信してからレスポンスが返るまでの間に行われている通信に、順に番号をつけました。この時の認証アプリケーションの処理時間を図にすると以下のようになります。
上の図からわかるように、認証アプリケーションに、転送アプリケーションがサーバに転送した結果を待つ時間が生じています。つまり、認証アプリケーション処理時間は、転送アプリケーションの処理時間に大きく影響を受けます。これでは、仮に認証アプリケーションがせっかく高速に処理をできたとしても、自分とは関係ないところ(=転送処理)で時間がかかっているせいで、処理を終えるまでに結局時間がかかってしまいます。
極端な例として、動画エンコーディングを考えてみます。動画ファイルを受け取るだけの受信アプリケーションと、受信アプリケーションから受け取った動画をエンコーディングするだけのアプリケーションの2つを考えると、後者のアプリケーションのほうが前者の受信アプリケーションよりもうんと時間がかかります。この場合、二つのアプリケーションが同期する構成をとろうとは考えないでしょう。
これを解決したければ非同期な仕組みを導入する必要がありそうです。
メッセージングアーキテクチャ
認証アプリケーションと転送アプリケーションの間のLBがMessageBrokerというものに変わりました。
これは、この記事でメッセージングアーキテクチャと呼んでいるものの一つの構成例です。
メッセージングアーキテクチャとは、メッセージ(=データ)という概念を基調にして、中間層であるMessageBrokerを介し、コンポーネント間が非同期でメッセージをやりとりする構成パターンを指します。イベントドリブンアーキテクチャと呼ばれることのほうが多いです。また、これはよく知られるPub/Subモデルです。
Pub/Subモデルでは、メッセージをMessageBrokerに保存する役割(今の場合認証アプリケーション)をPublisher、メッセージをMessageBrokerから取得する役割(今の場合転送アプリケーション)をSubscriberと呼びます。MessageBrokerは、Publisherから受け取ったメッセージを保存し、またSubscriberから要求されたメッセージを渡す役割を持っています。またPublisherがMessageBrokerにメッセージを保存することをpublish、SubscriberがMessageBrokerからメッセージを取得することをsubscribeと言います。
MessageBrokerで有名なソフトウェアとして私がすぐに思いつくのは、Kafka、RabbitMQ、またRedis(のPub/Sub機能)です。他にも多くのソフトウェアがあるようですし、AzureやAWSがサービスとして提供しているMessageBrokerもあります。
さて、再度私たちが作ろうとしているアーキテクチャ図に戻りましょう。認証アプリケーションは、そのレスポンスを、MessageBrokerから受け取っています。このとき、MessageBrokerは転送アプリケーションの処理の完了を待たずに、認証アプリケーションにレスポンスを返します。すなわち、認証アプリケーションは転送アプリケーションの処理時間に影響を受けません。自分のペースで処理ができるため、本来の自分の力を発揮することができ、一つの認証アプリケーションでより多くのセンサーデータを処理できるようになりました。
一方転送アプリケーションは、データが来るのを待つのではなく、自らMessageBrokerにセンサーデータを取りにいっています。転送機能アプリケーションもまた、自分のペースで処理をしています。
これまで処理速度に注目してきましたが、今度はアプリケーション間の機能的な関係に注目します。
認証アプリケーションは、転送アプリケーションの存在を知らずとも動きます。処理をした結果をMessageBrokerにただただpublishしているだけです。転送アプリケーションもまた、受け取ったデータが誰から送信されたものかを知りません。つまりPublisherとSubscriberそれぞれに対する依存関係がより小さくなっています。PublisherとSubscriber間の関係が疎結合になりました。
この疎結合性に注目すると、機能の追加がより容易になります。例えば、「デバイスから送信されたセンサーデータを、お客様が指定したクラウドサービス(例えばAzure IoT Hub)へ転送する」という クラウドアダプタを新しく機能追加したいとします。すると以下のような構成を考えられます。
新しく追加されたクラウド転送アプリケーションは、MessageBrokerからsubscribeしているだけです。認証アプリケーションと転送アプリケーションは、この機能追加による影響をより受けにくくなっています。
IoTサービスでは、これを基本的な考え方として設計しています。
運用的な観点でこの設計に助けられたのは、メンテナンスの時です。IIJ IoTサービスも、ローリングアップデートなど、工夫を凝らして無停止のメンテナンスに努めていますが、それでも、止むを得ずサービスを停止しなければならないメンテナンスが存在します。そうした場合も、例えば転送アプリケーションだけ停止してしまっても、コンポーネント間が疎結合であるため認証アプリケーションは全く影響を受けず、認証アプリケーションはセンサーデータを受け取り続けることができます。つまり、サービスを停止する単位をより小さくすることもでき、同時にお客様への影響をより小さくすることにつながります。
終わりに
この記事では、IIJ IoTサービスで用いている構成パターンであるメッセージングアーキテクチャについて説明しました。メッセージングアーキテクチャはアプリケーション間を疎結合にします。このコンポーネント間の疎結合性は、性能・開発・運用の観点で多くの利益をもたらします。
今回、具体的にどのように実装すればメッセージングアーキテクチャを実現できるのかまでは触れませんでした。IIJ IoTサービスでは、MessageBrokerとしてKafkaを利用し、これと連携するアプリケーションはSpring Cloud StreamやSpring Integrationなどで実装しています。また、この構成を管理するプラットフォームとしてSpring Cloud Data Flowを使っています。(参考スライド: Spring Cloud Data Flow で構成される IIJ IoTサービス)
メッセージングアーキテクチャにもデザインパターンがあります。それがEnterprise Integration Patterns、いわゆるEIPです。
EIPにも大変助けられました。私がイベントドリブンではなくメッセージングと頑なに呼び続ける理由は、私自身このEIPからこのアーキテクチャを学んだからです。例えば IIJ IoTサービスには、データストレージという、ある一まとまりのセンサーデータを一つのファイルにまとめ、IIJのオブジェクトストレージへアップロードする機能がありますが、その設計はEIPのAggregatorパターンを大いに参考にしました。
他にもKafkaを使ったアプリケーションのスケールアウトやPropagateの仕組みなど、お話したいことがまだまだ沢山ありますが、それを書くには余白があまりに狭すぎる。ここまでお読みいただき、ありがとうございました。