KafkaからNiFiでconsumeしてWebSocketでZeppelinに渡してD3でリアルタイムなグラフを描画する

[Hortonworksアドベントカレンダー](http://qiita.com/advent-calendar/2016/hortonworks) 12/21 の記事です。

[12/3 NiFi 1.1.0でWebSocketに対応](http://qiita.com/ijokarumawak@github/items/b77c7fb28660c3aacc37)に続き、今回もWebSocketネタで。

Zeppelin、NiFi、Kafka、D3と色々学べるお得なチュートリアル形式となっております。

先日、ある知人から、「NiFiのWebSocketを使って、Zeppelinにリアルタイムなグラフが表示できたらCoolじゃない?」という意見をいただいたのがきっかけでした。

まず、完成図のイメージから。

![リアルタイムグラフ描画](https://gist.githubusercontent.com/ijokarumawak/60b9ab2038ef906731ebf4c0eee97176/raw/6deae9d4932f03daaf94a4024ad79cd3157ff7c7/80-How-it-works.png)

[Zeppelin](https://zeppelin.apache.org/)(左上)は複数のグラフをノートブックという形でまとめて、可視化したデータをメンバ間で共有したり、分析するのに便利なツールです。

今回はZeppelinに図のような円グラフを表示します。円グラフに表示するデータはNiFiからWebSocket経由でPushします。

せっかくサーバサイドからPushできるので、さらにNiFiからKafkaのメッセージを受信するようにしてみましょう。SparkやStormなどのストリーム分析結果をKafkaにパブリッシュしてZeppelinにリアルタイムで表示するような利用シーンを想定しています。

お試しいただくには、Zeppelin、NiFi、KafkaとWebブラウザが必要です。それぞれのインストールと起動はコマンドを数発叩くだけなので非常に簡単です。

## 環境構築

### Zeppelinのインストールと起動

[Zeppelinのダウンロードページ](https://zeppelin.apache.org/download.html)からBinary Packageをダウンロードして適当なところに展開しましょう。以下のコマンドを実行すると、8080ポートでZeppelinのUIにアクセスできます:

```bash
$ cd zeppelin-0.6.2-bin-all
$ ./bin/zeppelin-daemon.sh start
```

### Kafkaのインストールと起動

[KafkaのQuickStart](https://kafka.apache.org/quickstart#quickstart_download)に記載の通り、ダウンロードして適当な場所に展開し、次のコマンドで、まずZookeeperを起動します:

```bash
$ bin/zookeeper-server-start.sh config/zookeeper.properties
```

つづいて別のターミナルを開き、Kafkaサーバを起動しましょう:

```bash
$ bin/kafka-server-start.sh config/server.properties
```

また別のターミナルを開いてトピックを作りましょう:

```bash

$ bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test

# ちゃんと作成できたか確認
$ bin/kafka-topics.sh --list --zookeeper localhost:2181
test
```

QuickStartに記載のコマンドでターミナルからメッセージの送受信ができるか確認しときましょう。

### NiFiのインストールと起動

[NiFiのダウンロードページ](https://nifi.apache.org/download.html)からバイナリをダウンロードして、適当な場所に展開します。

展開したディレクトリの中に設定ファイルがあるので、少し変更しましょう。ZeppelinもNiFiもデフォルトのHTTPリスンポートが8080なので、NiFiのポートを変更します:

```bash
$ vi conf/nifi.properties

# web properties #
nifi.web.war.directory=./lib
nifi.web.http.host=
nifi.web.http.port=8081 // 8080以外に変える
```

その後、NiFiを起動します:

```bash
$ ./bin/nifi.sh start
# 起動には少々時間がかかるので、ログを見ときましょう
$ tail -f logs/nifi-app.log
(次のログが表示されれば、起動が完了した合図です)
2016-12-20 14:58:48,166 INFO [main] org.apache.nifi.web.server.JettyServer NiFi has started. The UI is available at the following URLs:
2016-12-20 14:58:48,166 INFO [main] org.apache.nifi.web.server.JettyServer http://127.0.0.1:8081/nifi
```

ブラウザからlocalhost:8081でNiFiのUIにアクセスします。さて、NiFiのフローを構築していきましょう!

## NiFiのフローを作成する

### まずはデータが必要、Kafka -> NiFiの流れ

これから、わりと複雑なデータフローを組んでいきます。最初から全部設計できてなくてもねんどをこねるように、動かしながらだんだんとフローを構築できるのがNiFiの良いところです。Kafkaのトピックが用意できているので、そこからデータを取得する部分から始めてみましょう。

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/46439d05-b984-c2ef-c4a6-b59ef68a1827.png)

画面上部に並んでいるアイコンの一番左にある、Processorのアイコンを白いキャンバスにドラッグ&ドロップします。

検索窓にkafkaと入力すると、いろいろ出てきます。Kafkaのバージョンごとに対応するProcessorがあります:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/e572782a-84c9-22a9-aaaf-e330f641c95b.png)

|Kafka|プロセッサ|
|-----|---------|
|0.8.x|GetKafka, PutKafka|
|0.9.x|ConsumeKafka, PublishKafka|
|0.10.x|ConsumeKafka_0_10, PublishKafka_0_10|

勢いよくバージョンアップしていくKafkaに追いつくためにNiFiをうまく使うのも良いですね。NiFiでは複数のKafkaバージョンに対応するデータフローを作ることができます。

さて、今回利用するのはKafka 0.10.xなのでConsumeKafka_0_10をダブルクリックします。

おめでとうございます！これでNiFiにプロセッサが追加できましたね:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/ea72e178-a701-4050-2f0c-817ab7059752.png)

ワーニングのアイコンにマウスをのせると、Topic Name、Group ID、Relationship Successが未設定ですよと教えてくれます。プロセッサを右クリックして`Configure`をクリックします:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/ec5dad66-222b-56f5-6bc3-e530c65efcc2.png)

`PROPERTIES`タブにある、`Topic Name(s)`のValue列をクリックして、先程作成したトピックの名前`test`を、`Group ID`にはConsumer Groupの名前を入力します。何でも良いです。ここでは`nifi`としました:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/c3d879c7-9f23-fa8e-3e16-5a658ebbf1ee.png)

設定できたら`APPLY`をクリックしてダイアログを閉じます。

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/11c44373-b8c2-3911-b390-991041f78858.png)

残る警告は`Relationship success`が無いよ、というものです。Kafkaから受信したメッセージの転送先が未設定ということですね。何か作成しましょう。

こんな時に便利なのが、UpdateAttributeプロセッサです。UpdateAttributeプロセッサを追加しましょう。

そして、ConsumeKafka_0_10の真ん中あたりにマウスをのせると、丸い矢印が表示されます、それをドラッグして、UpdateAttributeプロセッサにドロップします。

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/e1534a41-a4cf-8dfd-2680-f8dbab56fd1b.png)

