技術部インフラグループの春日です。
Redis Clusterモード対応版のRuby用ライブラリである redis-cluster-client gemをほそぼそとメンテしております。
2024年11月現在でまだ600万ダウンロード程度のマイナーライブラリではありますが、 利用者が増えてくるにつれて不具合報告も何件か出てきており、 これまで業務の10%技術投資時間やプライベート時間を利用して修正してきました。
本記事ではクライアントライブラリの実装面における考慮漏れの反省も兼ねて、 Redis Clusterモードならではの考慮ポイントを紹介しつつissue対応を振り返ります。
- Ruby用Redisライブラリの変遷
- Redisの構成について
- Clusterの状態変化への追従
- Clusterの状態情報を取得するコマンドの負荷
- ClusterモードにおけるPipelining
- ClusterモードにおけるTransaction
- ClusterモードにおけるPub/Sub
- ClusterモードにおけるMGET, MSET and DEL
- ClusterモードにおけるSCAN
- Clusterモードのクライアントライブラリ開発におけるCIの充実
- Valkeyについての余談
- 最後に
Ruby用Redisライブラリの変遷
Redis Clusterモードの話に移る前に自分がメンテしているgemの立ち位置に関して背景を共有させてください。
RubyでRedisを扱う際のクライアントライブラリには古くから redis gemが存在します。 初期の頃ではRedis作者もメンテに加わっていたみたいです。
このGemは 4.*.*
系までシングル、Sentinel、Clusterモードをそれ1つでサポートしていました。
しかし 5.*.*
系からClusterモードのクライアントが redis-clustering gemに分離され、
さらに内部実装が以下のgemsに切り離されました。
- redis-client
- redis-cluster-client
- 本記事の対象gem
最終的な依存関係は以下です。
graph TB redis-->redis-client redis-clustering-->redis-cluster-client-->redis-client
redis
や redis-clustering
gemsの実装はRESP2依存が強く、
後続の redis-client
や redis-cluster-client
gemsはRESP3に主眼を置いて開発されました。
そして後者のgemsはオプションで指定すればRESP2でも動作するよう作られているため、前者のgemsから利用できています。
https://redis.io/docs/latest/develop/reference/protocol-spec/
余談ですが redis-clustering
というネーミングは redis-cluster
がすでに取られていたため仕方なくそうなりました。
Redisの構成について
RedisのClusterモードを説明するにあたり、まずはそれ以外の基本構成について再確認します。
最初はローカル開発環境に多いシングルのスタンドアローンモードでしょうか。
graph TB client[Client] redis[(Redis)] client--read/write-->redis
この構成だとSPOFなので次はレプリケーションして冗長化してみます。
graph TB client[Client] primary[(Primary Redis)] replica[(Replica Redis)] client--read/write-->primary replica--replicaof-->primary
マシにはなりましたが、これだとフェイルオーバー対応ができていません。そこでSentinelの登場です。
https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/
graph TB client[Client] sentinel[Sentinel] primary[(Primary Redis)] replica[(Replica Redis)] client--inquiry-->sentinel client--read/write-->primary replica--replicaof-->primary sentinel--monitor-->primary sentinel--monitor-->replica
実際にはSentinelは最低でも3台は必要になりそうです。
クライアントはSentinelにはあくまでレプリケーション状態を問い合わせるだけで、 実際はその情報を元に末端のノードに直接接続しに行く必要があります。 また、SentinelでなくてもHAProxyやKeepalivedなどでhookスクリプトを書けばフェイルオーバー対応もできそうです。
この構成で冗長化は実現できましたが、データ量が増えてくるとスケールアップの限界を迎えてしまいます。 そこでシャーディングができるClusterモードの登場です。
https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/
graph TB client[Client] node1[(Node 1)] node2[(Node 2)] node3[(Node 3)] client--read/write-->node1 client--read/write-->node2 client--read/write-->node3 node1--monitor-->node2 node2--monitor-->node1 node1--monitor-->node3 node3--monitor-->node1 node2--monitor-->node3 node3--monitor-->node2
上記の例だと3台しかいませんが、実際はそれぞれにレプリカが付いて台数はもっと増えるでしょう。 また、Primaryノードの負荷軽減目的でReplicaから参照することも設定次第で可能です。
https://redis.io/docs/latest/commands/readonly/
Redis Clusterモードでは各ノードにスロットが割り当てられており、キーはどれかのスロットに所属しています。
スロットは 2^14
個存在し、以下の計算でキーからスロット番号を導出します。
HASH_SLOT = CRC16(key) mod 16384
クライアント側が指定したキーが存在しないノードにコマンドを送信するとサーバー側はリダイレクトエラーを返します。
$ redis-cli get key1 (error) MOVED 9189 10.10.1.6:6379
上記の例では key1
が所属しているスロット 9189
は 10.10.1.6:6379
のノードにあると教えてくれています。
クライアントライブラリ側はこのリダイレクトを処理できるようにするだけで最低限のClusterモードのサポートができます。 しかし2往復する無駄があるので、実際の賢いクライアントライブラリはコマンドをどのノードに送信すべきか事前に知っていないといけません。
もちろんRedisのCluster機能を使わなくともtwemproxyなどのプロキシを介したり、 クライアントサイドでConsistent Hashing実装したりすることにより、 外からシャーディングを実現することも可能です。
前置きはこれくらいにして次の節からRedis Clusterモードにおける考慮ポイントや、
それに伴う redis-cluster-client
gem側のissue対応を紹介していきます。
Clusterの状態変化への追従
Redis Clusterモードの処理全般でまず考慮しないといけないのがCluster側の状態変化に追従しなければならない点です。 これは基本的にクライアントライブラリ側で対処するためユーザー側は気にしなくて良い部分です。
例えばCluster側では以下のような変化が発生します。
- フェイルオーバー
- リシャーディング
- スケールアウト
- スケールイン
これらの変化で発生するリダイレクトエラーに対応しつつ、コネクションエラーなども例外処理して接続先情報を更新する必要があります。 ライブラリやプロキシによっては別スレッドで一定間隔で取得して更新し続けるような実装もあるみたいです。
一部のノードが故障するなどの何らかの状態変化が発生した後にアプリケーション側でエラーが出続けるような事態は避けなければなりません。 毎回アプリケーションを再起動しなければならなくなる運用は地獄ですね。
Clusterの状態情報を取得するコマンドの負荷
前の節でRedis ClusterモードにおけるクライアントライブラリはCluster側の状態変化に追従し続ける必要がある旨を説明しました。
RedisにはClusterの状態情報を取得できるコマンドが何個か存在します。
これらのコマンドを叩くと以下の情報が得られます。
- Cluster内の全ノードのリスト (内部ID、IPアドレス、ポート番号など)
- どのノードにどのスロット番号が割り当てられているか
- ノードごとのロール情報 (primary or replica)
- ノードごとの生存情報、各種フラグ
2024年現在では CLUSTER SHARDS
の利用が推奨されていて CLUSTER SLOTS
は非推奨になっています。
なお redis-cluster-client
gemは CLUSTER NODES
を使っており、将来的には CLUSTER SHARDS
に変えたいと考えています。
Redisのドキュメントにはコマンドそれぞれに計算量オーダーが明記されており、 これらのCluster状態情報を取得するコマンドは全てスロークエリ扱いです。
redis-cluster-client
gemのちょっと前までの実装ではCluster状態が変化した後の例外処理の中で、
素朴に CLUSTER NODES
コマンドを発行して最新のCluster状態情報を取得しようとする実装にしていました。
しかしアプリケーションサーバーを何台も動かしているような大規模サービスだと一気に問い合わせが集中してRedisサーバーの負荷が急上昇してしまいます。 このissueはGitLabのスケーラビリティ担当エンジニアの方からご報告いただきました。
Clusterの状態変化の収束には数秒から1分くらいかかる場合があります。 なのでエラーの度にスロークエリコマンドで取得しに行くのではなく、ランダムにズラしながら最低でも数秒ごとの発行になるよう減らしました。 ロジックとしては素朴なものです。
また、CLUSTER NODES
コマンドを発行する対象のノード数を絞れるようにする設定も追加しました。
ただしこれはサーバー側の負荷軽減とサーバーから得るCluster状態情報の信頼性とのトレードオフな側面があります。
個人的にまだエレガントなやり方が見つけられていない部分です。
ClusterモードにおけるPipelining
Pipeliningは1個ずつコマンドを送信しては返信を受け取るのを繰り返すのではなく、 ある程度まとめて送って結果も一括で返してもらう問い合わせ方式で、 往復回数が減るのでパフォーマンスの改善が見込めます。
https://redis.io/docs/latest/develop/use/pipelining/
ユーザーはいちいちどのノードにどのPipelineを送信すれば良いか意識したくはないので、 Redis Clusterモードのクライアントライブラリには以下の処理が求められます。
- ユーザーから渡されたPipelineを分割して対象ノードごとにまとめる
- 分割したPipelineを各ノードに送信して返信を得る
- 各ノードからの返信を統合してユーザーに返す、ただしリダイレクトエラーが発生した場合は対象キーを取得し直してから
例えばユーザーから以下のPipelineを渡された場合に、
- GET key0
- GET key1
- GET key2
- GET key3
- GET key4
- GET key5
- GET key6
- GET key7
- GET key8
- GET key9
クライアントライブラリ内部では以下のように分解して発行され、
- Node 1
- GET key1
- GET key3
- GET key4
- Node 2
- GET key0
- GET key2
- GET key6
- GET key9
- Node 3
- GET key5
- GET key7
- GET key8
最終的に返信が統合されてユーザーには1台の場合と同じように返却される感じです。
- value0
- value1
- value2
- value3
- value4
- value5
- value6
- value7
- value8
- value9
これは初期の頃から実装していましたが、マルチスレッド処理におけるrace conditionの考慮漏れが一部ありました。 RubyにはGlobal VM Lockがあって救われていた部分もありました。 Zendeskエンジニアの方から修正Pull Requestをいただき直りました。
ClusterモードにおけるTransaction
Redis ClusterモードでもTransaction機能を使えますが、更新対象のキーを単一のスロットに限定するという制約があります。
- https://redis.io/docs/latest/develop/interact/transactions/
- https://redis.io/blog/redis-clustering-best-practices-with-keys/
Redis Clusterモードにはハッシュタグ機能があり、キーに特殊な囲いを施すことでスロットを偏らせることができます。
例えば以下のキーは全て同じスロットに作用し、スロット算出計算には user001
が使われます。
{user001}:foo
{user001}:bar
{user001}:baz
Redis ClusterモードでTransactionを実行するには必然的にこのハッシュタグ機能を使うことになります。 ただしせっかくのシャーディングが台無しにならないよう、キー設計には注意が必要です。
そしてクライアントライブラリ側では最初に指定されたコマンドのキーを元にTransactionを送信すべきノードを決定します。
キーがあるコマンドが指定されないと MULTI
コマンドをどのノードに送信すれば良いかわからないからです。
また、Redisには楽観的ロック機能があり WATCH
コマンドと組み合わせてTransactionを実行できます。
WATCH中のコネクションでTransactionを実行する前に他のコネクションで対象キーが更新された場合はTransactionは破棄されます。
そしてもちろん、これらのハンドリングに加えてリダイレクト処理も考慮する必要があります。
ハッシュタグを使わないといけない制約は発生するものの、
redis-cluster-client
gemではClusterモードでない単一インスタンス用クライアントと同じ書き味で使えるように実装しています。
cli.call('MSET', '{myslot}1', 'v1', '{myslot}2', 'v2') cli.multi(watch: %w[{myslot}1 {myslot}2]) do |tx| old_key1 = cli.call('GET', '{myslot}1') old_key2 = cli.call('GET', '{myslot}2') tx.call('SET', '{myslot}1', old_key2) tx.call('SET', '{myslot}2', old_key1) end
Zendeskエンジニアの方からissueでご報告いただく前までは redis-cluster-client
gemのTransaction機能はまともに動いていない状態でしたが、
その方のご協力もあって現在では不具合が修正されています。
なおClusterモードの有無に限らずRedisのTransactionはACID特性を満たしている訳ではありません。 ロールバックも実装されておらず、Redisはシンプルさとパフォーマンスに焦点を置いて開発されています。
https://redis.io/docs/latest/develop/interact/transactions/#what-about-rollbacks
Redis does not support rollbacks of transactions since supporting rollbacks would have a significant impact on the simplicity and performance of Redis.
なのでクリティカルセクション内の一部のコマンドが適用されて終わるパターンもあります。
https://redis.io/docs/latest/develop/interact/transactions/#errors-inside-a-transaction
Errors happening after EXEC instead are not handled in a special way: all the other commands will be executed even if some command fails during the transaction. It's important to note that even when a command fails, all the other commands in the queue are processed - Redis will not stop the processing of commands.
$ redis-cli 127.0.0.1:6379> get key3 (nil) 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> set key3 a QUEUED 127.0.0.1:6379(TX)> incr key3 QUEUED 127.0.0.1:6379(TX)> exec 1) OK 2) (error) ERR value is not an integer or out of range 127.0.0.1:6379> get key3 "a"
上記の例だと2つめのコマンドのインクリメントが型不正で失敗してますが、最初のSETには成功しています。 逆に存在しないコマンドを指定したりするエラーパターン (キューイング中にエラーが返る) だとTransactionはちゃんと破棄されます。
$ redis-cli 127.0.0.1:6379> get key3 (nil) 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> set key3 b QUEUED 127.0.0.1:6379(TX)> badcmd key3 c (error) ERR unknown command 'badcmd', with args beginning with: 'key3' 'c' 127.0.0.1:6379(TX)> exec (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6379> get key3 (nil)
余談ですがSidekiq作者のブログに redis-cluster-client
gemに関する言及があり、
もしかしてEnterprise版で使われているのか?と思うと今でも軽く動悸がしてきます。
https://www.mikeperham.com/2023/05/08/scaling-huge-transactional-datasets-with-redis-cluster/
ClusterモードにおけるPub/Sub
RedisのPub/Subは古くからある機能で初期の頃からClusterモードでも利用できました。
https://redis.io/docs/latest/develop/interact/pubsub/
Global Pub/Sub機能だとClusterモードでは全ノードにメッセージが伝搬します。 これはどのノードでsubscribeしてもメッセージを受け取れる便利さがありますが、ネットワーク帯域を消費するデメリットがありました。
そこでClusterモード専用のSharded Pub/Sub機能が登場しました。 これはchannel名でシャーディングされるのでキーと同じスロット計算で担当ノードを決定できます。
例のごとくユーザーはノードの存在を意識したくないので、 Clusterモードのクライアントライブラリは必然的に複数ノードのsubscription状態を管理する必要がでてきます。 Rubyでこれをマルチスレッド実装するにあたり、Goみたいにシンプルに実装できないか悩んでたところ以下の標準ライブラリに出会いました。
- https://docs.ruby-lang.org/ja/latest/class/Thread=3a=3aQueue.html
- https://docs.ruby-lang.org/ja/latest/class/Thread=3a=3aSizedQueue.html
GoのchannelとGoroutineみたいにRubyのQueueとThreadを使ってシンプルに実装できました。 言うまでもなくリダイレクトエラーの対応も必要でした。
ユーザーはノードを意識することなく純粋にchannelを指定してループで受信メッセージを待つだけで良く、 これはClusterモードではない単一インスタンスのRedisクライアントと同じインターフェースです。
とここまで書いたものの、初期の実装ではこのPub/Sub機能もまともに動いておらず、 GitLabのスケーラビリティ担当エンジニアの方からのissue報告で慌てて修正した感じでした。
ClusterモードにおけるMGET, MSET and DEL
複数キーを一度に渡して実行できるコマンドはいくつか存在します。 Pipeliningが縦ならこれらは横のイメージでしょうか。
その中でもメジャーなのが MGET
, MSET
, DEL
あたりです。
Redisをキャッシュストアとして使っているケースでは多用されているコマンドでしょう。
ところがClusterモードだとTransaction機能と同じく単一スロットにしか作用できないという制約が発生します。
$ redis-cli mget key1 key2 key3 (error) CROSSSLOT Keys in request don't hash to the same slot
この場合にTransaction機能と同様にハッシュタグでキーを同一スロットに寄せれば使えるようになります。
$ redis-cli mget {key}1 {key}2 {key}3 1) (nil) 2) (nil) 3) (nil)
しかしもともとClusterモードでないRedisからClusterモードなRedisへ移行する際に面倒になりそうです。
そこで redis-cluster-client
gemでは最初のキーにハッシュタグが含まれている場合はそのまま実行し、
そうでない場合は内部でPipeliningに変換して実行するよう実装しました。
互換性とパフォーマンスの両立を目指した感じですが、 パフォーマンス的には遅くなるので素直にハッシュタグを使った方が良いと個人的に考えます。
ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [x86_64-linux] Warming up -------------------------------------- MGET: original 648.000 i/100ms MGET: emulated 93.000 i/100ms MGET: single_get 29.000 i/100ms Calculating ------------------------------------- MGET: original 6.508k (± 7.2%) i/s (153.66 μs/i) - 32.400k in 5.015468s MGET: emulated 922.818 (± 6.1%) i/s (1.08 ms/i) - 4.650k in 5.062777s MGET: single_get 294.561 (± 7.8%) i/s (3.39 ms/i) - 1.479k in 5.068353s Comparison: MGET: original: 6507.9 i/s MGET: emulated: 922.8 i/s - 7.05x slower MGET: single_get: 294.6 i/s - 22.09x slower
対象 | 説明 |
---|---|
original | 通常の MGET コマンド実行 |
emulated | 内部でPipeliningに変換されて実行した場合 |
single_get | 愚直に1個1個 GET コマンドを実行した場合 |
余談ですがこの対応は以下のRails関連のProposalページに気付いて慌てて実装しました。
https://discuss.rubyonrails.org/t/propsal-redis-cluster-support-in-activesupport-cache/85617
ClusterモードにおけるSCAN
Redisに格納されている全キーを取得したいケースでよく使われるのが SCAN
コマンドです。
https://redis.io/docs/latest/commands/scan/
Redisユーザーであれば KEYS
コマンドは負荷が高いから SCAN
コマンドを使いなさい、と最初に教わるかと思います。
SCAN
コマンドの戻り値は以下の2つです。
- cursor
- keys
ユーザーは返り値のcursorを次の SCAN
コマンドで指定する形で繰り返して全走査できます。
ClusterモードでないRedisの場合は1つのインスタンスをSCANするだけで完結しますが、 Clusterモードの場合はノードを跨いてやる必要があります。
ユーザーはノードを意識したくないので、必然的にクライアントライブラリ側で面倒を見る必要が出てきます。 何かイケてる実装はないか探したところ、RedisLabsのRedis Cluster Proxyの実装に出会いました。
https://github.com/RedisLabs/redis-cluster-proxy
SCAN
が返す cursor を細工して「今どのノードをSCANしているか」の情報を加えた上でユーザーに返すことで、
シームレスに全ノードを全スキャンできるよう実装されていました。
SCAN: performs the scan on all the master nodes of the cluster. The cursor contained in the reply will have a special four-digits suffix indicating the index of the node that has to be scanned. Note: sometimes the cursor could be something like "00001", so you mustn't convert it to an integer when your client has to use it to perform the next scan.
実装者に敬意を払いつつ redis-cluster-client
gemでも真似させていただきました。
SCAN
コマンドはサーバーサイドも状態を持たない形で実装されているみたいで綺麗だなと思いました。
Clusterモードのクライアントライブラリ開発におけるCIの充実
redis-cluster-client
gemのメンテを任されたときに一番強く思ったのが「できる限りCIで全パターンをテストする」でした。
自分はWindowsのWSL環境で開発していて、CIはGitHub ActionsのUbuntu環境なのでDockerを使ったテスト環境を整備しました。
https://github.com/redis-rb/redis-cluster-client
Ruby x Redisのバージョンごとの基本的なマトリクステストに加えて以下の状態変化パターンも全てCIに盛り込みました。
- Cluster全ダウン、からの復旧
- Clusterの一部ダウン、からの復旧
- リシャーディング
- スケールイン、スケールアウト
あとは個人的にメモリアロケーションに気を使って実装したかったのでベンチマークやプロファイリングも仕込みました。
初期の頃はflakyなテストケースと格闘する日々でしたが、今では安定して6分程度で完了しています。
Redisをソースからビルドするのが嫌でDockerを使っていましたが、 macOSで利用者の多いDocker Desktop環境だとネットワークモードの関係でテストが通りませんでした。
https://docs.docker.com/desktop/networking/#there-is-no-docker0-bridge-on-the-host
CLUSTER NODES
などのCluster状態情報を返すコマンドから得られるノードの接続情報が、
ホスト側からアクセスできないネットワークIPアドレスになってしまいます。
Docker Desktopでは基本的にマッピングしたポート番号経由でしかホストからコンテナにアクセスできません。 Redis側にNAT環境を考慮した設定項目がいくつかありますが、クライアント側の対応も含めてやや面倒です。
これはissueで複数人から聞かれており、将来的にどうにかしたいところではありますが今のところ気力が湧きません。 LinuxやWSL環境でのDocker Engineユーザーが増えるか、Apple社が何とかしてくれることを祈ってます。
余談ですが簡単なpipelining処理ではenvoyにベンチマークで負け続けていて悔しい気持ちでいっぱいです。時間があったらチューニングしたい所です。
ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [x86_64-linux] Warming up -------------------------------------- pipelined: ondemand 56.000 i/100ms pipelined: pooled 71.000 i/100ms pipelined: none 74.000 i/100ms pipelined: envoy 81.000 i/100ms pipelined: cproxy 41.000 i/100ms Calculating ------------------------------------- pipelined: ondemand 631.513 (± 6.8%) i/s (1.58 ms/i) - 3.136k in 5.000559s pipelined: pooled 707.244 (± 6.2%) i/s (1.41 ms/i) - 3.550k in 5.050568s pipelined: none 742.375 (± 2.0%) i/s (1.35 ms/i) - 3.774k in 5.085886s pipelined: envoy 814.131 (± 6.0%) i/s (1.23 ms/i) - 4.050k in 5.001000s pipelined: cproxy 408.768 (± 4.6%) i/s (2.45 ms/i) - 2.050k in 5.027686s Comparison: pipelined: envoy: 814.1 i/s pipelined: none: 742.4 i/s - 1.10x slower pipelined: pooled: 707.2 i/s - 1.15x slower pipelined: ondemand: 631.5 i/s - 1.29x slower pipelined: cproxy: 408.8 i/s - 1.99x slower
対象 | 説明 |
---|---|
ondemand | マルチスレッドで都度スレッドを生成する実装 |
pooled | マルチスレッドであらかじめ生成してあるスレッドを使う実装 |
none | シングルスレッドの単純なループ実装 |
envoy | envoyのRedis cluster proxy |
cproxy | RedisLabsのRedis cluster proxy |
ネットワーク的に有利なので単発コマンドではさすがに勝ててはいますが、もっとぶっちぎりたいところではあります。
ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [x86_64-linux] Warming up -------------------------------------- single: cli 125.000 i/100ms single: envoy 38.000 i/100ms single: cproxy 55.000 i/100ms Calculating ------------------------------------- single: cli 1.225k (± 7.1%) i/s (816.16 μs/i) - 6.125k in 5.033841s single: envoy 376.037 (± 6.9%) i/s (2.66 ms/i) - 1.900k in 5.088755s single: cproxy 543.492 (± 1.8%) i/s (1.84 ms/i) - 2.750k in 5.061625s Comparison: single: cli: 1225.3 i/s single: cproxy: 543.5 i/s - 2.25x slower single: envoy: 376.0 i/s - 3.26x slower
Valkeyについての余談
RedisやValkeyなどのネーミングに依存している実装はサーバー・クライアントサイド共に存在しない認識です。
現に redis-cluster-client
gemのCIでValkeyでもテストしてますが何も変えずに通っています。
以前CIでどうしても通らなかったテストケースがあり、 その原因は「リシャーディング中のレプリカ参照でキーのリダイレクトエラーが発生しない」というものでした。
想定外の挙動だったためRedisにissueを作成しました。
https://github.com/redis/redis/issues/11312
そしたら数年後にValkey側で修正報告がありました。
https://github.com/valkey-io/valkey/pull/495
個人的には複雑な気持ちになりましたが、有り難くValkeyを使わせていただき問題のテストケースがちゃんと通るようになりました。
最後に
ここまで読んでいただきありがとうございます。
今まで挙げてきた通りRedisのClusterモードは2024年現在で考慮ポイントが複数存在するため、非Clusterモードからの移行には躊躇してしまうかもしれません。 そもそもよほどの大規模サービスでないとRedisをClusterモードで使う機会はないかもしれません。
クラウドのマネージドサービスのサーバーレス版などでは強制的にClusterモードがONになっていたりします。 可用性を担保した上で水平スケールさせつつ運用負荷も軽減させたい場合などは選択肢になり得るでしょう。
少なくともRuby用の redis-cluster-client
gemのメンテと、それを使った redis-clustering
gemへのコントリビュートは可能な限り続ける予定です。
もし不具合などありましたらissueで教えていただけますと幸いです。
https://github.com/redis-rb/redis-cluster-client
将来的にはサーバーサイドの機能が進化してクライアントサイドの責務が減り、もしかしたらClusterモード用のライブラリが不要になる日が来るかもしれません。