概要
インフラストラクチャーグループの春日です。
Ruby で Redis を扱う際は redis gem がよく使われてきました。 しかし色々と課題が出てきたため、それらを解決すべくRedisのバージョン6以上に対応した redis-client gem が作られた話をします。
Gem | 説明 | メンテナー |
---|---|---|
redis | 長年使われ続けてきたもの | Jean Boussier さん |
redis-client | 新しく作成されたもの | Jean Boussier さん |
redis-cluster-client | 新しく作成されたもの (クラスター対応版) | 春日 |
旧redis gemの課題
増え続けるコマンドのサポート負荷
Redisサーバーには日々の開発で様々な新機能が実装され続けており、クライアント側も追従し続けなければなりません。 新しいデータタイプやコマンドも増え続けています。
現行の redis gem の 設計 だとコマンドごとにメソッドを増やし続けなければならず、 Redisサーバー側のバージョンUPタイミングでよく Issue が起票される傾向にありました。
しかし本来であればRedisのコマンドはシンプルな文字列配列でしかなく、 多少の扱い易さや可読性のためだけにRubyのメソッドで読み替えをする実装は、あまり保守性が高くありませんでした。
Gemを使う側も コマンドマニュアル を読むだけでなく、Rubyメソッド呼び出しの学習コストを追加で払う必要がありました。
RESP2しか対応できていない
Redisはバージョン6以降で新しいプロトコルの RESP3 を追加していますが、現行の redis gem はサポートできていません。
今までの RESP2 では基本的に文字列や数値、配列程度しか表現できておらず、 クライアント側でコマンドごとに返信データを扱い易いように独自に変換する必要がありました。
しかしRESP3ではデータタイプの表現力がよりリッチになっており、 クライアント側ではプロトコルに従ってMapなどの様々なデータタイプにメンテナブルに変換できるようになりました。
ここでRESP2とRESP3の違いを確認するため telnet を用いて Hashタイプのコマンド を実行してみます。 まずは HSET コマンドでデータを登録しておきます。
$ docker run --rm -p 6379:6379 redis $ telnet 127.0.0.1 6379 hset myhash field1 1 field2 2 :2
既に出てきてしまいましたが、上記のHSETコマンド実行に対する返信 :2
の意味がプロトコルで決まっており、数値の2を表現しています。
HSETで登録したフィールドの数が返却値です。
HSETの返信ではRESP2とRESP3とで違いは見られないので次に進みます。
RESP2で HGETALL してみます。
hgetall myhash *4 $6 field1 $1 1 $6 field2 $1 2
この返信は以下を表現しています。
行 | 意味 |
---|---|
*4 |
4つの要素を持つ配列が次に来る |
$6 |
サイズが6の文字列が次に来る |
field1 |
文字列の実際の値 |
$1 |
サイズが1の文字列が次に来る |
1 |
文字列の実際の値 |
$6 |
サイズが6の文字列が次に来る |
field2 |
文字列の実際の値 |
$1 |
サイズが1の文字列が次に来る |
2 |
文字列の実際の値 |
今度はRESP3でHGETALLしてみます。 なおプロトコルの切り替えは HELLO コマンドで指定できますが、ここでは割愛します。
hgetall myhash %2 $6 field1 $1 1 $6 field2 $1 2
最初の記号が変わりました。以下にまとめます。
行 | 意味 |
---|---|
%2 |
2つのフィールドを持つMapが次に来る |
$6 |
サイズが6の文字列が次に来る |
field1 |
文字列の実際の値 |
$1 |
サイズが1の文字列が次に来る |
1 |
文字列の実際の値 |
$6 |
サイズが6の文字列が次に来る |
field2 |
文字列の実際の値 |
$1 |
サイズが1の文字列が次に来る |
2 |
文字列の実際の値 |
HGETALLの返信はRESP2では配列扱いでしたが、RESP3ではMapの表現に変わっています。 これはクライアントを実装する側からすると結構な違いを意味します。
RESP2では一度配列として読み込んだ後にMapに変換する必要が出てきます。 つまりコマンドごとの返信データの最終的な型をクライアント側で事前に把握している必要があるのです。
# RESP2で配列をMapに変換するイメージ ['field1', '1', 'field2', '2'].each_slice(2).to_h #=> {"field1"=>"1", "field2"=>"2"}
しかしRESP3では最初からMapとして読み込むことができるのです。 返信データの読み込み処理内で返り値の生成が完了するため、コマンドごとに特別な変換処理を実装する必要はありません。
RESP2のみ対応している現行の redis gem の 実装 ではコマンドごとにメソッドがあり、そのメソッドごとに必要に応じて返り値を加工しています。
RESP3に対応するために、これらの加工処理周りの実装を修正していくのは骨が折れそうです。
新redis-client / redis-cluster-client gemsによる解決
新規コマンド対応の柔軟化
新しい redis-client gem では主に #call
メソッドだけを用意してシンプルにコマンド配列を渡すインターフェースを採用し、
新規コマンドにも柔軟に対応できるように実装されています。
# 旧redis gem r = Redis.new r.set('foo', 1)
# 新redis-client gem r = RedisClient.new r.call('SET', 'foo', 1)
これでGemを使う側も コマンドマニュアル を見るだけで良くなり、余計な学習コストを払わずに済むようになりました。
RESP3のサポート
Redisのバージョン5以前のサポートを切ることでRESP2のことを考えなくて良くなり、RESP3に従って返り値を変換して返すシンプルな実装を実現できました。
この対応により、返り値に関してもコマンド引数と同じくGemを使う側に余計な学習コストは発生しません。
その他の背景
redis-rbとしてGitHubのOrganizationを新規に作成した理由
redis gem はGitHubの redis org に所属していて公式サポートの安心感があります。
しかし2022年7月現在 redis gem リポジトリにおいてアクティブなメンテナーは1人しかおらず、GitHub上の権限設定の関係で柔軟にメンテナーを増減させることができませんでした。
そこで新たに redis-rb org を作成して実装することになりました。
新redis-client gemと新redis-cluster-client gemとを分けた理由
redis gem ではRedisの全機能 (Standalone, Sentinel, Cluster) をこれ1つで扱えるよう実装されていますが、 redis-client gem ではクラスター用とそれ以外とでGemとGitリポジトリが分割されています。
Redis Cluster は V2のissue にて大幅な進化を遂げることが予想されます。 その中の1つに Request proxying 機能があり、こちらが実現されるとそもそもクライアント側でクラスター用のロジックを実装する必要がなくなります。
また、現行のクラスター対応はコマンドのキーを基にキーが格納されているノードに正確にコマンドを送信しなければならない点でクライアント側の責務が重く、 実機を使った場合のテストも時間がかかったり不定期に落ちるテストケースが多かったりするため、今のうちから分割管理する方針になりました。
なお RubyのRedis Client LibraryをCluster Modeに対応させた話 から時間が経過していますが、その間に多くのバグが直されました。 今回新生した redis-cluster-client gem ではテストケースを拡充し、さらに多くのバグが修正されています。
旧redis gemの今後
redis gem は Rails も含めて様々な箇所で使われており、 完全にメンテナンスを止めるのは難しいことが予想されます。 次のメジャーバージョンUP時にはできる限りの互換性を保ちつつ、内部的には redis-client gem が使われる形に修正されそうです。
アプリケーションから直接Redisを扱う場合において、今後は redis-client gem の利用をおすすめします。 ただしRedisバージョン6以上が対象です。既に sidekiq version 7 では使われています。
最後に
以上、簡単ではありますが新しい redis-client gem のご紹介でした。最後までお読みいただきありがとうございます。
リブセンスでは技術投資10%ルールがあり、業務時間の10%ほどを業務以外の開発に充てることができます。 redis-cluster-client gem は弊社の技術投資枠とプライベート時間を使って実装しました。
インフラストラクチャーグループでは HCL や YAML に限らず Go や Ruby が好きな方も募集しています。