何が起きたのか
書き込みコマンド — SET、HSET、LPUSH、またはデータを変更するあらゆるコマンド — を実行したところ、Redisが以下のエラーを返しました:
READONLY You can't write against a read only replica.
クライアントがプライマリではなくレプリカノード(以前はスレーブと呼ばれていた)に接続しています。レプリカはデフォルトで読み取り専用です — 例外なし。書き込みの試みは即座に拒否されます。
修正方法は、クライアントが最初から間違ったノードに接続してしまった原因によって異なります。
本当にレプリカに接続しているか確認する
推測は禁物です。何かに触れる前に、まずこれを実行してください:
redis-cli -h your-redis-host -p 6379 INFO replication
出力のroleフィールドを確認します:
# Replication
role:slave
master_host:192.168.1.10
master_port:6379
master_link_status:up
role:slaveまたはrole:replica(Redis 5以降)が表示されれば問題が確認できます。master_hostの値をメモしてください — そこが書き込みの送り先です。
よくある原因
- レプリカIPのハードコード — アプリの設定や接続文字列に直接記述している
- Sentinelフェイルオーバーが発生した — 旧プライマリがレプリカになったが、アプリが再接続していない
- ロードバランサーが書き込みをレプリカにルーティングしている — 読み書き分離なしのラウンドロビン
- ポートの誤り — SentinelはRedisデフォルトの6379ではなく26379でリッスンする
- クラスタークライアントの設定ミス — MOVEDリダイレクトに従わず、シャード内のレプリカに接続してしまっている
修正1 — クライアントをプライマリに向ける
最も直接的なアプローチ:プライマリのアドレスを取得して接続します。
# 任意のノードからプライマリホストを取得する
redis-cli -h your-redis-host -p 6379 INFO replication | grep master_host
# 直接接続する
redis-cli -h 192.168.1.10 -p 6379
127.0.0.1:6379> SET foo bar
OK
次に、アプリケーションの設定を合わせて更新します:
# Python redis-py の例
import redis
r = redis.Redis(host='192.168.1.10', port=6379) # プライマリ、レプリカではない
r.set('foo', 'bar')
これは機能しますが、脆弱です。フェイルオーバーが発生した瞬間に、また振り出しに戻ります。本番環境では修正2に進んでください。
修正2 — Sentinelを使ってアプリが自動的にプライマリを見つけられるようにする
プライマリIPをハードコードするのはトラブルの元です。Sentinelが30秒以内にトリガーできるフェイルオーバーが発生した後、アプリは再びレプリカを指してしまいます。
Sentinelは現在のプライマリを追跡し、要求に応じて正しいアドレスを提供します:
# Python redis-py と Sentinel の使用例
from redis.sentinel import Sentinel
sentinel = Sentinel(
[('sentinel1', 26379), ('sentinel2', 26379), ('sentinel3', 26379)],
socket_timeout=0.1
)
# 常に現在のプライマリを返す
master = sentinel.master_for('mymaster', socket_timeout=0.1)
master.set('foo', 'bar')
# レプリカへの読み取り専用接続
slave = sentinel.slave_for('mymaster', socket_timeout=0.1)
slave.get('foo')
すでにSentinelを使用しているのにエラーが発生している場合は、mymasterがsentinel.confの名前と一致しているか再確認してください:
redis-cli -h sentinel-host -p 26379 SENTINEL masters
名前の不一致は、ルーティングエラーのように見える接続の失敗を静かに引き起こします。
修正3 — レプリカへの書き込みを一時的に許可する(本番環境には非推奨)
Redisには読み取り専用ガードを解除するreplica-read-only noという設定オプションがあります。便利そうに聞こえますが、少なくとも本番環境には適していません。
問題はここにあります:レプリケーション同期中、プライマリはレプリカにフルデータセットのダンプを送信します。ローカルに書き込んだデータはすべて消去されます。マージはありません。データは消えてしまいます。
# 実行時の変更(再起動でリセットされる)
redis-cli -h replica-host CONFIG SET replica-read-only no
# 永続的な変更(redis.conf に追加)
replica-read-only no
**これは隔離されたレプリカでの一時的なデバッグやマイグレーションタスクにのみ使用してください。**データが重要なライブ環境では絶対に使用しないでください。
修正4 — Redisクラスター:クライアントでフォローリダイレクトを有効にする
クラスターモードには追加の複雑さがあります。間違ったシャードへの書き込みはREADONLYではなくMOVEDを返します。しかし正しいシャード内のレプリカにアクセスするとREADONLYが返されます。2つの異なるエラーですが、原因は似ています。
クラスタークライアントが書き込みのためにプライマリをターゲットにしていることを確認してください:
# Python redis-py クラスタークライアント
from redis.cluster import RedisCluster
rc = RedisCluster(
host='redis-node-1',
port=6379,
read_from_replicas=False # すべての操作をプライマリノードに送る
)
rc.set('foo', 'bar')
レプリカによる読み取りスケーリングが必要ですか?read_from_replicas=Trueを設定してください — ただし、クライアントが読み取りのみをそのようにルーティングし、書き込みはしないことを確認してください。
修正5 — Sentinelフェイルオーバー後に強制再接続する
プライマリのアドレスをキャッシュしているアプリは、フェイルオーバー後に問題が発生します。キャッシュされたIPが今やレプリカを指しています。
# 現在のプライマリを確認する
redis-cli -h sentinel-host -p 26379 SENTINEL get-master-addr-by-name mymaster
# 返り値: 192.168.1.11 6379 ← フェイルオーバー後の新しいプライマリ
アプリを再起動するか接続プールをフラッシュすると、古いアドレスがクリアされます。適切に設定されたSentinelクライアントでは、これが自動的に行われます — 手動での介入は不要です。
修正を確認する
完了と宣言する前に、簡単な書き込み/読み取りテストを実行してください:
redis-cli -h correct-primary-host -p 6379 SET test:write ok
# 期待値: OK
redis-cli -h correct-primary-host -p 6379 GET test:write
# 期待値: "ok"
# クリーンアップ
redis-cli -h correct-primary-host -p 6379 DEL test:write
そしてもう一度ロールを確認します:
redis-cli -h correct-primary-host -p 6379 INFO replication | grep role
# 期待値: role:master
得られた教訓
- プライマリのIPをハードコードするのは時限爆弾です — フェイルオーバーがアプリに対して透過的になるよう、Sentinelまたはクラスター対応クライアントを使用してください。
- 接続プールを分離してください — レプリカは読み取りを処理し、プライマリは書き込みを処理します。その分割を暗黙的にではなく、接続設定で明示的にしてください。
- ロール変更アラートを設定してください — プライマリであるべきノードで
role:slaveを監視するか、Sentinelイベントを購読してください:redis-cli -h sentinel-host -p 26379 SUBSCRIBE +switch-master。 - **本番環境ではreplica-read-onlyをそのままにしておいてください。**それには理由があります:レプリカのデータは一時的なものであり、ガードを無効にすると次の同期時に書き込みが静かに失われることになります。

