Redis Sentinel(哨兵)是 Redis 官方提供的集群管理工具,是 Redis 高可用的解决方案,本身是一个独立运行的进程,它可以监视多个 Master-Slave 集群,发现 Master 宕机之后,能进行自动切换,将该 Master 下的某个 Slave 晋升为 Master,继续处理请求。
Redis Sentinel 主要功能:
Redis Sentinel 是一个分布式系统,你可以在一个架构中运行多个 Sentinel 进程,这些进程使用流言协议 (Gossip Protocols) 接收 Master 是否下线的信息,并使用投票协议来决定是否执行自动故障迁移,以及选择哪个 Slave 作为新的 Master。这个跟 Zookeeper 是比较类似的。
流言协议:
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 彼此之间也能自动感知到。
客户端初始化时连接的是 Sentinel 节点集合,不再是具体的 Redis 节点,但 Sentinel 只是配置中心不是代理。Sentinel 中的数据节点与普通数据节点没有区别。
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 命令获取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 的实现。
Redis Sentinel 通过三个定时任务实现了 Sentinel 节点对于主节点、从节点、其余 Sentinel 节点的监控。Sentinel 在对节点做失败判定时分为主观下线和客观下线。Sentinel 实现读写分离高可用依赖 Sentinel 节点的消息通知,获取 Redis 数据节点的状态变化。
1、三个定时任务
为了保证 Redis Sentinel 可以对 Redis 节点做失败判定以及做故障转移,在 Redis Sentinel 内部是有三个定时任务作为基础的:
__sentinel__:hello
频道交互;2、三个消息
3、主观下线和客观下线
主观下线:单个 sentinel 节点对 Redis 节点做出的下线判断;
客观下线:超过 quorum(sentinel monitor < masterName> < ip> < port> < quorum> 配置)个 sentinel 节点对 Redis 节点做出的下线判断;
只有当 master 被认定为客观下线时,才会发生故障迁移。
4、领导者选举
完成故障转移的过程只需要一个 sentinel 来完成。
选举:通过 sentinel is-master-down-by-addr 命令都希望成为领导者。
sentinel 领导者选举使用的是 raft 算法,大致过程:
5、故障转移(sentinel 领导者节点完成)
选择 “合适的” slave 节点:
节点运维场景问题:
主节点下线:主节点下线需要手动故障转移,通过 sentinel failover < masterName> 命令去给任意一个 sentinel 去执行,完成故障转移的过程。这个故障转移过程中,忽略了主观下线、客观下线、领导者选举,因为这个 sentinel 已经是领导者了,而且确定了要下线的 master,所以没有这些过程。
从节点/sentinel 节点下线:临时下线还是永久下线,例如是否做一些清理工作。但是要考虑读写分离的情况。
主节点上线:sentinel failover 进行替换。
从节点上线:slaveof 即可,sentinel 节点可以感知。
sentinel 节点上线:参考其他 sentinel 节点启动即可。
从节点的作用: