Node.jsからJMXを介してCassandraの情報を取得する

Cassandraには[nodetoolユーティリティ](https://docs.datastax.com/ja/cassandra-jajp/3.0/cassandra/tools/toolsNodetool.html)というJMXを介してCassandraクラスタを管理するツールが付属しています。
nodetoolで取得できる情報をNode.js上で利用したいという事情があり、その方法を探していたところ[`node-jmx`](https://github.com/zuazo/node-jmx)というJMXと通信するためのパッケージを発見しました。
`node-jmx`を利用してCassandraの情報を取得する方法についてわかったことをまとめます。

## node-jmx について

### インストール

`node-jmx`の利用にはJDKが必要です。`node-jmx`は内部で[`node-java`](https://github.com/joeferner/node-java)を利用しているようですので、install時のトラブルは[`node-java`のInstallation](https://github.com/joeferner/node-java#installation)を参照したほうが良いでしょう。

node-gypを利用しているので、python2.7とmake, g++も予め用意しておく必要があります。

準備が整っていれば以下のコマンドで`node-jmx`が利用可能になります。

```
npm instal jmx
```

### 使い方

基本は[公式ドキュメント](https://github.com/zuazo/node-jmx#documentation)通りですが、Clientから返ってくる値はJavaオブジェクトになっているようですので実際の値を取得する際には前処理が必要になります。
以下に`HeapMemoryUsage`を取得するコードを載せました。`HeapMemoryUsage`は`javax.management.openmbean.CompositeData`インタフェースのオブジェクトですので、`get(String key)`で取得したい項目の数値を取得できます。ただし、`node-java`を介した操作ですのでメソッド名はSyncを付けた`getSync(String key)`になります。（ちなみに`node-java`は非同期メソッドも提供しているようです。）取得した値は`java.lang.Number`インタフェースのオブジェクトですので、`longValue()`等で数値が取得できます。（こちらはSyncを付けないようです。）

```javascript
const jmx = require('jmx');

const client = jmx.createClient({
    host: 'localhost',
    port: 7199,
});

client.on('connect', () => {
    client.getAttribute('java.lang:type=Memory', 'HeapMemoryUsage', data => {
        const used = data.getSync('used');
        console.log(used.longValue);
    });
});
client.connect();
```

## nodetool status をNode.js上で実装する

本題のCassandraクラスタの情報取得の方法についてまとめます。
手始めに`nodetool status`が提供している機能を`node-jmx`を利用して実装してみたいと思います。

### nodetool statusとは

クラスタに参加している各ノードの情報を収集、表示するコマンドです。（ [参考](https://docs.datastax.com/ja/cassandra-jajp/3.0/cassandra/tools/toolsStatus.html) ）
特に、ノードが機能しているかどうかを示すStatus (Up/Down) の項目は`nodetool status`を実行する際に最も関心のある情報の一つではないかと思われます。

### 本家 nodetoolのstatusコマンドの実装

GitHubのものですが、本家Apache Cassandraのnodetool statusに関する主なソースコードを纏めておきます。

- [org/apache/cassandra/tools/nodetool/Status.java](https://github.com/apache/cassandra/blob/cassandra-3.11/src/java/org/apache/cassandra/tools/nodetool/Status.java)
- [org/apache/cassandra/tools/NodeProbe.java](https://github.com/apache/cassandra/blob/cassandra-3.11/src/java/org/apache/cassandra/tools/NodeProbe.java)
- [org/apache/cassandra/service/StorageService.java](https://github.com/apache/cassandra/blob/028b6772231e18e2357dfff1fa04477efa377730/src/java/org/apache/cassandra/service/StorageService.java)
- [org/apache/cassandra/locator/EndpointSnitchInfo.java](https://github.com/apache/cassandra/blob/f22fec927de7ac291266660c2f34de5b8cc1c695/src/java/org/apache/cassandra/locator/EndpointSnitchInfo.java)

（だいたいこの辺りだったと思いますが、過不足間違い等あったらすみません）

大まかな内容としては、`nodetool status`で収集している情報はStorageServiceとEndpointSnitchInfoが管理しており、それぞれMBeanとして公開されています。

それぞれ以下のような情報を管理しています（ごく一部だけ載せてあります）

- StorageService:
  - クラスタに参加しているノードのIPアドレスとHostIDのMap
  - 健全(Up)なノード、疎通不可(Down)になっているノードのリスト
  - クラスタに参加しようとしている or 抜けようとしているノードのリスト
  - トークンのリストと担当ノードのIPアドレスのMap
 
 など

- EndpointSnitchInfo:
  - ノードが所属しているデータセンタ名
  - ノードが所属しているラック名
 
`nodetool status` ではこれらの情報を適当なフォーマットで表示してくれます。

余談ですが本家nodetoolの実装は少々雑と思わざるを得ない部分もあります。
例えばvnodeやトークンレンジの数が増えるほど`resolveIp`オプション適用時の`nodetool status`の応答時間が悪化する問題があり、その原因はDNSによる名前解決の際に`トークンレンジ * vnode数`だけのループを行う非効率な処理が行われているためのようです。（実際ほんの数ノードのクラスタでも`nodetool status`はめちゃくちゃ遅い）
今回のNode.jsでの実装はもうちょっと効率の良い実装にできたら良いなと考えています。

( 参考: [ASF JIRA: improvement of nodetool status -r](https://issues.apache.org/jira/browse/CASSANDRA-14847) ) ← 忘れ去られているように見えますが果たして反映されるのでしょうか…

### Node.jsでnodetool statusを実装してみた

いきなりですが実装したものをnpm packageとして作成し、公開してみました。

[npmjs: cassandra-nodetool]( https://www.npmjs.com/package/cassandra-nodetool )
[GitHub: cassandra-nodetool]( https://github.com/Aoinu/cassandra-nodetool )

#### エッセンス

`node-jmx`でStorageServiceやEndpointSnitchInfoを取得する際のMBean名は以下のとおりです。

- StorageService: `'org.apache.cassandra.db:type=StorageService'`
- EndpointSnitchInfo: `'org.apache.cassandra.db:type=EndpointSnitchInfo'`

ですので、例えばクラスタに参加しているノードのリストを取得するには以下のようなコードになります。
クラスタに参加しているノードのリストは`StorageService`の`LiveNodes`属性にありますので、これを取得しています。

```javascript
// client.on('connect') イベントを受け取ったあとで
client.getAttribute('org.apache.cassandra.db:type=StorageService', 'LiveNodes', javaLiveNodes => {
    const liveNodes = javaLiveNodes.toArraySync();
    console.log(liveNodes);
});
```

Unreachableノード（疎通不可ノード）のリストを取得するには`UnreachableNodes`属性を取得すればOKです。
上でも少し触れましたがStorageServiceには他にも様々な情報を管理しており、属性の一覧を知りたい際にはソースコードを参照するかjconsoleで接続して直接探してみるのが良いかと思われます。

EndpointSnitchInfoからデータセンター名を取得する例も載せておきます。
EndpointSnitchInfoには`Rack`や`Datacenter`という属性があり、そちらでもデータセンター名やラック名を取得することが可能です。
ただしこちらから取得できるのは、JMXで接続したノードのデータセンター名やラック名のみです。
クラスタに参加している他のノードのラック情報等を取得したい際は`getRack`, `getDatacenter`メソッドを利用します。

```javascript
const endpoint = '127.0.0.1';
client.invoke('org.apache.cassandra.db:type=EndpointSnitchInfo', 'getDatacenter', [endpoint], javaDCName => {
    console.log(javaDCName.toString());
});
```


最後に、StorageServiceの`EndpointToHostId`属性などのような`java.util.Map`オブジェクトをJavaScript Objectとして扱えるようにパースする処理が度々必要になりましたので、そのイディオムのようなものも載せておきたいと思います。(もっと良い方法があったら教えてください！）

```javascript
const convertedObject = {};
const entrySets = javaMap.entrySetSync().toArraySync();
for(const entry of entrySets) {
    convertedObject[entry.getKeySync()] = entry.getValueSync();
}
```


## 所感

`node-jmx`と`node-java`がこの先もメンテナンスされていくのかは若干不安はありますが、これらのパッケージの利用でNode.jsからJMXを介した処理がとても簡単に実装できました。
当初child_processで本家nodetoolをシェルコマンドとして実行して結果をパースする方法を考えておりましたが、JMXを介して直接情報を取得する方法が分かりましたので、Cassandraの運用ツール開発にJava以外の選択肢が作れそうです。
本家nodetoolにはJSON/YAMLフォーマット出力に対応していないコマンドがあったり、上でも触れたとおりパフォーマンスや信頼性に満足できないところがあったりしたので、しばらくはNode.js向けの代替モジュールとして前述のパッケージ開発を続けていきたいと思います。

## 参考

[nodetoolユーティリティ]( https://docs.datastax.com/ja/cassandra-jajp/3.0/cassandra/tools/toolsNodetool.html )
[nodetool status]( https://docs.datastax.com/ja/cassandra-jajp/3.0/cassandra/tools/toolsStatus.html )
[GitHub: Apache Cassandra]( https://github.com/apache/cassandra )
[ASF JIRA Cassandra: improvement of nodetool status -r]( https://issues.apache.org/jira/browse/CASSANDRA-14847 )
[GitHub: node-jmx]( https://github.com/zuazo/node-jmx )
[GitHub: node-java]( https://github.com/joeferner/node-java )
[GitHub: cassandra-nodetool]( https://github.com/Aoinu/cassandra-nodetool )