すると、`Create Connection`ダイアログが表示されます。`Add`をクリックしてコネクションを作成しましょう。

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/fb060773-41b8-462d-1841-313f23a98c3b.png)

これで、`success` Relationshipのコネクションが作成できました。ConsumeKafka_0_10の設定が完了したので、警告アイコンが消え、停止状態となっています:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/16818a48-246d-b9f5-2572-16f774ae5a3a.png)

UpdateAttributeには警告が出ていますが、そのままで構いません。ConsumeKafka_0_10を起動してみましょう！

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/986658dd-f742-3e16-3afb-cd060d6e3d28.png)

プロセッサが再生中のアイコンに変わり、時折右上の方に数字の`1`が見えます。これはアクティブなスレッド数を表しています。NiFiでは同一のフローの中にある複数のプロセッサが並列で動きます:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/2fdd1e7c-875f-5774-db74-645616165134.png)

### Kafkaトピックにメッセージを送る

NiFi側の準備は整いました、Kafkaへとメッセージを送信しましょう。コンソールから次のコマンドを入力します:

```bash
$ bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test
(何かテキストを入力してEnterすると送信される)
Hello Kafka.
```

このコマンドは次の入力を待つ状態で起動しっぱなしにしておきましょう。

NiFiのUIに戻ってみると、`success`に`Queued 1`と表示されています！何か受信したようですね:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/e5dd061f-948e-c46e-0ba1-24b6df511793.png)

コネクションを右クリックして、`List queue`を選択します。

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/1ecd35a8-0711-06c0-b177-d4e8f77f3a08.png)

すると、`success`に滞留しているメッセージが確認できます。NiFiではこのデータをFlowFileと呼びます:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/3bd2293a-9ef8-15d0-530a-357f476ae292.png)

左端のインフォメーションアイコンをクリックすると、FlowFileの情報が色々確認できます。

#### ミニクイズ

コンソールから送信した文字列はNiFiのUIから確認できます。どこで閲覧できるかわかりますか? UIを色々探索してみましょう :)

