强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Redis 传输协议精讲 / 09 - 哨兵协议

哨兵协议

9.1 Sentinel 概述

Redis Sentinel 是 Redis 的高可用方案,提供:

功能说明
监控(Monitoring)持续检查主从节点是否正常工作
通知(Notification)通过 Pub/Sub 通知客户端故障事件
自动故障转移(Automatic Failover)主节点不可用时自动选举新主节点
配置提供(Configuration Provider)客户端通过 Sentinel 获取当前主节点地址

架构

┌──────────┐  ┌──────────┐  ┌──────────┐
│ Sentinel │  │ Sentinel │  │ Sentinel │
│    1     │  │    2     │  │    3     │
└────┬─────┘  └────┬─────┘  └────┬─────┘
     │              │              │
     └──────────────┼──────────────┘
                    │ 监控
          ┌─────────┴─────────┐
          │                   │
     ┌────┴────┐         ┌────┴────┐
     │ Master  │ ──复制──→│ Slave   │
     └─────────┘         └─────────┘

9.2 Sentinel 通信协议

Sentinel 本身是一个特殊的 Redis 实例,使用标准的 Redis 协议进行通信。

Sentinel 端口

# 默认端口 26379(不是 6379)
redis-sentinel /path/to/sentinel.conf

# 或
redis-server /path/to/sentinel.conf --sentinel

Sentinel 之间的通信

Sentinel 之间通过 Pub/Sub 频道交换信息:

# Sentinel 订阅的频道
__sentinel__:hello

每个 Sentinel 定期向该频道发布消息,包含:

  • Sentinel 自身的信息(IP、端口、ID)
  • 主节点的信息(IP、端口、ID、配置纪元)
→ PUBLISH __sentinel__:hello "<sentinel-ip>,<sentinel-port>,<sentinel-runid>,<sentinel-epoch>,<master-name>,<master-ip>,<master-port>,<master-epoch>"

9.3 SENTINEL 命令

SENTINEL masters

获取所有被监控的主节点信息:

→ SENTINEL masters
← *1              ← 一个主节点
← %16             ← 16 个字段
← $4
← name
← $6
← mymaster
← $2
← ip
← $9
← 127.0.0.1
← $4
← port
← $4
← 6379
← ...

SENTINEL master

获取特定主节点的详细信息:

→ SENTINEL master mymaster
← %16
← $2
← ip
← $9
← 127.0.0.1
← $4
← port
← $4
← 6379
← $6
← runid
← $40
← <40字符的运行ID>
← ...

SENTINEL replicas

获取从节点信息:

→ SENTINEL replicas mymaster
← *1              ← 一个从节点
← %14
← $2
← ip
← $9
← 127.0.0.1
← $4
← port
← $4
← 6380
← ...

SENTINEL sentinels

获取其他 Sentinel 的信息:

→ SENTINEL sentinels mymaster
← *2              ← 两个其他 Sentinel
← %6
← $2
← ip
← $9
← 127.0.0.1
← $4
← port
← $4
← 26380
← ...

SENTINEL get-master-addr-by-name

获取主节点地址(最常用的命令):

→ SENTINEL get-master-addr-by-name mymaster
← *2
← $9
← 127.0.0.1
← $4
← 6379

9.4 健康检查机制

主观下线(SDOWN)

单个 Sentinel 认为主节点不可达:

# 配置(sentinel.conf)
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
参数说明
mymaster主节点名称
127.0.0.1 6379主节点地址
2法定人数(quorum),至少需要几个 Sentinel 同意
down-after-milliseconds多少毫秒无响应判定为 SDOWN

客观下线(ODOWN)

quorum 个 Sentinel 都认为主节点下线时,标记为客观下线:

SDOWN(单个 Sentinel)
    ↓ quorum 个 Sentinel 同意
ODOWN(客观下线)
    ↓ 触发故障转移
Failover(故障转移)

检测命令

# 查询主节点状态
redis-cli -p 26379 SENTINEL master mymaster

# 关键字段:
# flags: master / s_down / o_down
# num-other-sentinels: 其他 Sentinel 数量

9.5 故障转移流程

步骤详解

1. 主节点 SDOWN → 多个 Sentinel 确认 → ODOWN
2. Sentinel Leader 选举(Raft 协议)
3. Leader 选择最优从节点
4. Leader 向从节点发送 SLAVEOF NO ONE
5. 等待从节点晋升为主节点
6. 通知其他从节点复制新主节点
7. 通知客户端新主节点地址

从节点选择算法

1. 排除 SDOWN 或 ODOWN 的从节点
2. 排除最近 5 秒内无响应的从节点
3. 排除与旧主节点断开时间超过 (down-after-milliseconds * 10) 的从节点
4. 按优先级(slave-priority)排序
5. 优先级相同,按复制偏移量排序(数据最新的优先)
6. 偏移量相同,按 runid 排序(字典序最小的优先)

故障转移期间的协议交互

# Sentinel → 从节点
→ SLAVEOF NO ONE
← +OK

# Sentinel → 其他从节点
→ SLAVEOF <new-master-ip> <new-master-port>
← +OK

# Sentinel → 旧主节点(恢复后)
→ SLAVEOF <new-master-ip> <new-master-port>
← +OK

9.6 Sentinel Pub/Sub 通知

频道列表

Sentinel 通过 Pub/Sub 频道广播事件:

