Redis 技术内幕——Redis Sentinel
Posted 一叶知秋V
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis 技术内幕——Redis Sentinel相关的知识,希望对你有一定的参考价值。
Redis Sentinel(哨兵)是 Redis 官方提供的集群管理工具,是 Redis 高可用的解决方案,本身是一个独立运行的进程,它可以监视多个 Master-Slave 集群,发现 Master 宕机之后,能进行自动切换,将该 Master 下的某个 Slave 晋升为 Master,继续处理请求。
Redis Sentinel 主要功能:
- 监控:检查主从服务器是否运行正常;
- 提醒:通过 API 向管理员或者其他应用程序发送故障通知;
- 自动故障迁移:主从切换。
Redis Sentinel 是一个分布式系统,你可以在一个架构中运行多个 Sentinel 进程,这些进程使用流言协议 (Gossip Protocols) 接收 Master 是否下线的信息,并使用投票协议来决定是否执行自动故障迁移,以及选择哪个 Slave 作为新的 Master。这个跟 Zookeeper 是比较类似的。
流言协议:
- 每个节点都随机地与对方通信,最终所有节点的状态达成一致;
- 种子节点定期随机向其他节点发送节点列表以及需要传播的消息;
- 不保证信息一定会传递给所有节点,但是最终会趋于一致。
1.架构说明
Redis Sentinel 架构:
Redis Sentinel 故障转移:
1、如果 master 宕机了,连接出现中断,多个 Sentinel 发现并确认 master 有问题;
2、选举出一个 Sentinel 作为领导;
3、选举一个 slave 作为新的 master;
4、通知其余 slave 成为新的 master 的 slave;
5、通知客户端主从变化;
6、Sentinel 等待老的 master 复活成为新 master 的 slave。
整个过程其实就是由我们手动处理故障变成了 Sentinel 进行故障发现、故障自动转移、通知客户端的过程。
Redis Sentinel 还可以监控多个 master-slave 集群,每个 master-slave 使用 master-name 作为标识,有效节省资源。
#安装与配置
最终要达到的效果如下:
Sentinel 的默认端口是 26379。生产环境中 Sentinel 节点个数应该为大于等于 3 且最好为奇数。
安装与配置大致过程如下:
1、配置开启主从节点;
2、配置开启 Sentinel 监控主节点(Sentinel 是特殊的 Redis,Sentinel 本身是不存储数据的,而且支持的命令非常有限,主要作用就是监控、完成故障转移、通知);
3、实际应该多机器部署 Sentinel,保证 Sentinel 高可用;
4、详细配置节点
详细配置过程:
Redis 主节点 redis/config/redis-7000.conf 配置(redis.conf 模板文件在 redis/redis.conf):
# 关闭保护模式
protected-mode no
# 配置启动端口
port 7000
# 配置后台启动
daemonize yes
# 修改pidfile指向路径 redis-$port.pid
pidfile /var/run/redis-7000.pid
# 日志记录方式 redis-$port.log
logfile "redis-7000.log"
# 配置dump数据存放目录
dir "/opt/soft/redis/data/"
# 配置dump数据文件名 redis-$port.rdb
dbfilename dump-7000.rdb
启动命令:
redis-server redis-7000.conf
Redis 从节点 redis/config/redis-7001.conf 配置(redis-7002.conf 只是端口号不同):
# 关闭保护模式
protected-mode no
# 配置启动端口
port 7001
# 配置后台启动
daemonize yes
# 修改pidfile指向路径 redis-$port.pid
pidfile /var/run/redis-7001.pid
# 日志记录方式 redis-$port.log
logfile "redis-7001.log"
# 配置dump数据存放目录
dir "/opt/soft/redis/data/"
# 配置dump数据文件名 redis-$port.rdb
dbfilename dump-7001.rdb
# 配置master的ip地址、端口,在Redis启动时会自动从master进行数据同步
slaveof 127.0.0.1 7000
启动命令:
redis-server redis-7001.conf
redis-server redis-7002.conf
Sentinel 节点 redis/config/redis-sentinel-26379.conf 配置(这里只给出其中一个配置,另外两个只是端口号不同,sentinel.conf 模板文件在 redis/sentinel.conf):
# 配置启动端口
port 26379
# 配置后台启动
daemonize yes
# 配置工作目录
dir "/opt/soft/redis/data/"
# 日志记录方式 redis-sentinel-$port.log
logfile "redis-sentinel-26379.log"
# sentinel监听master的名字、ip、端口、几个sentinel认为master有问题就发生故障转移(最好配置sentinel节点的二分之一加一)
sentinel monitor mymaster 127.0.0.1 7000 2
# 每一个sentinel节点都会向master节点、slave节点和其他sentinel节点1秒钟ping一次。在down-after-milliseconds毫秒内没有进行回复,则判定该节点失败
sentinel down-after-milliseconds mymaster 30000
# slave复制时可以并行的个数,建议1,减轻master的压力
sentinel parallel-syncs mymaster 1
# 故障转移时间
sentinel failover-timeout mymaster 180000
启动命令:
redis-sentinel redis-sentinel-26379.conf
redis-sentinel redis-sentinel-26380.conf
redis-sentinel redis-sentinel-26381.conf
Sentinel 会自动发现 Redis slave,并写入到 redis-sentinel-$port.conf,Sentinel 彼此之间也能自动感知到。
2.客户端连接
客户端初始化时连接的是 Sentinel 节点集合,不再是具体的 Redis 节点,但 Sentinel 只是配置中心不是代理。Sentinel 中的数据节点与普通数据节点没有区别。
1.请求响应流程
- 客户端遍历 Sentinel 节点集合,获取一个可用的 Sentinel 节点;
- 客户端执行一个 Sentinel 的 API:get-master-addr-by-name < masterName> 获取 master 节点真正的地址和端口;
- 客户端执行 role 或者 role replication 进行一次验证,验证是否是真的 master 节点;
- 连接 master 进行操作;
- 客户端会订阅 Sentinel 的某一个频道,这个频道里会有谁是 master 的一个变化,假如有变化,则发送消息给客户端,客户端再去新的 master 进行连接。
2.Java客户端连接
Jedis 方式:
JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(masterName, sentinelSet, poolConfig, timeout);
Jedis jedis = null;
try
jedis = jedisSentinelPool.getResource();
// jedis command
catch (Exception e)
logger.error("error", e);
finally
if(jedis != null)
jedis.close();
JedisSentinelPool 的实现原理:
先看下 JedisSentinelPool 的构造函数:
public JedisSentinelPool(String masterName, Set<String> sentinels,
final GenericObjectPoolConfig poolConfig, final int timeout)
this(masterName, sentinels, poolConfig, timeout, null, Protocol.DEFAULT_DATABASE);
public JedisSentinelPool(String masterName, Set<String> sentinels,
final GenericObjectPoolConfig poolConfig, final int connectionTimeout, final int soTimeout,
final String password, final int database, final String clientName)
this.poolConfig = poolConfig; //连接池配置
this.connectionTimeout = connectionTimeout; // 连接超时时间
this.soTimeout = soTimeout; // 读写超时时间
this.password = password; // 密码
this.database = database; // 数据库, Redis一共有16个数据库, 默认使用0
this.clientName = clientName; // 客户端名
HostAndPort master = initSentinels(sentinels, masterName); // 初始化sentinel
initPool(master); // 初始化连接池, 连接master
看下 initSentinels 方法:
private HostAndPort initSentinels(Set<String> sentinels, final String masterName)
HostAndPort master = null;
boolean sentinelAvailable = false;
// 遍历所有sentinel节点, 获取master节点
for (String sentinel : sentinels)
final HostAndPort hap = HostAndPort.parseString(sentinel);
Jedis jedis = null;
try
jedis = new Jedis(hap);
// 执行sentinel的API:get-master-addr-by-name <masterName>命令获取master节点真正的地址和端口
List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
sentinelAvailable = true;
if (masterAddr == null || masterAddr.size() != 2)
// 如果不可用则continue
continue;
// 如果可用则break
master = toHostAndPort(masterAddr);
break;
catch (JedisException e)
log.warn("Cannot get master address from sentinel running @ . Reason: . Trying next one.", hap, e.toString());
finally
if (jedis != null)
jedis.close();
if (master == null)
if (sentinelAvailable)
throw new JedisException("Can connect to sentinel, but " + masterName + " seems to be not monitored...");
else
throw new JedisConnectionException("All sentinels down, cannot determine where is " + masterName + " master is running...");
// 遍历所有sentinel节点, 订阅消息(主观下线、客观下线、领导者选举、主从切换)
for (String sentinel : sentinels)
final HostAndPort hap = HostAndPort.parseString(sentinel);
JedisSentinelPool.MasterListener masterListener = new JedisSentinelPool.MasterListener(masterName, hap.getHost(), hap.getPort());
// whether MasterListener threads are alive or not, process can be stopped
masterListener.setDaemon(true);
masterListeners.add(masterListener);
masterListener.start();
return master;
MasterListener 是一个线程:
protected class MasterListener extends Thread
...
@Override
public void run()
while (running.get())
j = new Jedis(host, port);
try
...
j.subscribe(new JedisPubSub()
@Override
public void onMessage(String channel, String message) // 订阅+switch-master(主从节点切换)频道
String[] switchMasterMsg = message.split(" ");
if (switchMasterMsg.length > 3)
if (masterName.equals(switchMasterMsg[0]))
// 重新初始化连接池
initPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
else
log.debug("Ignoring message on +switch-master for master name , our master name is ", switchMasterMsg[0], masterName);
else
log.error("Invalid message received on Sentinel : on channel +switch-master: ", host, port, message);
, "+switch-master");
catch (JedisException e)
...
finally
j.close();
最后看下 initPool 方法:
private void initPool(HostAndPort master)
synchronized(initPoolLock)
if (!master.equals(currentHostMaster))
currentHostMaster = master;
if (factory == null)
factory = new JedisFactory(master.getHost(), master.getPort(), connectionTimeout,
soTimeout, password, database, clientName);
initPool(poolConfig, factory);
else
factory.setHostAndPort(currentHostMaster);
internalPool.clear();
log.info("Created JedisPool to master at " + master);
这样就完成了整个 JedisSentinelPool 的实现。
2.故障转移实现原理
Redis Sentinel 通过三个定时任务实现了 Sentinel 节点对于主节点、从节点、其余 Sentinel 节点的监控。Sentinel 在对节点做失败判定时分为主观下线和客观下线。Sentinel 实现读写分离高可用依赖 Sentinel 节点的消息通知,获取 Redis 数据节点的状态变化。
1、三个定时任务
为了保证 Redis Sentinel 可以对 Redis 节点做失败判定以及做故障转移,在 Redis Sentinel 内部是有三个定时任务作为基础的:
- 每 10 秒每个 sentinel 对 master 和 slave 执行 info 操作;
- 发现 slave 节点;
- 确认主从关系;
- 每 2 秒每个 sentinel 通过 master 节点的 channel(频道)交换信息(pub/sub);
- 通过
__sentinel__:hello
频道交互; - 交互对节点的 “看法” 和自身信息;
- 通过
- 每 1 秒每个 sentinel 对其他 sentinel 和 redis 执行 ping;
2、三个消息
- +switch-master:切换主节点(从节点晋升为主节点);
- +convert-to-slave:切换从节点(原主节点降为从节点);
- +sdown:主观下线。
3、主观下线和客观下线
主观下线:单个 sentinel 节点对 Redis 节点做出的下线判断;
客观下线:超过 quorum(sentinel monitor < masterName> < ip> < port> < quorum> 配置)个 sentinel 节点对 Redis 节点做出的下线判断;
只有当 master 被认定为客观下线时,才会发生故障迁移。
4、领导者选举
完成故障转移的过程只需要一个 sentinel 来完成。
选举:通过 sentinel is-master-down-by-addr 命令都希望成为领导者。
sentinel 领导者选举使用的是 raft 算法,大致过程:
- 每一个做主观下线的 sentinel 节点向其他 sentinel 节点发送命令,要求将它设置为领导者;
- 收到命令的 sentinel 节点,如果在发生故障周期内没有同意其他 sentinel 节点发送的命令,那么将同意该请求,否则拒绝;
- 如果该 sentinel 节点发现自己的票数已经超过 sentinel 集合半数且超过 quorum(sentinel monitor 配置),那么它将称为领导者;
- 如果此过程有多个 sentinel 节点成为了领导者,那么将等待一段时间重新进行选举。
5、故障转移(sentinel 领导者节点完成)
- 从 slave 节点中选出一个 “合适的” 节点作为新的 master 节点;
- 对步骤 1 的 slave 节点执行 slaveof no one 命令让其成为 master 节点;
- 向剩余的 slave 节点发送命令,让它们成为新 master 节点的 slave 节点,复制规则和 parallel-syncs 参数有关;
- 对原来的 master 节点配置为 slave,并保持对其 “关注”,当其恢复后命令它去复制新的 master 节点。
选择 “合适的” slave 节点:
- 选择 slave-priority(slave 节点优先级)最高的 slave 节点,如果存在则返回,不存在则继续;
- 选择复制偏移量最大的 slave 节点(复制的最完整),如果存在则返回,不存在则继续;
- 选择 runid 最小的 slave 节点(启动最早的节点)。
3.常见开发运维问题
1.节点运维
节点运维场景问题:
- 机器下线,例如过保等情况。
- 机器性能不足。例如 CPU、内存、硬盘、网络等。
- 节点自身故障,例如服务不稳定。
主节点下线:主节点下线需要手动故障转移,通过 sentinel failover < masterName> 命令去给任意一个 sentinel 去执行,完成故障转移的过程。这个故障转移过程中,忽略了主观下线、客观下线、领导者选举,因为这个 sentinel 已经是领导者了,而且确定了要下线的 master,所以没有这些过程。
从节点/sentinel 节点下线:临时下线还是永久下线,例如是否做一些清理工作。但是要考虑读写分离的情况。
主节点上线:sentinel failover 进行替换。
从节点上线:slaveof 即可,sentinel 节点可以感知。
sentinel 节点上线:参考其他 sentinel 节点启动即可。
2.高可用读写分离
从节点的作用:
- 副本,高可用的基础;
- 扩展读能力;
以上是关于Redis 技术内幕——Redis Sentinel的主要内容,如果未能解决你的问题,请参考以下文章