## NiFiからWebSocketでデータをPushする

Kafka -> NiFiのルートが開拓され、冒頭の絵に描いた餅の右下部分ができました！次は取得したメッセージをWebSocketでクライアントにPushする部分を作りましょう。

### 話を単純にする、まずは単一クライアントで

いきなり難しいフローを作成するのは大変なものです。まずは簡略化したフローでプロトタイプを行いましょう。

WebSocketは複数のクライアントを扱うことができます。NiFiのWebSocketコンポーネントは各クライアントのセッションをIDで管理しています。複数クライアントを相手にするには多少工夫が必要なので、後回しにして、単一のクライアントと対話できるフローにしましょう。

## NiFiをWebSocketサーバにする

[以前の投稿](http://qiita.com/ijokarumawak@github/items/b77c7fb28660c3aacc37)でNiFiのWebSocket実装について詳しく書きましたので、こちらも参照してください。

WebSocket周りのフローをまとめるために、Process Groupを作ってみましょう。

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/53739eee-e611-bbe9-7c3b-5794eee6a877.png)

Process Groupアイコンをキャンバスに配置します。適当に名前をつけましょう:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/e3137b38-fb7d-b07a-5c46-b8021a253be0.png)

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/f527b97b-b459-e2e6-00c0-61e9b9e2fbe9.png)

作成したProcess Groupをダブルクリックすると、中に入れます。ここにWebSocketサーバの実装を組んでいきます。

### ListenWebSocketとJettyWebSocketServer

`ListenWebSocket`プロセッサを配置して、ConfigureからPROPERTIESタブを表示します。WebSocket Server ControllerServiceから`Create new service`を選択します:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/c21f53ec-d5ca-671e-e652-9ac9ebea8e4f.png)

`JettyWebSocketServer`を作成し、`Server URL Path`には`/realtime-data`と指定します:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/a5a47c67-4264-0b63-7828-a9045cd1445a.png)

`APPLY`してプロセッサの設定は完了です。

もう一度ListenWebSocketのPROPERTIESを開き、`JettyWebSocketServer`の隣に表示されている矢印をクリック、Editアイコンをクリックして、ControllerServiceの設定に遷移します。

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/c1bb74be-b15a-1074-518c-ffbb47aa0d48.png)

Listen Portに9091と設定し、APPLYで設定を反映させます。そして稲妻アイコンをクリックしてこのControllerServiceを有効化しましょう！

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/e5b9a10e-b3da-fe5b-3dab-cef4aac00dbe.png)

もう一度、ListenWebSocketのConfigureから、今度は`SETTINGS`タブを表示します。`binary message`と`text message`は今回受信しても利用しないので、自動的に終了するようにします。

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/5dc45eb2-bcf4-fda5-3e92-dde89be32c2e.png)

ConsumeKafkaと同じように、`connected`の先にはUpdateAttributeを仮置きして、ListenWebSocketを起動しましょう。

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/ed1b8a4b-4e3a-504d-b8c4-c9ebdcc9a6bf.png)

これで、WebSocketクライアントからの接続を受け付ける準備が整いました！

### websocket.orgのエコーテスト

websocket.orgの[Echo Test](http://www.websocket.org/echo.html)を使って、接続確認を行います。

`Location`にはNiFiへとつながるURLを入力します。`Connect`して、Logに`CONNECTED`と表示されればOKです:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/f35798ad-d9dc-c8a7-d6c3-32418610ae23.png)

NiFi側では`connected`にクライアントが接続してきたイベントがFlowFileとして生成されているはずです。List queuesから中身を確認すると、次のAttributesが設定されています:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/6d98ca63-fa30-9660-0cda-bd350258fd47.png)

これらの情報があれば、接続しているWebSocketクライアントへとメッセージを返すことができます。
この情報をNiFiのキャッシュに保持しておきましょう。

### WebSocketセッションIDをDistributedCacheに格納する

さぁ、ここからはNiFi標準で使える`DistributedCache`という仕組みを利用してみましょう。これはNiFi上で動くオンメモリのKVSみたいなものです。もっと簡単に言ってしまうと、連想配列です。任意のキーで任意の値を保存でき、フローの色々な場所から共有できます。

まずは`DistributedCache`サーバを立てましょう。UIの左側にある`Operate`パレットの設定アイコンをクリックすると、ControllerServiceの一覧画面が表示されます:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/bde80bc4-518f-dd87-3825-4540785821f6.png)

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/7d0d9c96-d96c-8a97-86ae-87a075103187.png)

