LIVESENSE ENGINEER BLOG

リブセンスエンジニアの活動や注目していることを発信しています

redis-cluster-client gem開発の振り返り

技術部インフラグループの春日です。

Redis Clusterモード対応版のRuby用ライブラリである redis-cluster-client gemをほそぼそとメンテしております。

2024年11月現在でまだ600万ダウンロード程度のマイナーライブラリではありますが、 利用者が増えてくるにつれて不具合報告も何件か出てきており、 これまで業務の10%技術投資時間やプライベート時間を利用して修正してきました。

本記事ではクライアントライブラリの実装面における考慮漏れの反省も兼ねて、 Redis Clusterモードならではの考慮ポイントを紹介しつつissue対応を振り返ります。

Ruby用Redisライブラリの変遷

Redis Clusterモードの話に移る前に自分がメンテしているgemの立ち位置に関して背景を共有させてください。

RubyでRedisを扱う際のクライアントライブラリには古くから redis gemが存在します。 初期の頃ではRedis作者もメンテに加わっていたみたいです。

このGemは 4.*.* 系までシングル、Sentinel、Clusterモードをそれ1つでサポートしていました。 しかし 5.*.* 系からClusterモードのクライアントが redis-clustering gemに分離され、 さらに内部実装が以下のgemsに切り離されました。

最終的な依存関係は以下です。

graph TB
  redis-->redis-client
  redis-clustering-->redis-cluster-client-->redis-client

redisredis-clustering gemsの実装はRESP2依存が強く、 後続の redis-clientredis-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 が所属しているスロット 918910.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モードのクライアントライブラリには以下の処理が求められます。

  1. ユーザーから渡されたPipelineを分割して対象ノードごとにまとめる
  2. 分割したPipelineを各ノードに送信して返信を得る
  3. 各ノードからの返信を統合してユーザーに返す、ただしリダイレクトエラーが発生した場合は対象キーを取得し直してから

例えばユーザーから以下の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機能を使えますが、更新対象のキーを単一のスロットに限定するという制約があります。

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機能はまともに動いていない状態でしたが、 その方のご協力もあって現在では不具合が修正されています。

www.youtube.com

なお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みたいにシンプルに実装できないか悩んでたところ以下の標準ライブラリに出会いました。

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モード用のライブラリが不要になる日が来るかもしれません。

https://github.com/valkey-io/valkey/issues/384