Redis レプリケーション・Sentinel・Cluster の構築
Redis のレプリケーション(主従)・Sentinel・Cluster という 3 つのモードの構築手順を自分用に記録しておきます。Redis のデプロイモードはどれも複雑ではなく、ポイントは設定ファイルの書き方と、各モードのメリット・デメリットの分析にあります。
公式サイト:Redis
デプロイ環境
OS:CentOS7
Redis バージョン:Redis7.0.5
レプリケーション(主従)モード
メリット: 設定がシンプルで素早く構築でき、読み書き分離によってマスターノードの読み取り負荷を完全に分担できます。さらにスレーブノードの下に「入れ子」のように多段のスレーブノードを構成することも可能です。
デメリット: 高可用性ではなく、マスターノードがダウンすると書き込み能力を失います。また、データの冗長量が大きく、各スレーブノードがマスターノードのデータを 100% 複製します。非高可用アーキテクチャに分類されるため、本番環境で単独で使われることは一般的にありません。
設定手順
1)スレーブノードの redis.conf ファイルを設定します。

マスターとスレーブのパスワードは揃えておくのが望ましいです。
2)マスターノードの redis.conf ファイルを設定します。通常どおり認証パスワードとリモート接続情報を設定すれば十分です。

設定の結果:
Sentinel モード
公式ドキュメント:Redis Sentinel による高可用性の実現

メリット: Sentinel モードは、主従クラスタでマスターノードがダウンすると書き込めなくなる問題を解決します。マスターノードがダウンすると、sentinel がマスターノードの利用不可を検知し、残りの利用可能なスレーブノードの中から新しいマスターノードを選出することで、書き込み不能の問題を解消します。元のマスターノードは再起動後、スレーブノードとしてクラスタに参加します。
デメリット: 依然として大量のデータ冗長が存在します。また Sentinel ノードはデータストレージに参加しないため計算リソースの無駄があり、ストレージ能力は向上せず、書き込み能力も向上せず、中心化された構成のままです。
デプロイ時の注意事項
- Sentinel ノード自身はデータを保存しません。
- Sentinel ノードは最低 3 つ、かつ必ず奇数個にします(Raft アルゴリズムに基づいて選出を行うため)。
- Sentinel ノードは独立してデプロイし、Sentinel プロセスのみを動かします。他のアプリケーションを同居させると Sentinel が TILT モードに入る恐れがあり、またハードウェアリソースの奪い合いによって Sentinel の監視信頼性が低下します。
Sentinel モードはレプリケーションモードを基盤としています。 マスター 1 台 + スレーブ 2 台に Sentinel 3 台を加えると、すでに 6 つの計算インスタンスが必要です。本番環境では、クラスタ全体の安定性を保つために Sentinel は必ず単独でデプロイし、マスターノードやスレーブノードに同居させてはいけません。
このモードは本番環境で利用可能な高可用アーキテクチャであり、Redis のすべての機能が保持されます。
Sentinel ノードの設定手順
前提:レプリケーションモードがすでに構成済みであること。
1)sentinel.conf ファイルを設定し、監視対象の master アドレスを指定します。Sentinel はマスターノードの info 情報から対応するスレーブノードの情報を自動的に読み取ります。

2)すべてのマスター・スレーブノードのパスワードを揃えます。
3)redis-sentinel コマンドで Sentinel を起動します。
4)spring-boot で Sentinel 接続を設定します。

Sentinel ノードの数が多い場合は、行を分けて設定できます:

Sentinel によるノードダウンの判定には主観的ダウンと客観的ダウンがあり、十分な数の Sentinel がマスターノードを利用不可と判断したときにのみ選出投票が行われます。選出の間、クラスタ全体は外部にサービスを提供できません。
主観的ダウンと客観的ダウン
SDOWN(subjectively down)は直訳すると「主観的」障害で、現在の sentinel インスタンスがある redis サービスを「利用不可」状態と判断していることを意味します。
ODOWN(objectively down)は直訳すると「客観的」障害で、複数の sentinel インスタンスがいずれも master を SDOWN 状態と判断した場合、master は ODOWN になります。ODOWN は、master がクラスタによって「利用不可」と確定されたと簡単に理解でき、フェイルオーバーの仕組みが開始されます。

Cluster モード
公式ドキュメント:Redis Cluster によるスケーリング