频道事件
+sdown主观下线
-sdown恢复在线
+odown客观下线
-odown恢复在线
+failover-state-reconf-slaves故障转移:重新配置从节点
+failover-detected检测到故障转移
+slave发现新从节点
+switch-master主节点切换(最重要
+sentinel发现新 Sentinel

订阅方式

# 订阅所有事件
→ SUBSCRIBE *
← *3
← $9
← subscribe
← $1
← *
← :1

# 订阅特定主节点的事件
→ SUBSCRIBE +switch-master

消息格式

*3
$14
+switch-master
$8
mymaster
$26
127.0.0.1 6379 127.0.0.1 6380

消息内容格式:<master-name> <old-ip> <old-port> <new-ip> <new-port>


9.7 客户端集成

自动发现主节点

import redis
from redis.sentinel import Sentinel

# 创建 Sentinel 实例
sentinel = Sentinel(
    [("127.0.0.1", 26379),
     ("127.0.0.1", 26380),
     ("127.0.0.1", 26381)],
    socket_timeout=0.5
)

# 获取主节点连接
master = sentinel.master_for("mymaster", socket_timeout=0.5)
master.set("key", "value")

# 获取从节点连接(只读)
slave = sentinel.slave_for("mymaster", socket_timeout=0.5)
value = slave.get("key")

手动实现 Sentinel 客户端

class SimpleSentinelClient:
    def __init__(self, sentinel_addrs):
        self.sentinel_addrs = sentinel_addrs
        self.master_addr = None

    def discover_master(self, master_name):
        """通过 Sentinel 发现主节点"""
        for host, port in self.sentinel_addrs:
            try:
                s = socket.create_connection((host, port), timeout=2)
                cmd = f"*3\r\n$8\r\nSENTINEL\r\n$28\r\nget-master-addr-by-name\r\n${len(master_name)}\r\n{master_name}\r\n"
                s.sendall(cmd.encode())

                # 读取响应(简化处理)
                resp = s.recv(4096).decode()
                # 解析两元素数组:[ip, port]
                lines = resp.split("\r\n")
                ip = lines[4]   # 跳过数组头和长度
                port = int(lines[6])
                s.close()
                return (ip, port)
            except:
                continue
        raise ConnectionError("All sentinels unreachable")

    def get_master(self, master_name):
        """获取主节点连接"""
        if self.master_addr is None:
            self.master_addr = self.discover_master(master_name)
        host, port = self.master_addr
        return socket.create_connection((host, port))

自动故障转移处理

import redis
import time

class ResilientRedisClient:
    """支持自动故障转移的 Redis 客户端"""

    def __init__(self, sentinel_addrs, master_name):
        self.sentinel = Sentinel(sentinel_addrs)
        self.master_name = master_name
        self._master = None

    @property
    def master(self):
        if self._master is None:
            self._master = self.sentinel.master_for(
                self.master_name,
                socket_timeout=2,
                retry_on_timeout=True
            )
        return self._master

    def execute_with_retry(self, func, max_retries=3):
        """执行命令,失败时自动重试"""
        for attempt in range(max_retries):
            try:
                return func(self.master)
            except (redis.ConnectionError, redis.TimeoutError):
                # 连接失败,可能发生了故障转移
                self._master = None  # 清除缓存,下次重新发现
                if attempt < max_retries - 1:
                    time.sleep(0.5 * (attempt + 1))
        raise redis.ConnectionError("All retries failed")

    def get(self, key):
        return self.execute_with_retry(lambda r: r.get(key))

    def set(self, key, value, **kwargs):
        return self.execute_with_retry(lambda r: r.set(key, value, **kwargs))


# 使用
client = ResilientRedisClient(
    [("127.0.0.1", 26379), ("127.0.0.1", 26380), ("127.0.0.1", 26381)],
    "mymaster"
)
client.set("key", "value")
print(client.get("key"))

9.8 Sentinel 配置管理

动态配置

# 添加新的主节点监控
SENTINEL MONITOR newmaster 127.0.0.1 6381 2

# 移除监控
SENTINEL REMOVE newmaster

# 修改配置参数
SENTINEL SET mymaster down-after-milliseconds 10000
SENTINEL SET mymaster failover-timeout 60000

配置持久化

Sentinel 会自动将运行时配置写入配置文件:

# sentinel.conf(自动生成的内容)
sentinel myid abc123...
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel config-epoch mymaster 0
sentinel leader-epoch mymaster 0
sentinel known-replica mymaster 127.0.0.1 6380
sentinel known-sentinel mymaster 127.0.0.1 26380 other-sentinel-id

9.9 Sentinel 集群拓扑

推荐部署

最少 3 个 Sentinel(奇数个,用于选举)
分布在不同的物理机器/可用区

Node 1: Redis Master (6379) + Sentinel (26379)
Node 2: Redis Slave  (6379) + Sentinel (26379)
Node 3: Redis Slave  (6379) + Sentinel (26379)

法定人数(Quorum)

# 法定人数 = 需要多少个 Sentinel 同意才能判定 ODOWN
sentinel monitor mymaster 127.0.0.1 6379 2

# 3 个 Sentinel 中有 2 个同意 → ODOWN
# 建议:quorum = (num_sentinels / 2) + 1

9.10 注意事项

⚠️ Sentinel 不是代理 客户端通过 Sentinel 获取主节点地址后,直接连接 Redis 实例。Sentinel 不转发数据。

⚠️ 网络分区 在网络分区场景下,可能出现脑裂(split-brain)。配置 min-replicas-to-write 可以缓解。

⚠️ 配置纪元(Configuration Epoch) Sentinel 使用 Raft 协议选举 Leader,使用纪元号确保配置一致性。纪元号越大表示配置越新。

⚠️ 客户端缓存 客户端应缓存主节点地址,并在连接失败时重新查询 Sentinel。不要每次都查询 Sentinel。


9.11 扩展阅读

资源说明
Redis Sentinel 文档官方文档
Sentinel 客户端指引客户端实现指南
Raft 共识算法Sentinel Leader 选举的基础

上一章:Lua 脚本协议 | 下一章:集群协议