ここには、先程作成したJettyWebSocketServerが表示されていますね。プラスアイコンをクリックして、`DistributedMapCacheServer`を追加しましょう:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/94b2874c-1bd5-a22f-0079-1d87e4124705.png)

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/00f42888-b18b-6c6d-a985-d3d84fd949ce.png)

`DistributedMapCacheServer`の設定はデフォルトのままで大丈夫です。続いてこのキャッシュサーバに接続するクライアントの`DistributedMapCacheClient`を追加します。`Server Hostname`には`localhost`と設定しましょう:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/a6e5fdef-3fe1-795b-c555-fd762d4adfd2.png)

作成したキャッシュサーバとクライアントのサービスを有効化しておきましょう:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/12d743b5-aa53-b5aa-4279-e33090c2d940.png)

キャッシュサーバの準備ができたのでデータを格納しましょう。WebSocketクライアントが接続した時に生成されたセッションIDを捕獲します。キャッシュサーバにはFlowFileのコンテンツ部分が格納されるので、`websocket.session.id`のAttribute値を、コンテンツ部分に抽出する必要があります。

これには`ReplaceText`プロセッサを使います。このプロセッサは、テキスト形式のFlowFileのコンテンツ内文字列を置換するのに利用できます。`Replacement Strategy`に`Always Replace`を、`Replacement Value`に`${websocket.session.id}`と設定します。これはNiFiエクスプレッションの式で、FlowFileのAttributeの値を返します。詳細はまたの機会に。。この設定で、該当のAttribute値でFlowFileコンテンツの値を置換します。

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/743e6530-2ddc-09f4-7479-db016f5a3830.png)

`PutDistributedMapCache`プロセッサを追加して`ReplaceText`の出力をつなぎましょう。キャッシュのキーは固定値で`websocket.session.id`としました。先程作成した`DistributedMapCacheClientService`も指定します:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/04410801-551b-7e09-2146-bde822a5a13d.png)

長くなりましたが、ここまで手順にそって進めていくと、次の図のようなフローになっていると思います。`ListenWebSocket`の`connected`の先端にある青いポイントをドラッグして、接続先を`ReplaceText`に移しましょう。こんなことができるのもNiFiならではですね!

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/3e057f77-96c2-b46d-f041-cf818036d9c6.png)

`ReplaceText`と`PutDistributedMapCache`の`failure` relationshipはAuto Terminateしています。出来上がったフローはこの通り:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/78ace7b9-8b14-df39-d8d1-097d1207e0f9.png)

これで生成されたWebSocketのセッションIDがキャッシュに格納できました！

### WebSocketクライアントにKafkaから取得したメッセージをPushする

さあ、WebSocketクライアントにメッセージをPushしてみましょう！使うプロセッサは`PutWebSocket`です。設定を見てみましょう、デフォルトでは次のようになっています:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/096b7d9c-5883-3a92-cf56-9b4b716eb2b4.png)

Session Id, ControllerService Id, Endpoint Idはそれぞれ、渡ってきたFlowFileから解決するようになっています。これは複数のクライアントを相手にする場合に効果を発揮しますが、今回は単一クライアントでのプロトタイプなので、固定値を設定してしまいましょう。

ControllerService一覧から`JettyWebSocketServer`のインフォメーションアイコンをクリックして、SETTINGSタブにあるIdをコピーします:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/6c7fcdc7-d8a4-aa5e-b265-da4988fd21c0.png)


![image](https://qiita-image-store.s3.amazonaws.com/0/20515/04a8a836-ce29-e899-491f-031be27e0dca.png)

コピーしたControllerServiceのIdを`PutWebSocket`プロセッサの`WebSocket ControllerService Id`に設定します。`WebSocket Endpoint Id`は`ListenWebSocket`プロセッサが待ち受けているPathを指定すれば良いので、`/realtime-data`としましょう:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/72eba3c2-bcde-d0b4-4f56-09122625ec1a.png)

あとは、`WebSocket Session Id`があれば、WebSocketの接続相手にメッセージを返すことができます。

セッションIDはDistributedCacheに保存したのでしたね。取り出すために、`FetchDistributedMapCache`を配置します。`Cache Entry Identifier`には固定値で`websocket.session.id`を指定しましょう。`Distributed Cache Service`にはセッションIDを格納するときにも利用したControllerServiceを再利用できます。