よく使われるクラスタソリューションは 3 つあります:
1)Twemproxy(Twitter がオープンソース化した Redis プロキシ)
- 使いやすい監視・管理用のバックエンド画面がなく、運用監視に不向きです。
- 最大の課題は、スムーズなスケールアウト/スケールインができないことです。ビジネス上の必要から Redis インスタンスを追加する際、運用の作業量が非常に大きくなります。
2)Codis(豌豆荚(Wandoujia)による独自開発)
- UI 管理画面があります。
- ノードの動的な追加をうまく扱えます。
3)公式ソリューションの Redis Cluster
メリット: Cluster モードは Sentinel モードと比べて中心化の考え方を取り除き、ロードバランシングを自動的に実現します。データストレージの上限はノード数の増加に伴って拡大します。ノード間は軽量なプロトコルを使用して帯域の消費を抑え、ノードの動的な拡張をサポートします。クラスタはハッシュスロット(hash slot)を使ってキーとインスタンスのマッピングを扱います。クラスタ全体で 16384 個のハッシュスロットがあり、デフォルトでは 16384 個のハッシュスロットが全ノードに割り当てられ、各インスタンスノードが一区間のハッシュスロットを担当します。これはデータパーティションに似ています。各キーは CRC16 アルゴリズムでハッシュ値を求め、それを 16384 で割った剰余、すなわち CRC16(key) mod 16384 の結果によって、キーが属するハッシュスロットの位置が決まります。
まとめると 3 点です:書き込み能力の向上、ストレージ能力の向上、非中心化。
デメリット: データ保存の偏りとデータアクセスの偏り(データスキュー)の問題が存在します。
データスキューの問題
データがシャーディングクラスタの複数インスタンスに不均等に分布し、大量のデータが 1 つまたは少数のインスタンスに集中すると、ストレージ負荷が均等に分散されず、さらにホットデータへのアクセスが常に特定のインスタンスに集中するため、そのインスタンスの負荷が増大し、最終的にはダウンのリスクに直面します。データスキューが発生する主な原因は 3 つあります。
1)bigkey を保存している。
bigkey は value が非常に大きい(String 型)か、あるいは大量のコレクション要素を保持している(コレクション型)ため、そのインスタンスのデータ量が増加し、メモリリソースの消費もそれに応じて増えます。しかも bigkey の操作は一般にインスタンスの IO スレッドをブロックするため、bigkey へのアクセスが多いと、そのインスタンス上の他のリクエストの処理速度にも影響します。
bigkey によるデータスキューを避けるための根本的な対策は、ビジネス層でデータを生成する際に、過剰なデータを同じキーバリューに保存しないようにすることです。bigkey がコレクション型である場合は、bigkey を多数の小さなコレクション型データに分割し、異なるインスタンスに分散して保存する方法もあります。
2)Slot の割り当てが不均衡。
クラスタの運用担当者が Slot を均等に割り当てていないと、大量のデータが同じ Slot に割り当てられます。同じ Slot は 1 つのインスタンスにしか配置されないため、大量のデータが 1 つのインスタンスに集中し、データスキューを引き起こします。
運用規約によって、割り当て前に過剰な Slot が同じインスタンスに集中しないようにできます。すでに Slot が割り当て済みのクラスタでは、まず Slot とインスタンスの具体的な割り当て関係を確認し、過剰な Slot が同じインスタンスに集中していないかを判断します。集中している場合は、一部の Slot を他のインスタンスに移行することで、データスキューを回避できます。
Slot の割り当て状況を確認する方法はクラスタによって異なります。Redis Cluster であれば CLUSTER SLOTS コマンドを使用し、Codis であれば codis dashboard の UI コントロールパネルで確認できます。
3)Hash Tag を使用している。
Hash Tag とは、キーバリューの key に付ける一対の波括弧 {} のことです。この括弧で key の一部を囲むと、クライアントは key の CRC16 値を計算する際に、Hash Tag の波括弧内の key の内容だけを計算対象にします。
key が user:profile:3231 だとして、そのうちの 3231 を Hash Tag にすると、key は user:profile:{3231} になります。クライアントがこの key の CRC16 値を計算するときは 3231 の CRC16 値だけを計算します。そうでなければ、クライアントは "user:profile:3231" 全体の CRC16 値を計算します。
Hash Tag を使うメリットは、異なる key の Hash Tag の内容が同じであれば、それらの key に対応するデータが同じ Slot にマッピングされ、同時に同じインスタンスに割り当てられることです。

