NATSについて調べて動かしてみた - サーバー起動からクライアントの接続まで

NATSについて公式のドキュメントを読み解き、実際に触ってみた。

## NATSとは

[NATS](https://nats.io/)は、セキュアでシンプルなハイパフォーマンスメッセージングシステムであると謳っている。
他のメッセージングミドルウェアとしては、KafkaやApache MQ,Rabbit MQ, IBM MQ, Kestrel, NSQなどが挙げられる。
CNCF(Cloud Native Computing Foundation)のプロジェクトとして、[2018/03にホストされた](https://www.linuxfoundation.jp/blog/2018/03/cncf-to-host-nats/)。

## メッセージング

https://docs.nats.io より

### メッセージングの重要性

分散システムの中で通信するアプリケーションやサービスの開発は複雑で困難が伴うものである。とはいえ、基本的なパターンは、リクエスト/リプライ型やRPC型か、イベントやデータのストリーミングの2つに大別される。
モダンなテクノロジーは以下のような特徴が容易に実現できるべきである。

- スケーラブルである
- セキュアである
- ロケーションに左右されない
- 可観測である

### 今日の分散コンピューティングのニーズ

モダンなメッセージングシステムでは以下の特性が求められる。NATSはこのような要件を満たすメッセージングシステムである。

- マイクロサービスやエッジプラットフォーム、デバイスがセキュアにコミュニケーションを取ることができる
- 単一の分散コミュニケーションテクノロジーでセキュアなマルチテナンシーを実現
- ロケーションアドレッシングやディスカバリーが透過である
- システム全体の健全性を重視したレジリエンシーを持つ
- アジャイル開発、CI/CDやオペレーション、スケールが容易である
- ロードバランシングや動的なオートスケールといった高いスケーラビリティを高パフォーマンスで実現
- エッジデバイスからバックエンドシステムまで一貫したセキュリティ


## NATS

### NATSの特徴
NATSの特徴としては以下が挙げられる。

- NATSサーバーのクラスタリングが可能
- 様々なメッセージングモデルが実現可能
- At-Most-Once-deliveryのメッセージング
- メッセージが送信された時に、受信者がそのsubjectをlistenしていなかったりした場合は、その受信者にはメッセージが送られない。

### Subject

メッセージはsubject(他のメッセージングシステムでのQueueやTopicに相当)に対して送信され、受信者はsubjectに対してsubscribeを行うことでメッセージを受信する。
subjectは階層構造を持っており、`.`区切りで表現される。
e.g.`time.us`, `time.us.east`, `time.us.east.atlanta` `time.eu.east`

subjectのサブスクライブにはワイルドカード`*`, `>`が使用可能
`*`は単一のトークンに対するワイルドカードで、`time.*.east`のような使い方をする。
上記のsubjectの例で言うと、`time.us.east`と`time.eu.east`の2つをサブスクライブする。
`>`は一つ以上のトークンに対するワイルドカードで、`time.us.>`のような使い方をする。
この場合は`time.us.east`, `time.us.east.atlanta`がマッチする。`*`では`time.us.east.atlanta`はマッチしない点に注意が必要。

### メッセージングモデル

メッセージングモデルとしては以下がある。

- [Pub/Sub](https://docs.nats.io/nats-concepts/pubsub)
    - 1:NまたはM:Nでのメッセージのやり取りを行う
- [Request-Reply](https://docs.nats.io/nats-concepts/reqreply)
    - 送信者がリプライを受け取るsubject名と共にメッセージを送信、受信側はそのsubjectに対してリプライメッセージを送信する
    - 受信側は動的にキューグループを形成してスケールアップが可能
- [Queue Groups](https://docs.nats.io/nats-concepts/queue)
    - 受信者がキュー名を登録、同一キュー名を持つクライアントでキューグループを形成する
    - メッセージが到着すると、グループ内のメンバーの1つがランダムで選択され、そのクライアントにメッセージが送信される
    - サーバー側の構成は一切不要

#### Acknowledgements

NATSはAt-Most-Onceのサービスレベルなので、メッセージのロストが発生する場合がある。request-replyであれば、タイムアウトを設定することで、応答がない場合に再送するなどの処理が可能だが、一方向型の場合には送達確認する術がない。
NATSでは、ACK機能により一方向型でも送達確認を取ることが可能。
実際には受信側がメッセージを受け取ると、空のメッセージを送信側に返す(Request-Reply)ことでACKを実現している。

## NATSサーバーを立ててメッセージの送受信をする

### NATSサーバー

コンテナイメージが用意されているのでそれを使う。
ポートの4222番はクライアントの接続用、8222番はマネジメント用のポート

```shell
% docker run -d -it --name nats -p 4222:4222 -p 8222:8222 nats:latest
168be256dcea4645b6032cc0f838b918a850b7c4f34ab30f692d9a53d1355100
% docker logs nats
[1] 2020/01/01 09:36:08.042981 [INF] Starting nats-server version 2.1.2
[1] 2020/01/01 09:36:08.043143 [INF] Git commit [679beda]
[1] 2020/01/01 09:36:08.043350 [INF] Starting http monitor on 0.0.0.0:8222
[1] 2020/01/01 09:36:08.043424 [INF] Listening for client connections on 0.0.0.0:4222
[1] 2020/01/01 09:36:08.043468 [INF] Server id is NAE5YIGCSPEBTY736NIOM362OUDT3TGRBU3EGY4Y7IGOVD6IRTMQ7BSQ
[1] 2020/01/01 09:36:08.043509 [INF] Server is ready
[1] 2020/01/01 09:36:08.043734 [INF] Listening for route connections on 0.0.0.0:6222
```

### NATSクライアント

pythonでクライアントを書いて、メッセージモデル毎に動きを確認していく。

#### 事前準備(環境構築)

https://github.com/nats-io/nats.py 
によると、python 3.5.1以上が前提なので、環境を整備する。

```shell
% pyenv local 3.5.3
% python --version
Python 3.5.3
```

pipでライブラリのインストール

```shell
pip install asyncio-nats-client
```

作成したクライアントは以下のリポジトリに保管しています。

https://github.com/takzok/nats-client

#### Pub/Sub
subscribeするクライアントを4つ起動する。それぞれ以下のsubjectをsubscribeする。

- `nats.pubsub`
- `nats.pubsub.alice`
- `nats.pubsub.*`
- `nats.>`

publishするクライアント側でメッセージの送信先を変えながらpublishする。

```shell
## nats.pubsubへのpublish
% python nats-pub.py nats.pubsub -s nats://localhost:4222
Connected to NATS at localhost:4222...
Connection to NATS is closed.

## nats.pubsub.aliceへのpublish
% python nats-pub.py nats.pubsub.alice -s nats://localhost:4222
Connected to NATS at localhost:4222...
Connection to NATS is closed.

## natsへのpublish
% python nats-pub.py nats -s nats://localhost:4222
Connected to NATS at localhost:4222...
Connection to NATS is closed.
```

それぞれのクライアントのメッセージの受信状況

```shell
## nats.pubsubをsubscribe
## 1件目のメッセージだけを受信
% python nats-sub.py nats.pubsub -s nats://127.0.0.1:4222 
Connected to NATS at 127.0.0.1:4222...
Received a message on 'nats.pubsub ': hello nats world
```

```shell
## nats.pubsub.aliceをsubscribe
## 2件目のメッセージだけを受信
% python nats-sub.py nats.pubsub.alice -s nats://127.0.0.1:4222
Connected to NATS at 127.0.0.1:4222...
Received a message on 'nats.pubsub.alice ': hello nats world
```

```shell
## nats.pubsub.*をsubscribe
## 2件目のメッセージだけを受信
## nats.pubsubへのメッセージは受信されない点に注意
% python nats-sub.py nats.pubsub.\* -s nats://127.0.0.1:4222   
Connected to NATS at 127.0.0.1:4222...
Received a message on 'nats.pubsub.alice ': hello nats world
```

```shell
## nats.>をsubscribe
## 1件目と2件目のメッセージを受信
% python nats-sub.py nats.\> -s nats://localhost:4222
Connected to NATS at localhost:4222...
Received a message on 'nats.pubsub ': hello nats world
Received a message on 'nats.pubsub.alice ': hello nats world
```

#### Request/Reply

```shell
## Reply先は_INBOX.で始まるsubjectとなる
% python nats-req.py
Connected to NATS at 127.0.0.1:4222...
Received a message on 'hello _INBOX.6X2intTSrlHmhsheAIYr1K.6X2intTSrlHmGtheAIYr1K': hello nats world
reply message on '_INBOX.6X2intTSrlHmhsheAIYr1K.6X2intTSrlHmGtheAIYr1K': Reply message: received message is 'hello nats world'
Connection to NATS is closed.
```

#### Queue Group

subscribe時にQueue Groupを指定することで、同一のQueue Groupを指定したクライアント間でメッセージの振り分けを行う。

9件のメッセージを送信して、subscribe側での振り分けの様子を観察する。

```shell
## publisher
% python nats-pub.py nats.qg -m '1st message'
Connected to NATS at 127.0.0.1:4222...
Connection to NATS is closed.
% python nats-pub.py nats.qg -m '2nd message'
Connected to NATS at 127.0.0.1:4222...
Connection to NATS is closed.
% python nats-pub.py nats.qg -m '3rd message'
Connected to NATS at 127.0.0.1:4222...
Connection to NATS is closed.
% python nats-pub.py nats.qg -m '4th message'
Connected to NATS at 127.0.0.1:4222...
Connection to NATS is closed.
% python nats-pub.py nats.qg -m '5th message'
Connected to NATS at 127.0.0.1:4222...
Connection to NATS is closed.
% python nats-pub.py nats.qg -m '6th message'
Connected to NATS at 127.0.0.1:4222...
Connection to NATS is closed.
% python nats-pub.py nats.qg -m '7th message'
Connected to NATS at 127.0.0.1:4222...
Connection to NATS is closed.
% python nats-pub.py nats.qg -m '8th message'
Connected to NATS at 127.0.0.1:4222...
Connection to NATS is closed.
% python nats-pub.py nats.qg -m '9th message'
Connected to NATS at 127.0.0.1:4222...
Connection to NATS is closed.
```

均等に分散しなかった。単純なラウンドロビンではないらしい。

```shell
## subscriber-1
% python nats-qg.py nats.qg -q queue-group-1
Connected to NATS at 127.0.0.1:4222...
Received a message on 'nats.qg ': 1st message
Received a message on 'nats.qg ': 4th message
Received a message on 'nats.qg ': 7th message
Received a message on 'nats.qg ': 8th message
```

```shell
## subscriber-2
% python nats-qg.py nats.qg -q queue-group-1
Connected to NATS at 127.0.0.1:4222...
Received a message on 'nats.qg ': 5th message
Received a message on 'nats.qg ': 9th message
```

```shell
## subscriber-3
% python nats-qg.py nats.qg -q queue-group-1
Connected to NATS at 127.0.0.1:4222...
Received a message on 'nats.qg ': 2nd message
Received a message on 'nats.qg ': 3rd message
Received a message on 'nats.qg ': 6th message
```

## さいごに
NATSの基本的なところを抑えてみた。
NATSサーバーのクラスタリングやTLS周り、モニタリング、NATS Streaming、クライアントの自動切断など、見ていくところはたくさんあるので、別エントリで書いてみようと思う。