キャッシュから取得したセッションIDを、`websocket.session.id`というAttributeに保存するために、`Put Cache Value in Attribute`を設定します。
これで、Kafkaから受信したメッセージをFlowFileのコンテンツに残したまま、さらにWebSocketのセッションIDをAttributeに付加することができます。

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/3168aaa0-c1a6-f221-c05a-1da2affa3745.png)

これまで解説したプロセッサ群は`WebSocket Server`というProcessor Group内で定義していました。ConsumeKafkaはルートProcess Groupにあります。Process Group間でFlow Fileをやりとりするために、`Input Port`を先頭に追加しましょう。渡ってきたFlow FileをクライアントにPushするので、Input Portの名前は`push`としました。

`push`、`FetchDistributedMapCache`、`PutWebSocket`をスタートしておきましょう:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/a523e514-6dc5-c30a-1e1b-253a588a721b.png)

それでは、ルートProcess Group、`NiFi Flow`に戻って、`ConsumeKafka_0_10`から、`WebSocket Server`へと、フローをつなげます:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/1e26962f-099e-3f61-e240-508c99c740b6.png)

では、Kafkaにコンソールからconsole-producerでメッセージを送信してみましょう。同時にEcho TestでメッセージがWebSocket経由で受信できるかも確認します:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/0a64e758-c77e-65ea-234b-e889eab17b9d.png)

おお！つながりましたね！

## Zeppelinにグラフを表示する

いよいよ、Zeppelinを使う時がきました！ZeppelinのUIから、新しいノートブックを作成しましょう。名前は何でも良いです:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/4ed103c1-61a8-a868-c634-508a6fc19d4f.png)

試しに、Zeppelinで円グラフを表示してみましょう。下図のようなコードを入力し、スタートすると、円グラフが表示されます:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/b57f37a8-4807-10e7-e5b3-fcaf9b39e817.png)

これはkey, valueを持つ二次元の表形式のデータをZeppelinに渡しているコードです。すると、Zeppelinではテーブルや棒、円グラフなどで表示することができるのですね。

今回、WebSocketで受信したデータをこのような形で渡せればよかったのですが、方法がわかりませんでした。とりあえず`%angular`インタプリタを使えば出力を自在にカスタマイズできるので、この手法でいきます。誰かもっと簡単な方法をご存じの方は教えてください。

### Angularインタプリタで任意のJavascriptを実行

Zeppelinでは、インタプリタが多数用意されていて、先頭の`%angular`のように指定すると、記述するコードを実行するエンジンを切替えることができます。

今回はAngularを使ってJavascriptのコードを書いていきましょう。といってもAngular自体はあんまり使いませんが。。

試しに、以下のコードを実行してみましょう。`script`タグに実行するJavascriptコードを、出力結果を表示するための`div`を用意して実行すると、下図のようになります:

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/72b6d4b1-de1d-1300-a58f-372ef9703e74.png)

### D3で円グラフを描画する

さて、それではD3を使って円グラフを描画してみましょう。結構長いコードですが、以下をZeppelinにコピペしてみてください。`graphId`は同じノートブック内に複数のパラグラフを作成する場合に重複しないように連番をふってます:

```html+jinja

%angular

<script>
var graphId = '#realtime-graph-2';
var w = 600;
var h = 400;
var r = Math.min(w, h) / 2;
var color = d3.scale.category20c();

var svg = d3.select(graphId).append('svg')
            .attr('width', w)
            .attr('height', h)
            .append('g')
            .attr('transform', 'translate(' + r + ',' + r + ')');
            
var pie = d3.layout.pie()
            .sort(null)
            .value(function(d) {return d.value});

var arc = d3.svg.arc()
            .outerRadius(r);


var updatePiChart = function(dataPoints) {
    
    var arcs = svg.selectAll('.slice')
        .data(pie(dataPoints));
        

    // Add new slices if needed.
    var g = arcs.enter()
        .append('g')
        .attr('class', 'slice');
    g.append('path');
    g.append('text')
        .attr('text-anchor', 'middle');

    // Update existing and added slices.
    arcs.select('path')
        .attr('fill', function(d, i){
            return color(i);
        })
        .attr('d', function(d) {
            return arc(d);
        });
        
    arcs.select('text')
        .attr('transform', function(d) {
            d.innerRadius = 0;
            d.outerRadius = r;
            return 'translate(' + arc.centroid(d) + ")";
        })
        .text(function(d){
            return d.data.label;
        });
    
    // Remove exitted slices.
    arcs.exit().remove();
}

</script>

<div id="realtime-graph-2">
</div>
```