Hash Tag は一般にどんな場面で使われるのでしょうか。実は主に Redis Cluster と Codis において、トランザクション操作と範囲クエリをサポートするために使われます。Redis Cluster と Codis 自体はインスタンスをまたぐトランザクション操作や範囲クエリをサポートしていないため、業務アプリケーションにこうした要件がある場合は、データをビジネス層に読み込んでトランザクション処理を行うか、各インスタンスを 1 つずつクエリして範囲クエリの結果を得るしかありません。
Hash Tag を使ってトランザクション操作や範囲クエリの対象データを同じインスタンスにマッピングすれば、トランザクションや範囲クエリを簡単に実現できます。
しかし Hash Tag を使う潜在的な問題として、大量のデータが 1 つのインスタンスに集中してデータスキューを引き起こし、クラスタ内の負荷が不均衡になる可能性があります。そのため、範囲クエリ・トランザクション実行の要件と、データスキューがもたらすアクセス負荷との間でトレードオフを考える必要があります。
私のおすすめは、Hash Tag でシャーディングしたデータが大きなアクセス負荷をもたらすようであれば、データスキューの回避を優先し、Hash Tag によるデータシャーディングは使わないことです。トランザクションも範囲クエリもクライアント側で実行できますが、データスキューはインスタンスの不安定化を招き、サービス停止につながるからです。
データアクセススキューの原因と対策
データアクセススキューが発生する根本原因は、インスタンス上にホットデータが存在することです(例えばニュースアプリのホットなニュースコンテンツ、EC のセールにおける人気商品情報など)。ホットデータがあるインスタンスに保存されると、そのインスタンスへのリクエストアクセス量は他のインスタンスよりはるかに多くなり、巨大なアクセス負荷に直面します。
ホットデータは通常 1 つか数個のデータなので、Slot を再割り当てするだけではホットデータの問題は解決できません。一般的に、ホットデータは読み取り操作が中心なので、この場合はホットデータの多重レプリカ方式で対応できます。
具体的には、ホットデータを複数コピーし、各データレプリカの key にランダムなプレフィックスを追加して、他のレプリカデータと同じ Slot にマッピングされないようにします。こうすることで、ホットデータには複数のレプリカがあって同時にリクエストを処理でき、しかもこれらのレプリカデータの key は互いに異なるため、異なる Slot にマッピングされます。これらの Slot にインスタンスを割り当てる際も、異なるインスタンスに配置するよう注意すれば、ホットデータのアクセス負荷は異なるインスタンスに分散されます。
注意:ホットデータの多重レプリカ方式は読み取り専用のホットデータにしか適用できません。ホットデータに読み書き両方がある場合、多重レプリカ間のデータ整合性を保証するための追加コストがかかるため、多重レプリカ方式は適しません。読み書きのあるホットデータについては、インスタンス自体のリソースを増強する、例えばより高スペックのマシンを使うなどして、大量のアクセス負荷に対応する必要があります。
クラスタ使用時の注意事項
Redis Cluster のノード間では、定期的に Gossip メッセージの交換とハートビートチェックが行われます。公式は Redis Cluster のノード数を 1000 以下にすることを推奨しています。クラスタ内のノード数が多すぎると、無視できない帯域消費が発生します。
- メッセージ送信頻度:あるノードが他ノードとの最終通信時刻が cluster-node-timeout/2 を超えたことを検知すると、直接 PING メッセージを送信します。
- メッセージのデータ量:slots スロット配列(2kb の領域)と、クラスタ全体の 1/10 の状態データ(ノード 10 個分の状態データで約 1kb)。
- ノードを配置するマシン規模:クラスタを分散させるマシンが多く、各マシンに割り当てるノード数が均等であるほど、クラスタ全体で利用可能な帯域は高くなります。
Redis Cluster クラスタの制限:
キーのバッチ操作のサポートは限定的:例えば mget、mset は同一 slot 内でなければならない
キーのトランザクションと Lua のサポートは限定的:操作するキーは同一ノード上になければならない
キーはデータパーティションの最小粒度:bigkey のパーティション分割はサポートされない
複数データベースはサポートされない:クラスタモードでは db0 のみ
レプリケーションは 1 段のみ:ツリー型レプリケーション構造はサポートされない
Redis Cluster が満たす容量と性能の拡張性は、多くのビジネスでは「必要ない」
多くの場合クライアントの性能は「低下」する
ノードをまたぐコマンドは使用不可:mget、keys、scan、flush、sinter など
Lua とトランザクションはノードをまたいで使用できない
クライアントの保守がより複雑になる:SDK とアプリケーション自体のコスト増(例えばより多くのコネクションプール)
多くの場面では Redis Sentinel で十分です。
Codis と Redis Cluster クラスタの比較:

