Redisクラスタ内部：クライアントによるシャードの特定方法

[![表紙](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/f4f0093b-b482-4de7-8ffb-c27f1e7c7314.png)](https://leapcell.io/?lc_t=q_redisshard)

## 背景：なぜ Redis Cluster が必要なのか

Sentinel モードはマスター・スレーブ構成に基づいており、読み書きの分離を実現しています。また、自動フェイルオーバー機能も備えているため、システムの可用性が高くなります。しかし、各ノードが同じデータを保持するため、メモリの無駄が生じ、オンラインでのスケールアウトが難しいという課題があります。

そのため、Redis Cluster クラスタ（シャーディングクラスタの実装方式）が登場しました。Redis 3.0 で導入され、Redis の分散ストレージを実現しています。データをシャーディング（分割）することで、各 Redis ノードが異なるデータを保持し、オンラインスケーリングの課題を解決しています。また、データを各 Redis インスタンスに分散して保存できるため、大容量データの保存も可能で、レプリケーションやフェイルオーバー機能も提供されます。

> たとえば、1 つの Redis インスタンスに 15GB 以上のデータを保存すると、応答が非常に遅くなります。これは Redis の RDB 永続化メカニズムによるもので、Redis は RDB 永続化操作を完了するために子プロセスを fork します。この fork 処理の所要時間は Redis 内のデータ量に正比例します。

このとき、「15GB のデータを分散保存すればいいのでは？」と考えるのは自然です。これこそが Redis シャーディングクラスタの目的です。シャーディングクラスタとは何か、例を見てみましょう。Redis で 15GB のデータを保存する必要がある場合、単一の Redis インスタンスを使う方法と、3 台の Redis インスタンスで構成されるシャーディングクラスタを使う方法があります。以下に比較します：

> シャーディングクラスタと Redis Cluster の違い：Redis Cluster は Redis 3.0 以降で公式に提供されたシャーディングクラスタの実装方式です。

データが複数の Redis インスタンスにシャーディングされている場合、クライアントはどのインスタンスにアクセスすればよいのか、どうやって判断しているのでしょうか？以下に Redis Cluster の仕組みを見ていきましょう。

## クライアントはどのようにしてアクセスすべきシャードを知るのか？

Redis Cluster は、データとインスタンス間のマッピング関係を処理するために「ハッシュスロット（Hash Slot）」という方式を採用しています。

> シャーディングクラスタは、全体で 16384 個のスロット（slot）に分割されます。Redis に送信される各キー・バリューのペアは、キーに基づいてハッシュ処理され、16384 個のスロットのいずれかに割り当てられます。使用されるハッシュ関数は比較的単純で、CRC16 アルゴリズムで 16bit の値を計算し、それを 16384 で割った余り（mod）を使ってスロットを決定します。データベース内のすべてのキーはこの 16384 個のスロットのいずれかに属しており、クラスタ内の各ノードはこのスロットの一部を処理します。

クラスタ内の各ノードは、ハッシュスロットの一部分を担当しています。仮に現在のクラスタにノード A、B、C の 3 台があるとすると、各ノードが担当するハッシュスロット数は 16384 ÷ 3 になります。例として、以下のような割り当てが可能です：

- ノード A：スロット 0 ～ 5460 を担当
- ノード B：スロット 5461 ～ 10922 を担当
- ノード C：スロット 10923 ～ 16383 を担当

クライアントが Redis インスタンスにデータの読み書きを要求したとき、そのインスタンスに対象のデータが存在しない場合はどうなるのでしょうか？ここで登場するのが、**MOVED リダイレクト**と**ASK リダイレクト**です。

---

## 3. インスタンスに対象データが存在しない場合はどうなるか？

Redis Cluster モードでは、ノードがリクエストを処理する流れは以下のようになります：

- ハッシュスロットに基づき、現在の Redis キーが自ノードに存在するか確認
- スロットが自ノードの担当でない場合は、**MOVED リダイレクト**を返す
- スロットが自ノードの担当であり、キーがスロット内に存在する場合は、該当キーの結果を返す
- キーがそのスロットに存在しない場合、そのスロットが「MIGRATING（移行中）」か確認
- キーが移行中であれば、**ASK リダイレクト**を返してクライアントを移行先ノードに誘導
- スロットが移行中でなければ、スロットが「IMPORTING（インポート中）」か確認
- スロットがインポート中で ASKING フラグが立っていれば処理を継続、なければ MOVED を返す

### 3.1 MOVED リダイレクト

クライアントが Redis インスタンスに対して読み書き操作を行った際に、対象スロットがそのノードに属していない場合、Redis は **MOVED リダイレクト**エラーを返します。このエラーには、対象スロットを保持している新しいインスタンスの IP アドレスとポート番号が含まれています。これが Redis Cluster の MOVED リダイレクトメカニズムです。

### 3.2 ASK リダイレクト

ASK リダイレクトは主にクラスタのスケーリング（拡張・縮小）時に発生します。スケーリングによりスロットが移動するため、元のノードにアクセスすると、データはすでに移行先のノードに存在している可能性があります。この場合、ASK リダイレクトを使うことで正しいノードへ誘導できます。

## 4. 各ノード間の通信はどう行われるか？

Redis クラスタは複数のノードで構成されており、それぞれのノードはどのように通信しているのでしょうか？答えは **Gossip プロトコル**です。Gossip はメッセージ伝播プロトコルの一種で、各ノードは周期的にノードリストの中から k 個のノードを選び、自ノードが持つ情報を送信します。これを繰り返していくことで、すべてのノードの情報が一致し、アルゴリズムが収束します。

> Gossip プロトコルの基本的な考え方：あるノードがネットワーク内の他のノードに情報を共有したいとき、周期的にランダムにいくつかのノードを選び、情報を送ります。情報を受け取ったノードも同様に、他のランダムなノードにその情報を送信します。通常、情報は一度に 1 ノードではなく N 個のターゲットノードに送信されます。この N のことを **fanout（ファンアウト）**と呼びます。

Redis Cluster はこの Gossip プロトコルを使って通信し、ノード同士が常に情報を交換しています。交換される情報には、ノードの障害、新ノードの参加、マスター・スレーブの変更、スロット情報などが含まれます。Gossip プロトコルでは、以下のような複数のメッセージタイプが使われます：

- **meet メッセージ**：新ノードの参加を通知します。送信ノードは受信ノードに対し、現在のクラスタに参加するように通知します。meet メッセージが正常に通信されると、受信ノードはクラスタに参加し、以後は周期的に ping および pong メッセージの交換を行うようになります。
- **ping メッセージ**：各ノードは毎秒、他のノードに ping メッセージを送信します。このメッセージには、ノードが把握している他の 2 つのノードのアドレス、スロット、状態情報、最終通信時間などが含まれます。
- **pong メッセージ**：ping や meet メッセージを受け取ったとき、応答として pong メッセージを返します。このメッセージにも、送信ノードが把握している他の 2 つのノードの情報が含まれます。
- **fail メッセージ**：あるノードが別のノードをダウンと判断した場合、その情報をクラスタ全体に fail メッセージとしてブロードキャストします。他のノードがこの fail メッセージを受信すると、該当ノードを「ダウン状態」として更新します。

> 特に、各ノードは **クラスタバス（cluster bus）** を通じて他のノードと通信します。この通信には、通常のサービス用ポートとは異なる特別なポート番号が使用されます。たとえば、あるノードのポートが 6379 の場合、クラスタ間の通信には 16379（6379 + 10000）番ポートが使われます。ノード間通信は特別なバイナリプロトコルを用いて行われます。

## 5. クラスタ内ノードに障害が発生した場合はどうなるか？

Redis クラスタは高可用性を実現しており、クラスタ内のノードに障害が発生しても、**フェイルオーバー**によってクラスタの正常なサービス提供が維持されます。

### ダウン（オフライン）

Redis クラスタは **ping/pong メッセージ**を用いて障害を検出します。この検出には「主観的ダウン」と「客観的ダウン」の 2 種類があります：

- **主観的ダウン（Subjective Down、略称：pfail）**：あるノードが他のノードを「利用不可」と判断した状態です。この状態は最終的な障害判定ではなく、あくまで 1 ノードの意見であり、誤判定の可能性があります。
- **客観的ダウン（Objective Down、略称：fail）**：複数のノードがあるノードを「利用不可」と認識し、クラスタ内で合意された正式なダウン状態です。もしダウンしたノードがスロットを保持するマスターノードであれば、フェイルオーバーが必要になります。
- たとえば、ノード A がノード B を主観的ダウンと判断し、一定時間経過後にその情報を他のノード（例：ノード C）に伝えます。ノード C が受信したメッセージを解析してノード B の状態が「pfail」であると確認した場合、「客観的ダウン」のプロセスが発動されます。
- ダウンしたのがマスターノードである場合、Redis Cluster はスロットを保持しているマスター数の過半数による投票を行い、その票数が過半数を超えたときに正式に「客観的ダウン」となります。

### 障害復旧（フェイルオーバー）

障害が検出された後、ダウンしたノードがマスターノードであれば、そのノードに紐づいたスレーブノードの中から 1 台を昇格させてマスターの代替とします。これによりクラスタの高可用性が保たれます。フェイルオーバーの流れは以下のとおりです：

- **資格確認**：スレーブノードがマスターの代替となる資格を持っているか確認します。
- **選挙準備時間の設定**：資格確認が通過した後、フェイルオーバー選挙のトリガー時間を更新します。
- **選挙の開始**：トリガー時間に達すると、選挙を開始します。
- **投票の実施**：スロットを保持するマスターノードだけが投票権を持ちます。スレーブノードが十分な票（過半数）を得られた場合、マスター交代が実行されます。

---

### [私たちはLeapcell、バックエンド・プロジェクトのホスティングの最適解です。](https://leapcell.io/?lc_t=q_redisshard)

[![Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/0c2ed8c2-e9df-416c-98fb-1b2aa2557823.png)](https://leapcell.io/?lc_t=q_redisshard)

[Leapcell](https://leapcell.io/?lc_t=q_redisshard)は、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです：

**複数言語サポート**

- Node.js、Python、Go、Rustで開発できます。

**無制限のプロジェクトデプロイ**

- 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

**比類のないコスト効率**

- 使用量に応じた支払い、アイドル時間は課金されません。
- 例: $25で6.94Mリクエスト、平均応答時間60ms。

**洗練された開発者体験**

- 直感的なUIで簡単に設定できます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

**簡単なスケーラビリティと高パフォーマンス**

- 高い同時実行性を容易に処理するためのオートスケーリング。
- ゼロ運用オーバーヘッド — 構築に集中できます。

[ドキュメントで詳細を確認！](https://docs.leapcell.io/)

[![Try Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/614263cd-3018-4ac3-8801-7b6f7a3a325b.png)](https://leapcell.io/?lc_t=q_redisshard)

Xでフォローする：[@LeapcellHQ](https://x.com/LeapcellHQ)

---

[ブログでこの記事を読む](https://leapcell.io/blog/redis-cluster-how-clients-access-the-right-shard)