Redis `MIGRATE` の暗黒面:なぜそのキー移行は本番を止めるのか
こんにちは。プロダクトの裏側で数千万リクエストをさばくインフラや、データ層の設計レビューをしていると、いまだにエンジニアが犯す「致命的なミス」に直面する。
その筆頭が、Redisインスタンス間でのデータ移行だ。
「ちょっと別のクラスタにキーを引っ越したい」「古いキャッシュノードから新しいノードへデータを移すだけだから」と、何の疑いもなく `MIGRATE` コマンドを叩く。そして、本番トラフィックが突如として数秒間完全に凍結し、アラートの嵐が鳴り響く。
今回は、Redisのデータ構造とシングルスレッドモデルの核心に踏み込み、`MIGRATE` コマンドがなぜ劇薬なのか、そして我々プロフェッショナルはどう設計すべきかを徹底的に叩き込む。
—
1. `MIGRATE` コマンドの正体:何が起きているのか?
`MIGRATE` は、指定したキーをあるRedisインスタンスから別のインスタンスへ、アトミック(不可分)に転送するためのコマンドだ。
MIGRATE host port key destination-db timeout [COPY] [REPLACE] [AUTH password]
パッと見は非常に高機能に見える。DB間を跨ぐこともでき、`COPY` をつければ元のインスタンスにキーを残せるし、`REPLACE` をつければ宛先の既存キーを上書きできる。
だが、このコマンドの最大の特異性は、「宛先へのネットワークI/O(ソケット通信)が完了するまで、Redisのメインスレッドが完全にブロックされる」という点にある。
シングルスレッドの呪縛とブロッキング
Redisは基本的にシングルスレッドでコマンドを処理する。`MIGRATE` が実行されると、内部で何が起きるか:
1. シリアライゼーション: 転送対象のキーと値をRedisのRDBフォーマットにエンコードする。
2. ネットワーク送信: 宛先Redisインスタンスに対してTCPソケット経由でデータを送信する。
3. 同期待ち: 宛先からの `+OK` 応答を受け取るまで、他のクライアントからのリクエスト処理は一切行われない(完全に停止する)。
4. 削除 (オプション): `COPY` が指定されていなければ、転送元からキーを削除する (`DEL`)。
もし移行するキーが「数MB〜数十MBある巨大なハッシュ(Hash)」だったり、ネットワークのレイテンシが揺らいでいたりしたらどうなるか?
想像してほしい。たった1つのキーを移行するために、Redis全体が数百ミリ秒、あるいは数秒間沈黙する。その間、数万件のクライアントリクエストがキューイングされ、タイムアウトの連鎖を引き起こす。これが `MIGRATE` が「暗黒面」と言われる理由だ。
—
2. 実務における危険なアンチパターン
コードレビューで次のような実装を見かけたら、即座に差し戻しを要求してほしい。
アンチパターン A: バッチ処理で数万のキーをループ移行
最悪のアンチパターン:絶対にやってはいけない
keys = redis_client.keys(“user:session:”)
for key in keys:
# 1キーごとにMIGRATEを同期実行。ネットワークRTTとシリアライゼーションのオーバーヘッドが直撃する。
redis_client.execute_command(
“MIGRATE”, target_host, target_port, key, 0, 5000
)
ループの回数分だけメインスレッドがブロックを繰り返し、アプリケーション側もRedis側も耐えられなくなり、確実にコネクションプールが枯渇する。
アンチパターン B: 巨大なコレクション型の移行
数万要素を持つ `ZSET` や `LIST`、巨大な `HASH` に対して `MIGRATE` を実行するケース。データサイズに比例してシリアライゼーションと送信コストが跳ね上がり、ブロッキング時間が意図せず数秒に達する。
—
3. 堅牢な設計パターン:どう移行すべきか?
では、システムを止めずに、あるいは停止時間を最小限に抑えてデータを移行するにはどう設計すべきか。プロフェッショナルが選択するアプローチは以下の2つだ。
パターン 1: SCAN と DUMP / RESTORE の組み合わせ(非同期の制御)
どうしてもアプリケーション層やバッチ層で移行を制御したい場合、`MIGRATE` の一撃にかけるのではなく、`DUMP` でシリアライズしたバイナリを取得し、宛先で `RESTORE` する手法をとる。
これでもブロッキングは完全にゼロにはならないが、ネットワーク転送とRedisのコマンド処理を多少なりともコントロールしやすくなる。
ただし、より安全なのは次の一手だ。
パターン 2: 二重書き込み(Dual Write)と遅延移行(推奨)
可用性と無停止を最優先するシステムでは、そもそも移行コマンドを使わない。
1. Phase 1 (Dual Write): アプリケーション層を改修し、新規の書き込み(Write)を「旧クラスタ」と「新クラスタ」の両方に行うようにする。読み込み(Read)は旧クラスタを見る。
2. Phase 2 (Backfill): 既存の静的データや古いキーを、バックグラウンドワーカーで少しずつ(レートリミットをかけながら)新クラスタへコピーする。この際、すでにデュアル書き込みによって新クラスタに新しいデータがある場合は上書きしないよう競合制御を入れる。
3. Phase 3 (Switch Read): 移行完了後、読み込み先を新クラスタに切り替える。
4. Phase 4 (Deprecate): 旧クラスタへの書き込みを停止し、破棄する。
このアーキテクチャであれば、Redisの単一インスタンスに過大な負荷をかけることなく、安全にマイグレーションを完遂できる。
—
4. パフォーマンス上の注意点とパラメータチューニング
どうしても `MIGRATE` を使わざるを得ない限定的なケース(例:Redis Clusterのリバランス時など、内部的に自動で行われる場合)における、最低限押さえておくべきポイントを挙げる。
- `TIMEOUT` パラメータの適切な設定
`MIGRATE` の第5引数はタイムアウト(ミリ秒)だ。これを短すぎるとネットワークの微小な揺らぎで転送失敗し、長すぎると障害時のコネクション詰まりを引き起こす。通常は `5000` (5秒) 程度を上限とし、ネットワーク帯域とデータサイズから逆算して厳密に決めること。
- ASYNCオプションの活用(Redis 4.0以降)
もしキーの削除を伴う移行であれば、Redis 4.0以降で導入された遅延削除(Lazy Free)の概念を意識する。ただし、`MIGRATE` 自体のブロッキング(データ送信完了まで)は避けられないため過信は禁物だ。
- ネットワーク帯域のボトルネック
インスタンス間が別AZや別リージョンにまたがる場合、TCPウィンドウサイズや帯域制限が原因で `MIGRATE` のブロック時間が想定以上に伸びる。移行時は必ず同一VPC内、可能なら同一ホスト(ローカルブリッジや同一ラック)の近傍で行うのが鉄則だ。
—
チーフアーキテクトからの総括
Redisの `MIGRATE` コマンドは、手軽に見えるがゆえにシステムの急所を突く劇薬だ。
「データを別の場所に移動する」という単純な要件であっても、裏側で何が動き、どのリソース(CPU、メモリ、ネットワーク)を消費し、どこをブロックするのか。それを想像できないままコマンドを叩くエンジニアに、本番環境のインフラを触る資格はない。
設計レビューにおいて、移行要件が出てきたらまず疑え。「本当にそのコマンドを使う必要があるのか?」「システム全体への影響範囲をどう隔離しているのか?」と。
妥協のない設計こそが、スケールするシステム手に入れる唯一の道である。
コメント