Redis Cluster のまとめ
1. Redis Cluster のデータパーティションルールは仮想スロット方式(16384 スロット)を採用し、各ノードが一部のスロットと関連データを担当することで、データとリクエストのロードバランシングを実現する
2. Redis Cluster の構築は 4 つのステップに分かれる:ノードの準備、meet 操作、スロットの割り当て、データの複製。
3. Redis 公式は redis-trib.rb ツールによる Redis Cluster の迅速な構築を推奨している(redis5.0 より前は redis-trib を、それ以降は redis-cli コマンドを使用)
4. クラスタのスケーリングはノード間でスロットと関連データを移動することで実現する。スケールアウト時はスロット移行計画に従ってスロットをソースノードから新ノードへ移行し、スケールイン時はオフラインにするノードが担当するスロットがあれば他ノードへ移行し、その後 cluster forget コマンドでクラスタ内の全ノードにオフラインノードを忘れさせる
5. smart クライアントを使ってクラスタを操作することで通信効率を最大化する。クライアント内部でキー、スロット、ノードのマッピングを計算・保持し、ターゲットノードへの高速な特定に用いる
6. クラスタの自動フェイルオーバープロセスは障害検出とノード復旧に分かれる。ノードのダウン判定は主観的ダウンと客観的ダウンに分かれ、過半数のノードが障害ノードを主観的ダウンと判断した場合、そのノードは客観的ダウン状態としてマークされる。スレーブノードが客観的ダウンとなったマスターノードに対する障害復旧フローを起動し、クラスタの可用性を保証する
7. 開発・運用でよくある問題:超大規模クラスタの帯域消費、pub/sub のブロードキャスト問題、クラスタの偏り問題、単一マシンとクラスタの比較など
redis-cli による Redis Cluster の構築(構築バージョン 7.0.4)
最低 6 ノード、(マスター 1 + スレーブ 1) × 3 = 6 ノードが必要です。redis 5.0 より前は redis-trib ツールを、それ以降のバージョンでは redis-cli コマンドを使用します。
1)redis.conf ファイルを設定します。すべてのノードのパスワードを同一にし、クラスタモードを有効にします(詳細な設定は公式ドキュメントを参照):
# クラスタモードを有効にする
cluster-enabled yes

2)クラスタ作成コマンドを実行します:
redis-cli --cluster create --cluster-replicas 1 192.168.121.145:6379 192.168.121.143:6379 192.168.121.142:6379 192.168.121.141:6379 192.168.121.144:6379 192.168.121.140:6379 -a 【パスワード】
ここで redis-cli --cluster または ./redis-trib.rb はクラスタ操作コマンドを表し、create はクラスタの作成を表します。--replicas 1 または --cluster-replicas 1 は、クラスタ内の各 master のレプリカ数を 1 に指定するもので、このときノード総数 ÷ (replicas + 1) が master の数になります。したがってノードリストの先頭 n 個が master となり、残りのノードはすべて slave ノードとして、異なる master にランダムに割り当てられます。
コマンドを入力するとクラスタの slot 割り当ての確認を求められます。必ず y ではなく完全な yes を入力してください。そうしないと slot の自動割り当てがスキップされ、クラスタが使用不能になります。未割り当ての場合は、redis-cli --cluster fix 127.0.0.1:6379 -a 【パスワード】(任意のノード)コマンドで slot を修復できます。
ログ出力から、クラスタが自動的に作成完了したことがわかります。

3)現在のクラスタノード情報を確認します:
redis-cli -h 【任意のノードIP】 -p 【ポート】 -a 【パスワード】 cluster nodes
下図はノード情報です(この時点では slot 未割り当て):

slot の割り当てを実行します(各 master が担当する slot スロットが確認できます):

値の挿入を試します:

UI ツールで値が保存されたかを確認します。クラスタの任意のノードに接続すれば OK です:

Redis クラスタの構築に成功したことが確認できます。
spring-boot からクラスタに接続します:

まとめ
レプリケーションモードは設定が最もシンプルですが、マスターノードがダウンするとクラスタは書き込み能力を失うため、他のモードの基盤として使われることがほとんどです。Sentinel モードはレプリケーションの上に自動フェイルオーバーを補うもので、多くのビジネスシーンではこれで十分です。Cluster モードは書き込み能力とストレージ容量の水平スケーリングをもたらしますが、データスキューやノードをまたぐコマンドの制限といった新たな問題も持ち込みます。選定の際はまずデータ量と書き込み負荷を評価するべきで、最初からいきなり Cluster を導入する必要はありません。
COMMENTS