IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。
上一篇我们搭建了 Redis 主从架构,实现了数据冗余和读写分离。但有个致命问题:当主节点宕机时,必须手动将某个从节点提升为主节点,再通知其他从节点和所有应用修改连接地址。这个过程少则几分钟,多则半小时,对于核心业务来说完全不可接受。
Redis Sentinel(哨兵) 就是为解决这个问题而生的。它能自动监控主从节点、自动发现故障、自动完成故障转移,让 Redis 真正具备生产级高可用能力。本文带你从原理到实战,用 Docker 搭建完整哨兵集群,并用 Python 客户端对接,感受自动故障转移的丝滑体验。
1. Sentinel 是什么?解决了什么问题?
Sentinel 是一套独立的分布式监控系统,它由多个哨兵进程组成,共同协作完成三个核心任务:
-
监控(Monitoring):持续检查主节点和从节点是否正常运行。
-
通知(Notification):当节点状态发生变化时,通过 API 或脚本通知管理员和其他应用。
-
自动故障转移(Automatic Failover):当主节点不可用时,自动选举一个从节点升级为新主节点,并让其他从节点和新主节点同步。
架构图:
bash
┌──────────────┬──────────────┬──────────────┐
│ Sentinel 1 │ Sentinel 2 │ Sentinel 3 │ ← 哨兵集群(至少 3 个)
└──────┬───────┴──────┬───────┴──────┬───────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Master │───>│ Slave 1 │ │ Slave 2 │
└──────────┘ └──────────┘ └──────────┘
💡 Sentinel 本身不存储数据,它是独立于 Redis 数据节点的控制平面。即使没有 Sentinel,Redis 也能正常工作;加上 Sentinel,就拥有了自动化的"守护神"。
2. 核心原理:主观下线与客观下线
哨兵判断一个节点是否故障,用了两层机制。
2.1 主观下线(SDOWN)
每个哨兵进程每隔 1 秒向主节点、从节点、其他哨兵发送 PING 命令。如果某个节点在 down-after-milliseconds(配置项,默认 30 秒)时间内没有有效回复,这个哨兵自身 就会标记该节点为主观下线(Subjectively Down)。
但单个哨兵的判断可能失误------比如自己的网络出了故障。所以不能仅凭 SDOWN 就发起故障转移。
2.2 客观下线(ODOWN)
当某个哨兵发现主节点进入 SDOWN 后,它会询问其他哨兵:"你们也觉得主节点挂了吗?" 当达到 quorum(法定人数,通常为哨兵数量的半数以上)个哨兵都认为主节点下线,该节点就被标记为客观下线(Objectively Down)。
此时,哨兵集群会通过 Raft 算法 选举出一个哨兵领导者,由它来执行故障转移。
2.3 故障转移流程
-
哨兵领导者从所有健康的从节点中,按优先级、复制偏移量、runid 等规则选出最合适的一个。
-
向被选中的从节点发送
SLAVEOF NO ONE,将其提升为主节点。 -
向其他从节点发送
SLAVEOF 新主IP 新主端口,让它们同步新主。 -
将旧主节点的地址更新到哨兵配置中,一旦旧主恢复,它将成为新主的从节点。
整个过程通常在 10~30 秒内完成。
⚠️ Sentinel 只对主节点做客观下线判断。从节点和哨兵节点只需要主观下线即可做出反应。
3. 关键配置参数详解
在开始实战之前,先理解 Sentinel 的核心配置项:
bash
# sentinel.conf 核心配置
# 监控的主节点:名称、IP、端口、quorum(法定人数)
sentinel monitor mymaster 127.0.0.1 6379 2
# 主观下线判定超时(毫秒),超过此时间无响应则 SDOWN
sentinel down-after-milliseconds mymaster 5000
# 故障转移最大超时(毫秒),超过此时间未完成则重新选举领导者
sentinel failover-timeout mymaster 10000
# 新主节点同步时,允许同时同步的从节点数量(越小同步越慢,但主节点压力小)
sentinel parallel-syncs mymaster 1
# 哨兵端口
port 26379
📌 quorum 建议 :3 个哨兵设
quorum=2;5 个哨兵设quorum=3。这保证即使少数哨兵故障,也能正常完成故障转移。
4. 实战:Docker 搭建 Sentinel 集群
我们搭建一个"1 主 2 从 + 3 哨兵"的完整环境。
4.1 整体架构
bash
Sentinel 集群(26379/26380/26381)
│
▼
主节点 Master(6379)
│
┌───┴───┐
▼ ▼
Slave1(6380) Slave2(6381)
4.2 创建网络和主从节点
bash
# 创建网络
docker network create sentinel-net
# 启动主节点
docker run -d --name redis-master --network sentinel-net \
-p 6379:6379 redis:7.2 redis-server --appendonly yes
# 启动从节点 1
docker run -d --name redis-slave1 --network sentinel-net \
-p 6380:6379 redis:7.2 redis-server \
--appendonly yes --slaveof redis-master 6379
# 启动从节点 2
docker run -d --name redis-slave2 --network sentinel-net \
-p 6381:6379 redis:7.2 redis-server \
--appendonly yes --slaveof redis-master 6379
验证主从状态:
bash
docker exec redis-master redis-cli INFO replication | grep connected_slaves
# connected_slaves:2
4.3 配置并启动 3 个哨兵
首先在宿主机创建哨兵配置文件(3 份):
bash
# 创建目录
mkdir -p ~/sentinel/conf
# 哨兵配置文件
cat > ~/sentinel/conf/sentinel.conf << 'EOF'
port 26379
dir /data
sentinel monitor mymaster redis-master 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
EOF
# 复制为 3 份(端口不同)
cp ~/sentinel/conf/sentinel.conf ~/sentinel/conf/sentinel1.conf
cp ~/sentinel/conf/sentinel.conf ~/sentinel/conf/sentinel2.conf
cp ~/sentinel/conf/sentinel.conf ~/sentinel/conf/sentinel3.conf
⚠️ 实际生产中 3 个哨兵最好部署在不同物理机上,这里仅演示原理,都放在同一台机器。
启动哨兵容器:
bash
# 哨兵 1 (端口 26379)
docker run -d --name sentinel1 --network sentinel-net \
-p 26379:26379 \
-v ~/sentinel/conf/sentinel1.conf:/etc/redis/sentinel.conf \
redis:7.2 redis-sentinel /etc/redis/sentinel.conf
# 哨兵 2 (端口 26380)
docker run -d --name sentinel2 --network sentinel-net \
-p 26380:26379 \
-v ~/sentinel/conf/sentinel2.conf:/etc/redis/sentinel.conf \
redis:7.2 redis-sentinel /etc/redis/sentinel.conf
# 哨兵 3 (端口 26381)
docker run -d --name sentinel3 --network sentinel-net \
-p 26381:26379 \
-v ~/sentinel/conf/sentinel3.conf:/etc/redis/sentinel.conf \
redis:7.2 redis-sentinel /etc/redis/sentinel.conf
4.4 验证哨兵状态
连接到任意哨兵查看信息:
bash
docker exec -it sentinel1 redis-cli -p 26379
127.0.0.1:26379> SENTINEL masters
1) 1) "name"
2) "mymaster"
3) "ip"
4) "redis-master"
5) "port"
6) "6379"
7) "flags"
8) "master"
9) "num-slaves"
10) "2"
...
127.0.0.1:26379> SENTINEL slaves mymaster
1) 1) "name"
2) "172.18.0.3:6379"
3) "ip"
4) "172.18.0.3"
...
127.0.0.1:26379> SENTINEL sentinels mymaster
(列出所有监控 mymaster 的哨兵)
看到这些信息,说明 3 个哨兵已经成功发现主节点和从节点,系统处于健康监控状态。
5. 模拟故障转移:亲眼见证自动切换
5.1 停止主节点
等待约 5~10 秒(down-after-milliseconds 为 5000ms),观察哨兵日志:
bash
docker logs sentinel1 --tail 20
关键日志行:
bash
+sdown master mymaster redis-master 6379
+odown master mymaster redis-master 6379 #quorum 2/2
+try-failover master mymaster redis-master 6379
+vote-for-leader ...
+elected-leader master mymaster redis-master 6379
+failover-state-select-slave master mymaster redis-master 6379
+selected-slave slave 172.18.0.3:6379 ... redis-slave1 6379
+failover-state-send-slaveof-noone slave redis-slave1:6379
+failover-state-wait-promotion slave redis-slave1:6379
+promoted-slave slave redis-slave1:6379
+failover-state-reconf-slaves master mymaster ...
+slave-reconf-sent slave redis-slave2:6379
+failover-end master mymaster redis-master 6379
+switch-master mymaster redis-master 6379 172.18.0.3 6379
日志清晰地展示了完整过程:检测主观下线 → 达成客观下线共识 → 选举哨兵领导 → 选择最佳从节点 → 提升为新主 → 重配置其他从节点。
5.2 验证新主节点
bash
# 查看新主状态
docker exec redis-slave1 redis-cli INFO replication | grep role
# role:master
# 查看原从节点 2 是否指向新主
docker exec redis-slave2 redis-cli INFO replication | grep master_host
# master_host:172.18.0.3 (即 redis-slave1)
原主节点恢复后会自动变成从节点:
bash
docker start redis-master
# 几秒后
docker exec redis-master redis-cli INFO replication | grep role
# role:slave
6. Python 客户端对接 Sentinel
最关键的环节来了:应用如何自动感知主节点切换?答案是使用 Sentinel 感知的客户端。
6.1 安装 redis-py
6.2 Sentinel 连接方式
redis-py 提供了 Sentinel 类来管理主从发现:
bash
from redis.sentinel import Sentinel
import time
# 配置所有哨兵地址
sentinel_hosts = [
('localhost', 26379),
('localhost', 26380),
('localhost', 26381),
]
# 创建 Sentinel 客户端
sentinel = Sentinel(sentinel_hosts, socket_timeout=0.5)
# 从哨兵获取当前主节点和从节点
master = sentinel.master_for('mymaster', socket_timeout=0.5, decode_responses=True)
slave = sentinel.slave_for('mymaster', socket_timeout=0.5, decode_responses=True)
# 写操作:走主节点
master.set('username', 'IT策士')
master.set('counter', 100)
print(f"写入完成: username={master.get('username')}, counter={master.get('counter')}")
# 读操作:走从节点
print(f"从节点读取: username={slave.get('username')}, counter={slave.get('counter')}")
# 获取当前主节点的实际地址
master_addr = sentinel.discover_master('mymaster')
print(f"当前主节点地址: {master_addr}")
slaves = sentinel.discover_slaves('mymaster')
print(f"当前从节点列表: {slaves}")
输出示例:
bash
写入完成: username=IT策士, counter=100
从节点读取: username=IT策士, counter=100
当前主节点地址: ('172.18.0.3', 6379)
当前从节点列表: [('172.18.0.4', 6379), ('172.18.0.2', 6379)]
6.3 验证故障转移对客户端透明
编写一个持续读写的脚本来测试:
bash
from redis.sentinel import Sentinel
import time
sentinel = Sentinel([
('localhost', 26379),
('localhost', 26380),
('localhost', 26381),
], socket_timeout=0.5)
master = sentinel.master_for('mymaster', socket_timeout=0.5,
decode_responses=True, retry_on_timeout=True)
print("开始持续写入...")
for i in range(1000):
try:
key = f'key:{i % 10}'
value = f'value:{i}'
master.set(key, value)
result = master.get(key)
print(f"[{i:03d}] SET/GET {key} = {result}")
time.sleep(0.1)
except Exception as e:
print(f"[{i:03d}] 错误: {e}")
time.sleep(0.5) # 等待故障转移完成
print("测试结束")
在脚本运行期间,用 docker stop redis-slave1 停掉当前主节点,观察输出:
bash
[042] SET/GET key:2 = value:42
[043] SET/GET key:3 = value:43
[044] SET/GET key:4 = value:44
[045] 错误: Connection refused
[046] 错误: Connection refused
[047] SET/GET key:7 = value:47 ← 恢复!自动连上新主
[048] SET/GET key:8 = value:48
客户端在短暂报错后自动恢复,无需任何人工干预。这就是 Sentinel 的核心价值。
6.4 封装高可用客户端类
bash
from redis.sentinel import Sentinel
from typing import List, Tuple
class RedisHA:
"""Sentinel 高可用 Redis 客户端"""
def __init__(self, sentinel_hosts: List[Tuple[str, int]], service_name: str = 'mymaster',
socket_timeout: float = 0.5, decode_responses: bool = True):
self.sentinel = Sentinel(sentinel_hosts, socket_timeout=socket_timeout)
self.service_name = service_name
self.decode_responses = decode_responses
self._master = None
self._slave = None
@property
def master(self):
"""获取主节点连接(自动发现)"""
return self.sentinel.master_for(
self.service_name, socket_timeout=0.5,
decode_responses=self.decode_responses,
retry_on_timeout=True
)
@property
def slave(self):
"""获取从节点连接(轮询)"""
return self.sentinel.slave_for(
self.service_name, socket_timeout=0.5,
decode_responses=self.decode_responses,
retry_on_timeout=True
)
def get_master_addr(self):
"""获取当前主节点地址"""
return self.sentinel.discover_master(self.service_name)
def get_slaves(self):
"""获取所有从节点"""
return self.sentinel.discover_slaves(self.service_name)
def set(self, key, value, **kwargs):
return self.master.set(key, value, **kwargs)
def get(self, key):
return self.slave.get(key)
def delete(self, key):
return self.master.delete(key)
# 使用
cache = RedisHA(
sentinel_hosts=[('localhost', 26379), ('localhost', 26380), ('localhost', 26381)]
)
cache.set('app:version', '3.0')
print(cache.get('app:version'))
print(f'当前主节点: {cache.get_master_addr()}')
7. 生产环境最佳实践
-
哨兵数量:至少 3 个,且部署在不同物理机或可用区。偶数个哨兵会增加投票平局的概率。
-
quorum 设置 :
sentinel_num / 2 + 1,例如 3 哨兵设 2,5 哨兵设 3。 -
客户端超时和重试 :设置
socket_timeout和retry_on_timeout,避免因网络抖动抛异常。 -
监控哨兵本身 :哨兵也可能挂,需要监控哨兵进程的存活和
SENTINEL masters的正确性。 -
避免在故障转移期间操作:故障转移一般 10~30 秒,此期间短暂不可写入是正常现象。
-
不要将哨兵和数据节点混布在同一台机器:否则机器宕机时数据节点和哨兵一起挂。
8. 动手试试
-
模拟主节点宕机 :在 Python 持续写入时,
docker stop主节点,观察恢复时间。 -
模拟哨兵宕机:停掉 1 个哨兵,观察剩下 2 个是否仍能正常完成故障转移(quorum=2)。
-
查看故障转移后配置 :检查哨兵配置文件,看
sentinel monitor行是否自动更新为新主地址。 -
网络分区模拟:通过 Docker 网络隔离主节点,观察哨兵的行为和客户端恢复。
预期效果:主节点宕机后 10~20 秒自动恢复写入;单哨兵宕机不影响集群可用性;配置文件自动更新;网络分区恢复后旧主变为从。
9. 总结
Sentinel 让 Redis 从"单点可用"跃升为"真正的自动高可用"。但它无法解决写的横向扩展------所有写入仍然只能走一个主节点。下一篇,我们将迎来 Redis 的终极形态:Redis Cluster,用哈希槽实现数据分片,彻底突破单机瓶颈。
想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维 !