D3の部分、ややこしいので解説をしたいのですが、またの機会に。。`updatePiChart`関数に表示するデータを渡すと、円グラフを描画するようにしました。Zeppelinでこのコードを実行すると、`updatePiChart`関数が作成されるので、WebブラウザのDevToolから試しに実行してみます。

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/e63388d3-c5a1-3108-1e12-a3466d5ef03f.png)

`dataPoints`は次のような配列です。`label`と`value`を持ったオブジェクトを格納しています:

```json
[{"label": "a", "value": 1},
 {"label": "b", "value": 2},
 {"label": "c", "value": 3}]
```

うまくグラフが表示されましたか? dataPointsの中身を変えて、`updatePiChart`を実行すると、円グラフが更新されると思います。

ここにWebSocketから受信したデータを流し込んでやればOKですね！

### WebSocketでNiFiと接続する

先程のコードに、WebSocketでNiFiと接続するコードを追加します。必要なコードは以下のようになります:

```html+jinja
%angular

<script>
var graphId = '#realtime-graph-2';

// ...

var updatePiChart = function(dataPoints) {
 // ...
}


// ここから追加
var wsUri = "ws://localhost:9091/realtime-data";

websocket = new WebSocket(wsUri);
websocket.onopen = function(evt) {console.log('connected')};
websocket.onerror = function(evt) {console.log('ERR', evt)};
websocket.onmessage = function(evt) {
    console.log(evt)
    var dataPoints = JSON.parse(evt.data);
    updatePiChart(dataPoints);
};
</script>

<div id="realtime-graph-2">
</div>
```

## Kafka -> NiFi -> (WebSocket) -> Zeppelin -> D3!!

これですべての準備が整いました！Zeppelinでパラグラフを実行してWebSocketでNiFiに接続します。

その後、console-publisherを利用してKafkaにdataPointsのJSON文字列を送信してみましょう。

NiFiがWebSocketサーバとなり、見事にリアルタイムグラフ更新ができました！

![image](https://qiita-image-store.s3.amazonaws.com/0/20515/6c739be1-9929-4769-08ac-048f5dca8780.png)

## 複数のWebSocketクライアントに対応する

今回の記事では、話を簡単にするために、単一クライアントのみに対応すれば良いことにしました。WebSocketセッションIDをDistributedCacheに格納してKafkaからメッセージを受信した際に利用しました。

これだと、別のクライアントがWebSocketで接続した際にキャッシュ内のWebSocketセッションIDが更新されてしまい、複数のクライアントでは利用できません。

一つの解として、NiFiのフローで次のようなループを実装する手があります:

![](https://gist.githubusercontent.com/ijokarumawak/60b9ab2038ef906731ebf4c0eee97176/raw/3326855274c3525b0f214327f587ec27ed7f1f32/91-NiFi-WebSocket.png)

WebSocketクライアントが接続すると、このループをぐるぐると回ります。Kafkaから受信したJSONデータを受信時のタイムスタンプと共に、DistributedCacheに格納しておきます。

フローをループするFlow Fileには、WebSocketのセッションID、ControllerService Id、Endpoint Id、そして、最終Pushタイムスタンプを持たせます。

次にキャッシュを確認したときに前回よりも新しい情報であれば、WebSocketクライアントにPushし、そうでなければ何もせずループを継続します。

このようにフローを構築すれば、クライアントが接続してくる度にFlow Fileが作成され、クライアント毎の情報はそのFlow File内に保持されるため、複数のクライアントに対応することができます。

複数クライアント対応版のNiFiテンプレートファイルは、[Realtime Data Visualization with Zeppelin via NiFi WebSocket](https://gist.github.com/ijokarumawak/60b9ab2038ef906731ebf4c0eee97176)のGistで公開しておきました。ご興味のある方はダウンロードしてNiFiにインポートして中身を探ってみてください。

## まとめ

今回のチュートリアルでは、色々なOSSを組み合わせてリアルタイムのグラフを表示する仕組みを構築してみました。

NiFiをデータ収集ツールとして使うだけでなく、NiFiから何かを発信することもできてしまいます。しかもサーバ側では一切コーディングをせず、NiFiフローの構築だけでここまでできました。

年末年始、何か新しいもの触ってみたいなー、という方は是非お試しくださいませ！