Ruby から Redis Cluster を使うための gem を作ってみた


概要

もう半年くらい前のことになりますが、ElastiCache で Redis の 3.2 系がサポートされ、Redis Cluster が使えるようになりました (https://aws.amazon.com/jp/blogs/news/amazon-elasticache-for-redis-update-sharded-clusters-engine-improvements-and-more/ )。これで書き込みを大量にするケースでもスケールアウトできるし、最大で 3.5TiB のメモリ上にデータをのせることができるようになりました。

早速 Ruby で使ってみようと思ったら Ruby 用のまともな Redis Cluster の gem が見つからなかったので作ってみました。この記事では簡単に使い方を紹介してみようかと思います。

Renoir

redis-cluster という gem 名がすでに取られていたので、Memcached のそこそこ有名な gem である dalli を微妙に意識しつつ renoir という名前にしました。

Gem: https://rubygems.org/gems/renoir
GitHub: https://github.com/saidie/renoir

Renoir がやってくれること

例えば、gem install renoir した後、こんな感じで使えます。

require 'renoir'

rc = Renoir::Client.new(cluster_nodes: ['127.0.0.1:30001'])

p rc.set('hoge', 123, nx: true)
p rc.zrange('fuga', 0, -1, with_scores: true)

127.0.0.1:30001 がクラスタの一つのノードのアドレスです。クラスタは例えば http://qiita.com/uryyyyyyy/items/fc767f7f41144e5f10a1 に載っている create-cluster スクリプトを使うと簡単に立ち上げられます。

上記コードでは hoge というキーに値をセットし、fuga というキーの sorted-sets から全要素とスコアを取得しています。シングルインスタンスの Redis にこれらのコマンドを投げると、普通にそのインスタンスがコマンドを処理してレスポンスを返してくるわけですが、Redis Cluster の場合はキー毎にそれを担当する Redis のインスタンス (ノード) が異なっているので、コマンドを投げるべきノードを同定する必要があります。間違ったノードにコマンドを投げると、正しいノードへのリダイレクト指示 (MOVED レスポンス) が返ってきます。また、ノード追加に伴う resharding などでキーをノード間で移行したりすることがあるのですが、この最中はあるキーを見つけるために移行先と移行元の両方のノードに問い合わせる必要があったりします。

で、Renoir はこれら

  • キー (正確にはキースロット) とノードの割当情報の管理
  • リダイレクトのハンドリング
  • 移行中のキーのハンドリング

をよしなにやってくれるというわけです。

redis gem との互換性

個人的に Ruby から Redis を使う場合は redis gem をよく使っており、同じように使えるようにしたいなと思ったので、Renoir::Client::Redis と「できるだけ」互換性のあるインターフェースを提供するという設計方針で実装しました。また、内部的なコネクションとしても ::Redis インスタンスを使っています。というわけで、redis と同様にドライバとして hiredis を使ったりもできます。redis gem は割とメジャーだと思うので、学習コストほぼなく使えるのではないかと思います。

ちなみに、このあたりはアダプタ (Renoir::ConnectionAdapters:: 系) で吸収しているので、例えば redic 向けのアダプタを提供することもできます (そのうち実装するかも?)

注意点としては、::Redis の全てのメソッドが同じように使えるわけではない、ということです。すなわち、コマンドのキーからノードを特定できない場合は、コマンドの送り先が不定なので Renoir::Client はエラーを返します。例えば、

  • MSET, MGET などで複数キーを指定して、それらのキースロットが異なる場合
  • KEYS, INFO などそもそもキーがないコマンド
    • ただし、KEYS に関してはパターンにキーハッシュタグが明示的に含まれてたら良しとするかもしれないです

などです。後者のケースは、Renoir::Client#each_node が全てのノードの ::Redis インスタンスを返すので、それを使って置き換えることはできます。

実際に使う時

gem をテストしている時に遭遇したいくつかの例外を考慮して、以下のような感じでコードを書くのが良さそうです。

rc = Renoir::Client.new(cluster_nodes: ['127.0.0.1:30001'])
begin
  rc.set('hoge', 123, nx: true)
rescue Redis::CannotConnectError
  # そもそもノードに繋がらないケース。そのうち直るかもしれないのでリトライしてみるといいかも
rescue Redis::ConnectionError, Redis::TimeoutError
  # 繋がったんだけど途中で切れたケース。コマンドが既に届いてる可能性があるからリトライしない方がいいかも
rescue Redis::CommandError => e
  if /CLUSTERDOWN/.match(e.message)
    # failover 中などクラスタの状態が不完全な時に返ってくる。そのうち直るはずなのでリトライすると良さげ
  end
  raise
rescue Renoir::RedirectionError
  # ノード間のクラスタ情報の差異が大きいと起こる可能性があるけど低確率だと思う
  # クラスタに存在しなかったキーのスロットが移行中で起きる(たぶん)ケースが多いはず
  # 数回リトライしてもいいかも
end

まとめ

redis gem と同じような手触りで使うことのできる Redis Cluster 用の gem の紹介でした。より詳しくはドキュメント http://www.rubydoc.info/gems/renoir とかを見ていただければと思います。

ちなみに、このコードは Redis の作者である antirez 氏のリファレンス実装 https://github.com/antirez/redis-rb-cluster をベースにしていろいろ整理して gem にしたものになっています。