Redis Clusterの真実:MOVEDとASKリダイレクトを完全制覇し、分散システムの迷宮を断つ
こんにちは。テックリードの私だ。
今日のコードレビューで、誰かが「Redis Clusterの接続先としてマスターの特定ノードをハードコーディングしている」のを見て、私は思わずコーヒーを吹き出しそうになった。
「なぜ、そんなバカな真似をした?」と聞くと、彼はこう答えた。
「だって、`MOVED`エラーとか面倒じゃないですか。特定のノードに叩けばリダイレクトなんて発生しないと思って……」
甘い。あまりにも甘い。
分散システムであるRedis Clusterの本質は、「データが流動し、ノードが動的にスケール・フェイルオーバーする」というカオスそのものにある。そのカオスを隠蔽し、美しく調停するのがクライアントライブラリの役割であり、それを理解せずに設計することは、目隠しをして時速200キロで高速道路を逆走するようなものだ。
今日は、Redis Clusterの心臓部である「クライアントリダイレクト(MOVEDとASK)」のメカニズムを丸裸にし、我々エンジニアが本番環境で踏み抜く地雷をすべて無効化する方法を伝授しよう。
—
1. Redis Clusterの空間分割モデル(前提知識)
まず大前提として、Redis Clusterはデータを単一の空間に保持しない。
全キー空間は数学的に 16384個のハッシュスロット(Hash Slots) に分割され、クラスター内の各マスターノードがそれぞれのスロット範囲を分担して所有している。
あるキー `user:1000` がどのスロットに属するかは、以下のアルゴリズムで一意に決まる。
$$\text{HASH\_SLOT} = \text{CRC16}(\text{key}) \pmod{16384}$$
もしハッシュタグ(例:`{user:1000}.following`)が使われていなければ、キー名そのものが対象だ。
クライアントがデータを読み書きする際、「どのキーがどのノード(IP:Port)にいるのか」を正確に把握していなければならない。だが、ノードの増減、障害によるフェイルオーバー、リシャスリング(スロットの再配置)によって、このマップは常に変化する。
ここで発生するのが、伝説の2大リダイレクトエラー、`MOVED` と `ASK` だ。
—
2. MOVEDリダイレクト:恒久的な王権交代
仕組み
クライアントが、あるスロットを「過去に持っていた(あるいは持っていると思い込んでいる)」ノードAに対してコマンドを投げたとしよう。しかし、スロットの再配置によって、そのスロットはすでにノードBに移管されていた。
この時、ノードAはデータを返せず、以下のようなエラーを返す。
MOVED 3999 192.168.1.50:6379
- 意味: 「おい、スロット `3999` はもう俺のところにはない。今あいつ(`192.168.1.50:6379`)の管轄だ。今後はそっちに直接聞きに行ってくれ」
- 性質: 恒久的な変更(Permanent)。今後、スロット `3999` へのリクエストはすべて新しいノードへ送るべきである。
クライアント側の挙動
1. エラーを受け取ったスマートクライアントは、内部の「スロット→ノード」のルーティングキャッシュを即座に更新(Invalidate & Refresh)する。
2. 新しいノード(`192.168.1.50:6379`)に対して、同じコマンドを再送する。
—
3. ASKリダイレクト:過渡期の亡命
仕組み
では、リシャスリング(スロットの移行中)にデータが移動している最中だったらどうなるか?
スロット全体の移行が終わる前の一瞬の隙間を縫って、クライアントからデータへのアクセスが飛んできた場合、ノードA(移行元)はこう応答する。
ASK 3999 192.168.1.50:6379
- 意味: 「今まさにスロット `3999` をあいつ(`192.168.1.50:6379`)へ引っ越し中だ! 今すぐにあいつを叩け。ただし、今回は一時的なものだ」
- 性質: 一時的な変更(Temporary)。今回のリクエストだけは新しいノードに行くべきだが、次回のスロット `3999` へのリクエストは、まだ移行元(またはクラスター全体の最新状態)を確認すべきである。
ASKを受け取ったクライアントの「特殊な作法」
ここがエンジニアの腕の見せ所だ。`ASK`を受け取ったクライアントは、単にリダイレクト先を叩くだけでは不十分である。以下のプロトコル上の儀式を厳守しなければならない。
1. リダイレクト先ノード(`192.168.1.50:6379`)に対し、まず `ASKING` コマンドを送信する。
- なぜか? 移行先ノードはまだそのスロットを「正式に自分のもの」として所有していないため、通常の状態ではコマンドを「CLUSTERDOWN」や「MOVED」で弾いてしまう。`ASKING`は、「一時的にこのスロットへのアクセスを許可しろ」という特赦命令なのだ。
2. その直後に、本来実行したかったコマンド(例: `GET user:1000`)を同じ接続(または新しい接続)で送信する。
3. 重要: このリダイレクトによって、ローカルの「スロット→ノード」キャッシュを書き換えてはならない。なぜなら、移行はまだ完了していないからだ。
—
4. ダメな設計 vs 堅牢な設計:コードレビューの視点
ここで、実務でやりがちな「アンチパターン」と、プロフェッショナルな「正しい設計」を比較しよう。
❌ アンチパターン:Dumb Client(単機能クライアント)の利用
Jedisの古いバージョンや、自作のいい加減なTCPラッパーを使い、`Cluster`対応をサボったケース。
【悪夢のコード例】
import redis
特定のノードに直接接続(Clusterの旨みを完全に殺している)
client = redis.Redis(host=’192.168.1.10′, port=6379)
def get_user_data(user_id):
try:
return client.get(f”user:{user_id}”)
except redis.exceptions.ResponseError as e:
if “MOVED” in str(e):
# 愚直に手動でパースしてリトライしようとする(車輪の再発明地獄)
parts = str(e).split()
target_host, target_port = parts[2].split(‘:’)
# ここで新しい接続を都度張る…(コネクション枯渇の音)
temp_client = redis.Redis(host=target_host, port=int(target_port))
return temp_client.get(f”user:{user_id}”)
raise e
何が地獄か?
- フェイルオーバーやリシャスリングが起きた瞬間、アプリケーション全体がエラーの嵐に包まれる。
- コネクションプールの管理が破綻し、TCPコネクションが乱立してRedisサーバーがダウンする。
—
⭕ 堅牢な設計:Smart Client(クラスタ認識型クライアント)の活用
現代のプロダクション環境では、Redis Clusterのプロトコルを完全に理解し、`MOVED` / `ASK` を水面下で自動処理してくれる「スマートクライアント(例: Pythonなら `redis-py` の `RedisCluster`、Javaなら `JedisCluster` や `Lettuce`)」を使うのが絶対条件だ。
【洗練されたプロダクションコード例】
from redis.cluster import RedisCluster
クラスター内の「適当な1台(シードノード)」を指定するだけでいい
クライアントが起動時に CLUSTER SLOTS を叩いて全ノードのマップを自動構築する
startup_nodes = [
{“host”: “192.168.1.10”, “port”: “6379”},
{“host”: “192.168.1.11”, “port”: “6379”}
]
内部でスロットキャッシュ、コネクションプール、MOVED/ASKのリトライロジックを完璧に制御
rc = RedisCluster(startup_nodes=startup_nodes, decode_responses=True)
def get_user_data_safely(user_id: int):
# アプリケーションコード側は、リダイレクトの存在を一切意識する必要がない
# すべてスマートクライアントの内部でハンドリングされる
return rc.get(f”user:{user_id}”)
これだけで十分だ。開発者はビジネスロジックに集中し、インフラストラクチャの動的な変動はRedis Clusterとスマートクライアントのペアが完全に隠蔽してくれる。
—
5. パフォーマンス・オブザーバビリティの極意:プロからの警告
最後に、チーフアーキテクトとして実運用における「見逃してはならない指標」を授けよう。
1. 「MOVEDストーム」の検知
もしアプリケーションのログやメトリクスで、`MOVED`リダイレクトが頻発している場合、それはクライアントのルーティングキャッシュが古くなっているか、クラスタのリシャスリング頻度が異常に高いことを意味する。
クライアント側が `CLUSTER SLOTS` や `CLUSTER NODES` を定期的に再取得(あるいはエラー時にトポロジーを強制リフレッシュ)する機構が正しく動いているか確認せよ。
2. ネットワークホップのコスト
クライアントが間違ったノードにリクエストを投げるたびに、余分なネットワークラウンドトリップ(Rtt)が発生する。高ス負荷システムにおいて、この「無駄な1往復」はレイテンシのp99を悪化させる最大の癌だ。
「常に最新のトポロジーキャッシュを維持すること」。これが分散キャッシュシステムを扱うエンジニアの絶対的責務である。
—
結言
Redis Clusterの `MOVED` と `ASK` は、単なるエラーコードではない。それは、システムが生き物のようにスケールし、障害を自ら克服していく「生命の鼓動」そのものだ。
その仕組みを理解し、適切なスマートクライアントを正しく設定すること。
それこそが、深夜のPagerDuty(アラート)に怯えない、真に堅牢な分散システムを構築唯一の道である。
さあ、設計レビューに戻ろう。君たちのコードから、ハードコードされたノードIPが綺麗に消え去っていることを期待している。
コメント