🛰️

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 に同意した状態

ここで一番間違えやすいのは、quorummajority の違いである。

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-writemin-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 が落ちても何も考えなくてよくなる」という意味ではない。

正しい使い方は次のようになる。

  1. Sentinel は master discovery と failover に使う
  2. クライアントは Sentinel 対応にする
  3. Redis の非同期 replication による損失窓を前提にする
  4. 必要なら min-replicas-to-write で損失窓を狭める
  5. 失ってはいけないデータは DB や durable log を真実の源にする
  6. failover は定期的に壊して測る

Sentinel は制御 plane であって、魔法の耐障害レイヤーではない。
その境界を理解して使えば、単一 master Redis の運用はかなり安定する。境界を誤解して使えば、failover した瞬間に「切り替わったのにデータがない」という事故になる。

参考

Discussion