最近でNode.jsで Redis Clusterを触る機会があったのですが
意外とRedis Cluster × Node.jsの日本語記事が無かったため、備忘録を兼ねて記事に起こしておきます。
今回利用するパッケージはこちら。
GitHub - luin/ioredis: 🚀A robust, performance-focused and full-featured Redis client for Node.js.
typescriptを使う場合は 一緒に型もimportしておきましょう。
npm i ioredis
npm i -D @types/ioredis
Constructor
cluster形式の場合は利用する nodeを配列形式で指定する必要があります。
他に何もOptionを指定する必要が無いのであれば、基本この設定だけで良いです。
constdriver:IORedis.Cluster=newIORedis.Cluster([{port:6380,host:'127.0.0.1'},{port:6381,host:'127.0.0.1'},]);
redisOptions
おそらく一番触る必要が出てくる箇所。
名前の通り Redisに対するオプションを設定します。
例えばパスワードが必要な場合は以下のように redisOptionで指定することが出来ます。
constdriver:IORedis.Cluster=newIORedis.Cluster([],redisOptions:{password:config.password});
設定できるオプションはAPI - ioredis / new Redis([port], [host], [options]で確認でき、
今回は個人的に使う機会がありそうな項目を列挙しておきます。
オプション名 | デフォルト値 | 詳細 |
---|---|---|
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) に設定項目が載っています。
一応 使いそうなものをピックアップしています。
オプション名 | デフォルト値 | 詳細 |
---|---|---|
clusterRetryStrategy | ノードに到達できない場合に呼び出されるcallback。 returnした数値分待機して再接続を試みる | |
scaleReads | master | 読み取り対象のnodeを指定する。指定可能なのは master, slave, all のいずれか |
maxRedirections | 16 | ターゲット nodeからエラー(MOVED, CLUSTERDOWN等)が返った場合に他のnodeにリダイレクトする回数 |
retryDelayOnFailover | 100 | ターゲット nodeが切断されている場合に指定秒後にコマンドを再送信する |
constdriver:IORedis.Cluster=newIORedis.Cluster([],redisOptions:{},retryDelayOnFailover:50);
ただ、redisOptionsと違って こちらを指定するケースは殆どなさそうです。
Command
基本Commandを中心に。
特に難しくありませんが、writeに関しては書き込む際の型に注意が必要です。
read
awaitredis.get('key');
write
awaitredis.set('key','value');
expire を指定する場合は 以下のように EX と 有効期限(秒)を指定します。
redis.set('key','value','EX',10);
注意点としては Object型をvalueとしてセットすると [Object object]
という形で保存されてしまうため、
Object型を保存するときは JSON.stringify
で文字型に変換して入れるようにしましょう。
delete
awaitthis.redis.del('key');
keys
正規表現マッチで取得する。
getでは 未ヒット時は nullが返りますが、こちらは空配列が返ります。
awaitthis.redis.keys('regExp');
Event
Redisへの接続状態に合わせてイベントがトリガーされます。
発火されるイベントは以下の通り。
イベント | 詳細 |
---|---|
connect | Redisサーバーへの接続が確立した時 |
ready | コマンドを受付が可能な時(enableReadyCheck で trueを確認できた時) |
error | 接続中にエラーが発生した時 |
close | Redisサーバー接続が閉じた時 |
reconnecting | 再接続が行われた時 |
end | Redisサーバーとの接続が閉じた時 |
発火したイベントは以下のようにすることでハンドリングできます。
this.redis.on('connect',()=>{console.log('trigger connect');});
その他 Tips
自分が実際に対応した細々とした内容です。
master nodeへの負荷を低減したい
ioredisでは基本的に commandは全て masterへ送信されます。
そのため、 masterの cpu消費が激しくパフォーマンスが低下する問題を引き起こす可能性があります。
そこでコンストラクタのオプション scaleRead
で read commandの向き先を slaveに変更します。
constdriver:IORedis.Cluster=newIORedis.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
npm i lz-string
npm i -D @types/lz-string
import*asIORedisfrom'ioredis';import{compress,decompress}from'lz-string'//// 省略///**
* 読み込み
* @param {string} key
*/asyncfunctionget(key:string):Promise<string|null>{constbuf:string|null=awaitredis.get(key);if(buf==null)returnnull;returndecompress(buf);}/**
* 書き込み
* @param {string} key
* @param {string} value
* @param {number} expireSec
*/asyncfunctionset(key:string,value:string,expireSec:number):Promise<void>{awaitredis.set(key,compress(value),'EX',expireSec);}
注意点としては 圧縮・解凍はCPUを消費する処理のため、パフォーマンスが低下する可能性があります。
なので導入する場合はCPUの消費を計測してリソースの再調整をする必要があります。
※レイテンシが悪化する問題に対してはデータサイズによっては逆効果だったするため、
計測して判断する必要があります。
おまけ
別段必要なさそうですが簡単に class化しておきました。
import*asIORedisfrom'ioredis';/**
* Redis操作クラス
* @class
*/exportclassDriver{privateredis:IORedis.Redis|IORedis.Cluster;privatestatus:ConnectionStatus=ConnectionStatus.READY;/**
* @constructor
* @param {IORedis.Redis} client
*/privateconstructor(client:IORedis.Redis|IORedis.Cluster){this.redis=client;this.redis.on('connect',()=>{this.status=ConnectionStatus.CONNECTED});this.redis.on('error',()=>{this.status=ConnectionStatus.ERROR});this.redis.on('reconnecting',()=>{this.status=ConnectionStatus.RECONNECT});}/**
* 死活判定
*/publicalive():boolean{return(this.status===ConnectionStatus.CONNECTED)}/**
* 読み込み
* @param {string} key
*/publicasyncget(key:string):Promise<string|null>{if(!this.alive())returnnull;returnawaitthis.redis.get(key);}/**
* 削除
* @param {string} key
*/publicasyncdel(key:string):Promise<void>{if(!this.alive())return;awaitthis.redis.del(key);}/**
* 書き込み
* @param {string} key
* @param {string} value
* @param {number} expireSec
*/publicasyncset(key:string,value:string,expireSec:number):Promise<void>{if(!this.alive())return;awaitthis.redis.set(key,value,'EX',expireSec);}/**
* 正規表現読み込み
* @param {string }prefix
*/publicasynckeys(prefix:string):Promise<string[]|[]>{returnawaitthis.redis.keys(prefix);}/**
* Creater
* @param {ConnectionConfig} config
*/publicstaticcreate(config:ConnectionConfig):Driver{returnnewDriver(newIORedis.Cluster(config.nodes,{// この辺の設定は環境に合わせて指定redisOptions:{}}));}}/**
* 接続設定インターフェイス
* @interface
*/interfaceConnectionConfig{nodes:Array<{port:number,host:string}>}/**
* コネクション状態
* @enum
*/enumConnectionStatus{READY,CONNECTED,ERROR,RECONNECT}
参考
Redis Cluster自体の仕組みについて (非常にわかりやすかったです)
https://qiita.com/keitatata/items/44678ad472e61a4606c5