Node.jsで ioredis を用いて Redis Clusterを取り扱う 

最近でNode.jsで Redis Clusterを触る機会があったのですが
意外とRedis Cluster × Node.jsの日本語記事が無かったため、備忘録を兼ねて記事に起こしておきます。


今回利用するパッケージはこちら。
[GitHub - luin/ioredis: 🚀A robust, performance-focused and full-featured Redis client for Node.js.](https://github.com/luin/ioredis)

typescriptを使う場合は 一緒に型もimportしておきましょう。

```bash
npm i ioredis
npm i -D @types/ioredis
```

## Constructor

cluster形式の場合は利用する nodeを配列形式で指定する必要があります。
他に何もOptionを指定する必要が無いのであれば、基本この設定だけで良いです。

```typescript
const driver: IORedis.Cluster = new IORedis.Cluster([
   { port: 6380, host: '127.0.0.1'},
   { port: 6381, host: '127.0.0.1'},
]);
```

### redisOptions
おそらく一番触る必要が出てくる箇所。
名前の通り Redisに対するオプションを設定します。
例えばパスワードが必要な場合は以下のように redisOptionで指定することが出来ます。


```typescript
const driver: IORedis.Cluster = new IORedis.Cluster([],
      redisOptions: {
        password: config.password
      }
);
```

設定できるオプションは[API - ioredis / new Redis([port], [host], [options]](https://ioredis.readthedocs.io/en/latest/API/)で確認でき、
今回は個人的に使う機会がありそうな項目を列挙しておきます。

|オプション名|デフォルト値|詳細|
|:---|:---|:---|
|db | 0 | 使用するデータベースインデックス
|password | null | パスワード
|dropBufferSupport | FALSE | バッファサポートの削除を有効化,。巨大な配列の応答を処理するときに有効にするとパフォーマンスが向上するらしい(未検証)
|enableReadyCheck | TRUE | Redisのサーバーステータスを確認してからコマンドを送信する
|connectTimeout | 10000 | 初期接続中にタイムアウトが発生するまでのミリ秒
|tls | null | TLS接続のサポート (https://github.com/luin/ioredis#tls-options)
| readOnly | FALSE | 読み取り専用


### その他のオプション
redisOption以外にも幾つか指定できるオプションがあるため、こちらも記載。
こちらも同様に[API - ioredis / new Cluster(startupNodes, options) ](https://ioredis.readthedocs.io/en/latest/API/)に設定項目が載っています。

一応 使いそうなものをピックアップしています。

|オプション名|デフォルト値|詳細|
|:---|:---|:---|
clusterRetryStrategy |  | ノードに到達できない場合に呼び出されるcallback。   returnした数値分待機して再接続を試みる
scaleReads | master | 読み取り対象のnodeを指定する。指定可能なのは   master, slave, all のいずれか |   |  
maxRedirections | 16 | ターゲット nodeからエラー(MOVED,   CLUSTERDOWN等)が返った場合に他のnodeにリダイレクトする回数
retryDelayOnFailover | 100 | ターゲット nodeが切断されている場合に指定秒後にコマンドを再送信する |   |  

```typescript
const driver: IORedis.Cluster = new IORedis.Cluster([],
      redisOptions: {},
      retryDelayOnFailover: 50
);
```

ただ、redisOptionsと違って こちらを指定するケースは殆どなさそうです。


## Command
基本Commandを中心に。
特に難しくありませんが、writeに関しては書き込む際の型に注意が必要です。

### read

```typescript 
await redis.get('key');
```

### write

```typescript
await redis.set('key', 'value');
```

expire を指定する場合は 以下のように EX と 有効期限(秒)を指定します。

```typescript 
redis.set('key', 'value', 'EX', 10);
```

注意点としては Object型をvalueとしてセットすると `[Object object]`という形で保存されてしまうため、
Object型を保存するときは `JSON.stringify`で文字型に変換して入れるようにしましょう。
 
### delete

```typescript
await this.redis.del('key');
```

### keys
正規表現マッチで取得する。
getでは 未ヒット時は nullが返りますが、こちらは空配列が返ります。

```typescript
await this.redis.keys('regExp');
```

## Event
Redisへの接続状態に合わせてイベントがトリガーされます。
発火されるイベントは以下の通り。

|イベント|詳細
|:---|:---|
connect | Redisサーバーへの接続が確立した時
ready | コマンドを受付が可能な時(enableReadyCheck で trueを確認できた時)
error | 接続中にエラーが発生した時
close | Redisサーバー接続が閉じた時
reconnecting | 再接続が行われた時
end | Redisサーバーとの接続が閉じた時

発火したイベントは以下のようにすることでハンドリングできます。


```typescript
this.redis.on('connect', () => {
  console.log('trigger connect');
});
```


## その他 Tips
自分が実際に対応した細々とした内容です。

### master nodeへの負荷を低減したい
ioredisでは基本的に commandは全て masterへ送信されます。
そのため、 masterの cpu消費が激しくパフォーマンスが低下する問題を引き起こす可能性があります。

そこでコンストラクタのオプション `scaleRead`で read commandの向き先を slaveに変更します。

```typescript
const driver: IORedis.Cluster = new IORedis.Cluster([],
      redisOptions: {},
      scaleReads: 'slave' 
);
```

これで read commandは slaveに向くので masterへの負荷は軽減されます。
ただし、レプリケーション遅延により、master, slave間でデータの差分が発生する可能性があるので注意が必要です。


### writeするデータを圧縮したい
例えば api cacheなどで大量のデータを Redisに書き込む場合に幾つかの問題が発生することが予想されます。

- ネットワーク帯域を圧迫して帯域詰まりを起こす
- レイテンシが悪化する
- Redisの容量を圧迫する 

これらの問題に対する一つの解決策としてデータを圧縮するという方法がありますが、
ioredisはデータを圧縮しないため圧縮機構を自前で用意する必要があります。

圧縮・解凍するライブラリは幾つかありますが 今回は高速が売りの `lz-string` を使っていきます
[GitHub - pieroxy/lz-string: LZ-based compression algorithm for JavaScript](https://github.com/pieroxy/lz-string)


```shell
npm i lz-string
npm i -D @types/lz-string
```

```typescript
import * as IORedis from 'ioredis';
import {compress, decompress} from 'lz-string'

//
// 省略
//

/**
 * 読み込み
 * @param {string} key
 */
async function get(key: string): Promise<string | null> {
  const buf: string | null = await redis.get(key);
  if (buf == null) return null;

  return decompress(buf);
}

/**
 * 書き込み
 * @param {string} key
 * @param {string} value
 * @param {number} expireSec
 */
async function set(key: string, value: string, expireSec: number): Promise<void> {
  await redis.set(key, compress(value), 'EX', expireSec);
}
```


注意点としては 圧縮・解凍はCPUを消費する処理のため、パフォーマンスが低下する可能性があります。
なので導入する場合はCPUの消費を計測してリソースの再調整をする必要があります。

※レイテンシが悪化する問題に対してはデータサイズによっては逆効果だったするため、
計測して判断する必要があります。


## 参考
Redis Cluster自体の仕組みについて (非常にわかりやすかったです)
https://qiita.com/keitatata/items/44678ad472e61a4606c5