标签:基本 变量 ack lock 消息 准备工作 事件通知 组成 建立
本文讲述Redis高可用方案中的哨兵模式——Sentinel,RedisClient中的Jedis如何使用以及使用原理。
Redis主从复制是Sentinel模式的基石,在学习Sentinel模式前,需要理解主从复制的过程。
Redis主从复制的含义和Mysql的主从复制一样,即利用Slave从服务器同步Master服务器数据的副本。主从复制的最为关键的点在于主从数据的一致性,在Redis中主要通过以下三点:
利用以上三点,Redis的主从复制保证数据的最终一致性。
假设有两台服务器,一台是Master,另一台是Slave。现在需求是保证Master和Slave的数据一致性。
如果要保证精确的一致性,最好的方式是实时的进行全量同步,基于全量肯定是一致的。但是这样造成的性能损耗必然不可估计。
增量同步即同步变化的数据,不同步未发生变化的数据,虽然实现程度比全量复杂,但是能让性能提升。
Redis中实现主从复制是全量结合增量实现。
增量同步,必须获取主从服务器之间的数据差异,对于数据同一份数据的差异获取,最常见的方式即版本控制。如常见的版本控制系统:svn、git等。在Redis主从关系中,数据的最初来源于Master,所以数据版本控制由Master控制。
Notes:
同一份数据的演变记录,最好的方式即版本控制
在Redis中,每个Master都有一个RelicationID,标识一个给定的历史数据集,是一串伪随机串。同时还有一个OffsetID,当Master将变化的数据发送给Slave时,发送多少个字节,相应的offsetID就增长多少,依据此做数据集的版本控制。即使没有Slave,Master也会增长OffsetID,一个RelicationID和OffsetID的组合都会标识一个数据集版本。
当Slave连接到Master时,Slave会向Master主动发送自己的RelicationID和OffsetID,Master依此判断Slave当前的数据版本,将变化的数据发送给Slave。当Slave发送的是一个未知的RelicationID和OffsetID,Master则会进行一次全同步。
Master会开启另一个复制进程。复制进程会创建一个持久化的RDB快照文件,并将新的请求命令缓冲在缓冲区中,达到Copy-On-Write的效果。在RDB文件创建完成后,会将RDB文件发送给Slave,Slave接收到后,将文件保存至磁盘,然后再载入内存。最后Master再将缓冲区的命令流发送给Slave,完成最终的数据同步。
对于主从复制还有很多特性,如:主从同步中的过期键处理,主从之间的认证,允许N个附加的副本,Slave只读模式等,可以参考:复制
Redis主从复制的配置比较简单,分为两种方式:静态文件配置和动态命令行配置。redis.conf中提供:
slaveof 192.168.1.1 6379
配置项用于配置Slave节点的Master节点,表示是谁的Slave。
同时还可以在redis-cli命令行中使用slaveof 192.168.1.1 6379格式的命令配置一个Slave的Master节点。可以使用slaveof no one取消其从节点的身份。
Redis已经具备了主从复制的功能,为什么仍然需要Sentinel模式?
Redis的主从模式从一定从程度上的确解决了可用性问题,这毋庸置疑。但是只仅仅主从复制来完成可用性,就比较简陋,灵活性不够,操作复杂。更不用说高可用!
基于以上的需求,Redis Sentinel是Redis提供的高可用的一种模型,在Sentinel模式下,无需人员的干预,Sentinel能够帮助完成以下工作:
Sentinel本身就是一个分布式系统。Sentinel基于一个配置运行多个进程协同工作,这些进程可以在一个服务器实例上,也可以分布在多个不同实例上。多个Sentinel工作有如下特点:
在Sentinel体系中,Sentinel、Redis实例和连接到Sentinel和Redis实例的应用这三者也共同组成了一个完整的分布式系统。
Redis中提供了搭建Sentinel的相关命令:redis-sentinel。其中Redis包中也包含了sentinel.conf的示例配置。
启动Sentinel实例,可以直接运行:
redis-sentinel sentinel.conf
但是在配置sentinel模式前,现需要做些准备工作:
关于Sentinel系统的其他关注点,请参考:Fundamental things to know about Sentinel before deploying
下面看下Sentinel的配置文件:
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000
sentinel monitor
举个例子,假设有5个sentinel进程:
down-after-milliseconds
sentinel parallel-syncs
sentinel failover-timeout
接下来实际演示配置Redis Sentinel过程:
准备环境,由于笔者没有如此多的服务器,虽然可以使用Docker,但是为了简单,直接使用一台机器,监听不同端口实现。
#sentinel实例
127.0.0.1:26379
127.0.0.1:26380
127.0.0.1:26381
127.0.0.1:26382
127.0.0.1:26383
#redis实例
127.0.0.1:6379
127.0.0.1:6380
127.0.0.1:6381
编写sentinel的配置:
port 26379
dir "/Users/xxx/redis/sentinel/data"
logfile "/Users/xxx/redis/sentinel/log/sentinel_26379.log"
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000
其他的sentinel实例配置依次类推,分别使用26380,26381,26382,26383端口,日志文件名称也做相应更换。主机节点使用127.0.0.1:6379。
配置6379端口的Redis实例如下:
port 6379
daemonize yes
logfile "/Users/xxx/redis/sentinel/log/6379.log"
dbfilename "dump-6379.rdb"
dir "/Users/xxx/redis/sentinel/data"
6380和6381端口另外再加上一行配置:slaveof 127.0.0.1 6379,表示slave节点。
再分别启动Redis实例和Sentinel实例:
redis-server redis6379.conf
....
redis-sentinel sentinel26379.conf &
启动结束后可以查找Redis的相关进程有:
501 2165 1 0 7:47下午 ?? 0:00.55 redis-server *:6379
501 2167 1 0 7:47下午 ?? 0:00.58 redis-server *:6380
501 2171 1 0 7:47下午 ?? 0:00.59 redis-server *:6381
501 2129 1890 0 7:39下午 ttys000 0:02.03 redis-sentinel *:26379 [sentinel]
501 2130 1890 0 7:39下午 ttys000 0:01.99 redis-sentinel *:26380 [sentinel]
501 2131 1890 0 7:39下午 ttys000 0:02.02 redis-sentinel *:26381 [sentinel]
501 2132 1890 0 7:39下午 ttys000 0:01.97 redis-sentinel *:26382 [sentinel]
501 2133 1890 0 7:39下午 ttys000 0:01.93 redis-sentinel *:26383 [sentinel]
表示整个Redis Sentinel模式搭建完毕!
可以使用redis-cli命令行连接到Sentinel查询相关信息
redis-cli -p 26379
#查询sentinel中的master节点信息和状态,考虑篇幅,这里只展示部分
127.0.0.1:26379> sentinel master mymaster
1) "name"
2) "mymaster"
3) "ip"
4) "127.0.0.1"
5) "port"
6) "6379"
7) "runid"
8) "67065dc606ffeb58d1b11e336bc210598743b676"
9) "flags"
10) "master"
11) "link-pending-commands"
#查询sentinel中的slaves节点信息和状态,考虑篇幅,这里只展示部分
127.0.0.1:26379> sentinel slaves mymaster
1) 1) "name"
2) "127.0.0.1:6381"
3) "ip"
4) "127.0.0.1"
5) "port"
6) "6381"
7) "runid"
8) "728f17ca3786e46cd28d76b94a1c62c7d7475d08"
9) "flags"
10) "slave"
这里可以将master节点的进程kill,sentinel会自动进行故障转移。
kill -9 2165
#再查询master时,sentinel已经进行了故障转移
127.0.0.1:26379> sentinel master mymaster
1) "name"
2) "mymaster"
3) "ip"
4) "127.0.0.1"
5) "port"
6) "6381"
7) "runid"
8) "728f17ca3786e46cd28d76b94a1c62c7d7475d08"
9) "flags"
10) "master"
sentinel get-master-addr-by-name mymaster命令用于获取master节点
Notes:
以上的sentinel配置中并没有配置slave相关的信息,只配置master节点。sentinel可以根据master节点获取所有的slave节点。
最后再来看下Sentinel中的Pub/Sub,Sentinel堆外提供了事件通知机制。Client可以订阅Sentinel的指定通道获取特定事件类型的通知。通道名称和事件名称相同,例如redis-cli - 23679登录sentinel,订阅subcribe +sdown通道,然后kill监听6379的Redis实例,则会收到如下通知:
1) "pmessage"
2) "*"
3) "+sdown"
4) "slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6381"
Redis Sentinel模式下的Client都是利用其特点,实现应用的故障自动转移。
关于Sentinel还有很多其他的功能特性,如:增加移除一个sentinel,增加移除slave等,更多细节,请参靠Redis Sentinel Documentation
前文中提到Redis Sentinel模式需要应用客户端的支持才能实现故障自动转移,切换至新提升的master节点上。同时也讲解Redis Sentinel系统提供了Pub/Sub的API供应用客户端订阅Sentinel的特定通道获取相应的事件类型的通知。
在Jedis中就是利用这些特点完成对Redis Sentinel模式的支持。下面循序渐进的探索Jedis中的Sentinel源码实现。
Jedis中实现Sentinel只有一个核心类JedisSentinelPool,该类实现了:
JedisSentinelPool直接提供了构造函数API,可以直接利用sentinel的信息集合构造JedisSentinelPool,其中的getResource直接返回与当前master相关的Jedis对象。
@Test
public void sentinel() {
Set<String> sentinels = new HashSet<>();
sentinels.add(new HostAndPort("localhost", 26379).toString());
sentinels.add(new HostAndPort("localhost", 26380).toString());
sentinels.add(new HostAndPort("localhost", 26381).toString());
sentinels.add(new HostAndPort("localhost", 26382).toString());
sentinels.add(new HostAndPort("localhost", 26383).toString());
String sentinelName = "mymaster";
JedisSentinelPool pool = new JedisSentinelPool(sentinelName, sentinels);
Jedis redisInstant = pool.getResource();
System.out.println("current host:" + redisInstant.getClient().getHost() +
", current port:" + redisInstant.getClient().getPort());
redisInstant.set("testK", "testV");
// 故障转移
Jedis sentinelInstant = new Jedis("localhost", 26379);
sentinelInstant.sentinelFailover(sentinelName);
System.out.println("current host:" + redisInstant.getClient().getHost() +
", current port:" + redisInstant.getClient().getPort());
Assert.assertEquals(redisInstant.get("testK"), "testV");
}
public class JedisSentinelPool extends JedisPoolAbstract {
// 连接池配置
protected GenericObjectPoolConfig poolConfig;
// 默认建立tcp连接的超时时间
protected int connectionTimeout = Protocol.DEFAULT_TIMEOUT;
// socket读写超时时间
protected int soTimeout = Protocol.DEFAULT_TIMEOUT;
// 认证密码
protected String password;
// Redis中的数据库
protected int database = Protocol.DEFAULT_DATABASE;
protected String clientName;
// 故障转移器,用于实现master节点切换
protected Set<MasterListener> masterListeners = new HashSet<MasterListener>();
protected Logger log = LoggerFactory.getLogger(getClass().getName());
// 创建与Redis实例的连接的工厂,使用volatile,保证多线程下的可见性
private volatile JedisFactory factory;
// 当前正在使用的master节点,使用volatile,保证多线程下的可见性
private volatile HostAndPort currentHostMaster;
}
JedisSentinelPool的构造函数被重载很多,但是其中最核心的构造函数如下:
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;
this.clientName = clientName;
// 初始化sentinel
HostAndPort master = initSentinels(sentinels, masterName);
// 初始化redis实例连接池
initPool(master);
}
继续看initSentinels过程
// sentinels是sentinel配置:ip/port
// masterName是sentinel名称
private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
HostAndPort master = null;
boolean sentinelAvailable = false;
log.info("Trying to find master from available Sentinels...");
// 循环处理每个sentinel,寻找master节点
for (String sentinel : sentinels) {
// 解析字符串ip:port -> HostAndPort对象
final HostAndPort hap = HostAndPort.parseString(sentinel);
log.debug("Connecting to Sentinel {}", hap);
Jedis jedis = null;
try {
// 创建与sentinel对应的jedis对象
jedis = new Jedis(hap);
// 从sentinel获取master节点
List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
// connected to sentinel...
sentinelAvailable = true;
// 如果为空,或者不是ip和port组成的size为2的list,则处理下一个sentinel
if (masterAddr == null || masterAddr.size() != 2) {
log.warn("Can not get master addr, master name: {}. Sentinel: {}", masterName, hap);
continue;
}
// 构造成表示master的HostAndPort对象
master = toHostAndPort(masterAddr);
log.debug("Found Redis master at {}", master);
// 寻找到master,跳出循环
break;
} catch (JedisException e) {
// resolves #1036, it should handle JedisException there‘s another chance
// of raising JedisDataException
log.warn(
"Cannot get master address from sentinel running @ {}. Reason: {}. Trying next one.", hap,
e.toString());
} finally {
if (jedis != null) {
jedis.close();
}
}
}
// 如果master为空,则sentinel异常,throws ex
if (master == null) {
if (sentinelAvailable) {
// can connect to sentinel, but master name seems to not
// monitored
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...");
}
}
log.info("Redis master running at " + master + ", starting Sentinel listeners...");
// 遍历sentinel集合,对每个sentinel创建相应的监视器
// sentinel本身是集群高可用,这里需要为每个sentinel创建监视器,监视相应的sentinel
// 即使sentinel挂掉一部分,仍然可用
for (String sentinel : sentinels) {
final HostAndPort hap = HostAndPort.parseString(sentinel);
// 创建sentinel监视器
MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
// whether MasterListener threads are alive or not, process can be stopped
// sentinel设置为守护线程
masterListener.setDaemon(true);
masterListeners.add(masterListener);
// 启动线程监听sentinel的事件通知
masterListener.start();
}
return master;
}
初始化sentinel中的主要逻辑分为两部分:
下面继续探索initPool方法,该方法以初始化setntinel中寻找的master节点为参数,进行初始化jedis与redis的master节点的JedisFactory。
// 该过程主要是为了初始化jedis与master节点的JedisFactory对象
// 一旦JedisFactory被初始化,应用就可以用其创建操作master节点相关的Jedis对象
private void initPool(HostAndPort master) {
// 判断当前的master节点是否与要设置的master相同,currentHostMaster是volatile变量
// 保证线程可见性
if (!master.equals(currentHostMaster)) {
// 如果不相等,则重新设置当前的master节点
currentHostMaster = master;
// 如果factory是空,则利用新的master创建factory
if (factory == null) {
factory = new JedisFactory(master.getHost(), master.getPort(), connectionTimeout,
soTimeout, password, database, clientName);
initPool(poolConfig, factory);
} else {
// 否则更新factory中的master节点
factory.setHostAndPort(currentHostMaster);
// although we clear the pool, we still have to check the
// returned object
// in getResource, this call only clears idle instances, not
// borrowed instances
internalPool.clear();
}
log.info("Created JedisPool to master at " + master);
}
}
initPool中完成了应用于redis的master节点的连接创建,Jedis对象工厂的创建。
这样应用就可以使用JedisSentinelPool的getResource方法获取与master节点对应的Jedis对象对master节点进行读写。这些步骤主要用于应用启动时执行与master节点的初始化操作。但是在应用运行期间,如果sentinel的master发生故障转移,应用如何实现自动切换至新的master节点,这样的功能主要是sentinel监视器MasterListener完成。接下来主要分析MasterListener的实现。
// MasterListener本身是一个线程对象的实现,所以sentinel模式中有几个sentinel进程
// 应用就会为其创建多少个相对应的线程监听,这样主要是为了保证sentinel本身的高可用
protected class MasterListener extends Thread {
// sentinel的名称,应用同样的Redis实例群体可以组建不同的sentinel
protected String masterName;
// 对应的sentinel host
protected String host;
// 对应的端口
protected int port;
// 订阅重试的等待时间,前文中介绍,实现自动故障转移的核心是利用sentinel提供的
// pub/sub API,实现订阅相应类型通道,接受相应的事件通知
protected long subscribeRetryWaitTimeMillis = 5000;
// 与sentinel连接操作的Jedis
protected volatile Jedis j;
// 表示对应的sentinel是否正在运行
protected AtomicBoolean running = new AtomicBoolean(false);
protected MasterListener() {
}
public MasterListener(String masterName, String host, int port) {
super(String.format("MasterListener-%s-[%s:%d]", masterName, host, port));
this.masterName = masterName;
this.host = host;
this.port = port;
}
public MasterListener(String masterName, String host, int port,
long subscribeRetryWaitTimeMillis) {
this(masterName, host, port);
this.subscribeRetryWaitTimeMillis = subscribeRetryWaitTimeMillis;
}
}
实现自动转移至新提升的master节点的逻辑在run方法中
@Override
public void run() {
// 线程第一次启动时,设置sentinel运行标识为true
running.set(true);
// 如果该sentinel仍然活跃,则循环
while (running.get()) {
// 创建与该sentinel对应的jedis对象,用于操作该sentinel
j = new Jedis(host, port);
try {
// 再次检查,因为在以上的操作期间,该sentinel可能会销毁,可以查看shutdown方法
// double check that it is not being shutdown
if (!running.get()) {
break;
}
/*
* Added code for active refresh
*/
// 获取sentinel中的master节点
List<String> masterAddr = j.sentinelGetMasterAddrByName(masterName);
if (masterAddr == null || masterAddr.size() != 2) {
log.warn("Can not get master addr, master name: {}. Sentinel: {}:{}.",masterName,host,port);
}else{
// 如果master合法,则调用initPoolf方法初始化与master节点的JedisFactory
initPool(toHostAndPort(masterAddr));
}
// 订阅该sentinel的+switch-master通道。+switch-master通道的事件类型为故障转移,切换新的master的事件类型
j.subscribe(new JedisPubSub() {
// redis sentinel中一旦发生故障转移,切换master。就会收到消息,消息内容为新提升的master节点
@Override
public void onMessage(String channel, String message) {
log.debug("Sentinel {}:{} published: {}.", host, port, message);
// 解析消息获取新提升的master节点
String[] switchMasterMsg = message.split(" ");
if (switchMasterMsg.length > 3) {
if (masterName.equals(switchMasterMsg[0])) {
// 将应用的当前master改为新提升的master,初始化。实现应用端的故障转移
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) {
// 如果繁盛异常,判断对应的sentinel是否仍然处于运行状态
if (running.get()) {
// 如果是处于运行,则是连接问题,线程睡眠subscribeRetryWaitTimeMillis毫秒,然后while循环继续订阅+switch-master通道
log.error("Lost connection to Sentinel at {}:{}. Sleeping 5000ms and retrying.", host,
port, e);
try {
Thread.sleep(subscribeRetryWaitTimeMillis);
} catch (InterruptedException e1) {
log.error("Sleep interrupted: ", e1);
}
} else {
log.debug("Unsubscribing from Sentinel at {}:{}", host, port);
}
} finally {
j.close();
}
}
}
以上的应用端实现故障发生时自动切换master节点的逻辑,注释已经讲述的非常清晰。这里需要关注的几点问题:
因为sentinel进程可能有多个,保证自身高可用。所以这里MasterListener对应也有多个,所以对于实现切换master节点是多线程环境。其中优秀的地方在于没有使用任何的同步,只是利用volatile保证可见性。因为对currentMaster和factory变量的操作,都只是赋值操作;
因为是多线程,所以initPool会被调用多次。一个是应用启动的main线程,还有就是N个sentinel对应的MasterListener监听线程。所以initPool被调用N+1次,同时发生故障转移时,将会被调用N次。但是即使是多次初始化,master的参数都是一样,基本上不会出现线程安全问题;
到这里,Redis的Sentinel模式和Jedis中实现应用端的故障自动转移就探索结束。下面再总结下Redis Sentinel模式在保证高可用的前提下的缺陷。
Redis Setninel模式固然结局了Redis单机的单点问题,实现高可用。但是它是基于主从模式,无论任何主从的实现,其中最为关键的点就是数据一致性。在软件架构中两者数据一致性的实现方式可谓五花八门:
在主从模式中,实现一致性,大多数是利用异步复制的方式,如:binlog、dumpfile、commandStream等等,且又分为全量和增量方式结合使用。
经过以上描述,提出的问题:
在使用主从模式中,很多情况下为保证性能,常将master的持久化关闭,所以经常会出现主从全部宕机,当主从自启动后,出现master的键空间为空,从又异步同步主,导致从同步空的过来,导致主从数据都出现丢失!
在Redis Sentinel模式中尽量设置主从禁止自启动,或者主开启持久化功能。
Redis Sentinel Documentation
jedis
标签:基本 变量 ack lock 消息 准备工作 事件通知 组成 建立
原文地址:https://www.cnblogs.com/lxyit/p/9829120.html