Redis Sentinel は何を保証し、何を保証しないのか
Redis を認証基盤やセッション管理に使うと、必ず次の問いにぶつかる。
master が落ちたとき、アプリケーションはどこへ書けばよいのか?
Redis Sentinel はこの問いに答えるための仕組みである。
ただし、Sentinel は万能な HA レイヤーではない。
Sentinel がやることは大きく 2 つだけだ。
- Redis master の死活監視と自動 failover
- クライアントに対する「現在の master はどこか」という service discovery
逆に、Sentinel がやらないことも明確にしておく必要がある。
- shard 分割はしない
- Redis の非同期 replication を強同期にはしない
- acknowledged write の完全な無損失を保証しない
- クライアント側の再接続コストを消さない
この記事では、Sentinel を「便利な自動切り替え機能」としてではなく、単一 master Redis を運用するときの故障検知・構成合意・クライアント発見プロトコルとして分解して見る。
まず全体像
典型的な構成は、Redis master 1 台、replica 2 台、Sentinel 3 台である。Sentinel は Redis プロセスと同じホストに置いてもよいし、別ホストに置いてもよい。重要なのは、Sentinel 同士が多数決を取れるように、独立して落ちる failure domain に分散することだ。
Hot path はアプリケーションから Redis master への通常のコマンドである。Sentinel は毎コマンドのプロキシにはならない。
クライアントは接続開始時や再接続時に Sentinel に問い合わせ、現在の master アドレスを得る。その後の GET / SET / Lua 実行は Redis master へ直接送る。
つまり、Sentinel を入れても通常コマンドの RTT は増えない。代わりに、failover 時の再接続と master rediscovery のコストをクライアントが負う。
Sentinel は Redis Cluster ではない
最初に混同を潰しておく。
| 項目 | Sentinel | Redis Cluster |
|---|---|---|
| データ分割 | しない | hash slot で分割する |
| 書き込みモデル | 単一 master | 複数 master |
| クライアント責務 | master discovery / reconnect | slot map / redirect handling |
| 主な目的 | 単一 Redis master の自動 failover | 水平分散と failover |
| データ再配置 | なし | slot migration がある |
Sentinel 構成のデータレイアウトはあくまでこれである。
one master
├── replica
└── replica
sharding したいなら Sentinel ではなく Redis Cluster の話になる。
Sentinel は「1 つの master group に対して、今どれが master か」を維持する仕組みだ。
最小構成
Sentinel の設定は master だけを指定する。replica は Sentinel が自動発見する。
port 26379
sentinel monitor mymaster redis-1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
それぞれの意味は次の通り。
| 設定 | 物理的な意味 |
|---|---|
sentinel monitor mymaster redis-1 6379 2 |
mymaster という master group を監視する。最後の 2 は ODOWN 判定に必要な Sentinel 数 |
down-after-milliseconds |
PING 応答がない状態を何 ms 続けたら「自分から見て down」とみなすか |
failover-timeout |
failover 試行のタイムアウトと再試行間隔に関係する |
parallel-syncs |
failover 後、同時に何台の replica を新 master に追随させるか |
parallel-syncs は地味だが重要である。大きくすると復旧は速くなるが、新 master に replica の再同期負荷が集中する。小さくすると復旧は遅くなるが、同時に読めなくなる replica の数を抑えられる。
キャッシュ用途なら速い復旧を優先してもよい。認証セッションやレート制限のように Redis が制御面に近い場合は、parallel-syncs 1 の方が挙動を読みやすい。
SDOWN と ODOWN
Sentinel の故障判定には 2 段階ある。
- SDOWN: Subjectively Down。ある Sentinel 1 台から見て master が応答しない
- ODOWN: Objectively Down。quorum 数の Sentinel が SDOWN に同意した状態
ここで一番間違えやすいのは、quorum と majority の違いである。
quorum は master を ODOWN と見なすための数であり、failover 実行の許可数ではない。
実際に failover するには、Sentinel 群の majority による承認が必要になる。
例として Sentinel が 5 台、quorum = 2 の場合を考える。
この設計により、少数派 partition では failover が進まない。
ただし、これが意味するのは「構成の切り替えが暴走しにくい」ということであって、「古い master に書かれたデータが失われない」という意味ではない。
Failover の中身
master が ODOWN になり、leader Sentinel が majority の承認を得ると、failover が始まる。
流れはおおよそ次の通り。
promote される replica は適当に選ばれるわけではない。Sentinel は replica の状態を見て、より安全な候補を選ぶ。
主な評価軸は次の 4 つである。
- master から切断されていた時間
- replica priority
- replication offset
- run ID
replication offset が進んでいる replica は、より多くの書き込みを受け取っている可能性が高い。
ただし replication は非同期なので、「最も進んだ replica」を選んでも、直前の acknowledged write が必ず残っているとは限らない。
クライアントは Sentinel 対応でなければならない
Sentinel を入れても、アプリケーションが固定の Redis master アドレスに接続していたら意味がない。
正しいクライアントの動きはこうなる。
Redis 公式の Sentinel client spec でも、クライアントは SENTINEL get-master-addr-by-name で master を発見し、接続先で ROLE を確認する流れになっている。
failover や再構成が起きたとき、Sentinel は reconfigured instance に対して CLIENT KILL type normal を送るため、クライアントは切断を契機に再 discovery する。
Go で go-redis を使うなら、固定 Addr ではなく FailoverOptions を使う。
package main
import (
"context"
"time"
"github.com/redis/go-redis/v9"
)
func NewRedisClient() *redis.Client {
return redis.NewFailoverClient(&redis.FailoverOptions{
MasterName: "mymaster",
SentinelAddrs: []string{
"redis-sentinel-1:26379",
"redis-sentinel-2:26379",
"redis-sentinel-3:26379",
},
DialTimeout: 300 * time.Millisecond,
ReadTimeout: 500 * time.Millisecond,
WriteTimeout: 500 * time.Millisecond,
PoolSize: 64,
MinIdleConns: 8,
})
}
func Ping(ctx context.Context, rdb *redis.Client) error {
return rdb.Ping(ctx).Err()
}
Hot path に Sentinel 問い合わせを入れないことが重要である。
毎リクエスト get-master-addr-by-name を叩くと、Sentinel が制御 plane ではなくデータ plane に混ざる。これは不要な RTT と syscall を増やすだけで、可用性も性能も落ちる。
接続 pool を持つクライアントでは、master address が変わったら古い pool を閉じ、新 master に張り直す必要がある。古いコネクションを残すと、旧 master や降格途中の replica に書き込む危険がある。
Sentinel が保証しないもの: ゼロデータロス
Redis replication は非同期である。
この 1 文が Sentinel の限界をほぼ決めている。
次の状態を考える。
クライアントは OK を受け取っている。
しかし、その書き込みが replica に届く前に master が落ちた場合、新 master はそのデータを持っていない。
これは Sentinel のバグではない。非同期 replication の物理的な帰結である。
write acknowledge と replica apply の間に時間差がある限り、そこが損失窓になる。
ネットワーク partition ではさらに悪くなる。
majority 側では R2 が新 master に昇格する。
一方で、minority 側に取り残されたクライアントが old master M1 に書けてしまうことがある。
partition が回復すると、M1 は新 master に追随する replica として再構成される。このとき M1 側にだけ存在した書き込みは捨てられる。
Redis 公式ドキュメントもこの問題を明示しており、min-replicas-to-write と min-replicas-max-lag によって損失窓を狭められるとしている。
min-replicas-to-write 1
min-replicas-max-lag 10
この設定を入れると、master は指定数の replica に書き込みを転送できていないと判断した場合、書き込みを受け付けなくなる。
ただし、これは無料ではない。
| 設定 | 得るもの | 失うもの |
|---|---|---|
min-replicas-to-write 0 |
master 単独でも書ける可用性 | acknowledged write の損失窓が広い |
min-replicas-to-write 1 |
replica 断絶時の損失窓を狭める | replica が足りないと master が書き込み拒否する |
min-replicas-max-lag を短くする |
古い replica への依存を減らす | ネットワーク揺れで write availability が落ちやすい |
キャッシュなら多少の損失は許容できる。
セッションや OAuth state でも、TTL 付き・再生成可能なデータなら許容しやすい。
しかし、決済・残高・監査ログのような acknowledged write を失ってはいけないデータを Redis + Sentinel だけに置くのは設計として無理がある。
Sentinel の設定ファイルは実行時に書き換わる
Sentinel は単なる静的設定ファイルの読者ではない。
Sentinel 自身が検出した replica、他の Sentinel、新しい master 構成、configuration epoch などを設定ファイルへ永続化する。
つまり、コンテナで Sentinel を動かすときに設定ファイルを read-only にしたり、再起動のたびに空の設定へ戻したりすると、収束に余計な時間がかかる。
Kubernetes や Docker で注意するべき点は 3 つある。
- Sentinel の設定ファイルを書き込み可能にする
- NAT / port mapping で Sentinel が誤ったアドレスを伝えないようにする
- hostname を返す場合、クライアントが hostname 応答に対応しているか確認する
Redis 公式ドキュメントは Docker や NAT と Sentinel の組み合わせに注意が必要だと明記している。Sentinel は Redis instance や Sentinel 同士を自動発見するため、外から見えるアドレスと内側で announce されるアドレスがズレると、クライアントが到達不能な master を掴む。
IdP で Sentinel を使うなら何を Redis に置くか
認証サーバーで Redis を使う場合、Redis に置くデータはだいたい 3 種類に分かれる。
| データ | Redis 向きか | 理由 |
|---|---|---|
| rate limit counter | 向いている | TTL 付きで再構築可能。多少の損失は制御緩和に留まる |
| OAuth state / nonce | 向いている | 短寿命。失われても再ログインで回復可能 |
| refresh token replay cache | 条件付き | DB を真実の源にし、Redis は高速判定に限定する |
| access token blacklist | 条件付き | TTL と DB fallback が必要 |
| 監査ログ | 向かない | acknowledged write を失うと証跡が欠ける |
| 課金・残高 | 向かない | 非同期 replication の損失窓を許容できない |
Redis + Sentinel は「失われても再生成できる制御データ」に向いている。
逆に、失った瞬間に業務整合性が壊れるデータは、DB transaction や commit log を真実の源に置くべきだ。
Hot path の観点では、Redis に置く値も小さくする。
例えばセッションや state は巨大 JSON を毎回 HGETALL するより、判定に必要な hot fields を分ける方が cache に優しい。
Hot path:
session:{id}:state -> small bitmask / status
session:{id}:expires -> unix timestamp
Cold path:
session:{id}:profile -> large JSON
Sentinel は master を切り替えてくれるが、巨大オブジェクトの読み書きコストは消してくれない。
failover 後の新 master は replica から昇格した直後で、接続再確立や replica 再同期の負荷を受ける。ここで hot path が大きな JSON を連打すると、復旧直後の tail latency が悪化する。
運用時に見るべきイベント
Sentinel は状態変化を Pub/Sub イベントとして出す。最低限、次のイベントはログやメトリクスで拾いたい。
| イベント | 意味 |
|---|---|
+sdown |
ある Sentinel が主観的に down と判断 |
+odown |
quorum により客観的 down へ昇格 |
+try-failover |
failover 試行開始 |
+elected-leader |
failover leader に選出 |
+promoted-slave |
replica が promote 対象になった |
+switch-master |
master address が切り替わった |
-sdown / -odown
|
down 状態から復帰 |
failover は「成功したか」だけでは足りない。
知りたいのは、どの段階で何 ms 使ったかである。
この図のどこが長いかで、対策は変わる。
- detection が長い:
down-after-millisecondsが大きすぎる - election が長い: Sentinel 間通信や majority 配置が悪い
- promotion が長い: replica の遅延、AOF/RDB、ディスク I/O を疑う
- client が長い: クライアント pool の再接続戦略が悪い
failover は必ず壊して測る
Sentinel は設定しただけでは信用できない。
本番に近い環境で、master を実際に止めて挙動を見る必要がある。
最低限の確認項目はこれである。
- master kill から新 master 書き込み可能までの時間
- アプリケーションが固定 IP を握ったままになっていないか
- connection pool が古い master への接続を捨てるか
- failover 中の write error が上位層で retry / fail fast されるか
- old master 復帰後、正しく replica に降格されるか
-
min-replicas-to-write有効時、replica 不足で write が止まることを検知できるか - Sentinel 設定ファイルが rewrite され、restart 後も構成が残るか
テスト用には次のような流れでよい。
# 現在の master を確認
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
# master を止める
docker stop redis-1
# failover イベントを見る
redis-cli -p 26379 PSUBSCRIBE '*'
# 新 master を確認
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
# アプリケーションから書けるか確認
redis-cli -h <new-master> -p 6379 SET failover:test ok
本番でやるなら、単に docker stop ではなく、ネットワーク partition、CPU stall、ディスク I/O stall も分けて試す。
Redis プロセスが死ぬケースと、PING に応答できないほど詰まるケースでは、Sentinel の見え方が違う。
まとめ
Sentinel の役割は明確である。
| Sentinel が保証するもの | Sentinel が保証しないもの |
|---|---|
| master の監視 | ゼロデータロス |
| quorum による ODOWN 判定 | 強整合 replication |
| majority による failover 承認 | sharding |
| replica promotion | クライアントの完全無停止 |
| master address discovery | 古い master への書き込み防止の完全保証 |
| 構成の eventual convergence | 業務データの整合性 |
Redis Sentinel を入れる価値はある。
ただし、それは「Redis が落ちても何も考えなくてよくなる」という意味ではない。
正しい使い方は次のようになる。
- Sentinel は master discovery と failover に使う
- クライアントは Sentinel 対応にする
- Redis の非同期 replication による損失窓を前提にする
- 必要なら
min-replicas-to-writeで損失窓を狭める - 失ってはいけないデータは DB や durable log を真実の源にする
- failover は定期的に壊して測る
Sentinel は制御 plane であって、魔法の耐障害レイヤーではない。
その境界を理解して使えば、単一 master Redis の運用はかなり安定する。境界を誤解して使えば、failover した瞬間に「切り替わったのにデータがない」という事故になる。
Discussion