本文讲述Redis高可用方案中的哨兵模式——Sentinel,RedisClient中的Jedis如何使用以及使用原理。html
Redis主从复制是Sentinel模式的基石,在学习Sentinel模式前,须要理解主从复制的过程。git
Redis主从复制的含义和Mysql的主从复制同样,即利用Slave从服务器同步Master服务器数据的副本。主从复制的最为关键的点在于主从数据的一致性,在Redis中主要经过如下三点:github
利用以上三点,Redis的主从复制保证数据的最终一致性。redis
假设有两台服务器,一台是Master,另外一台是Slave。如今需求是保证Master和Slave的数据一致性。
若是要保证精确的一致性,最好的方式是实时的进行全量同步,基于全量确定是一致的。可是这样形成的性能损耗必然不可估计。
增量同步即同步变化的数据,不一样步未发生变化的数据,虽然实现程度比全量复杂,可是能让性能提高。
Redis中实现主从复制是全量结合增量实现。sql
增量同步,必须获取主从服务器之间的数据差别,对于数据同一份数据的差别获取,最多见的方式即版本控制。如常见的版本控制系统: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
用于配置sentinel的名称,master节点,仲裁数。
举个例子,假设有5个sentinel进程:
down-after-milliseconds
用于配置sentinel认为Redis实例不可用的至少时间时多少,以毫秒为单位
sentinel parallel-syncs
用于配置故障转移后,同时进行从新配置slave节点的个数。从新配置slave时,则slave将没法处理客户端的查询请求。若是同时配置全部的slave,则将会出现,整个Redis不可用。可是若是该值较小,又会致使从新配置时间过长。须要trade off。
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模式中尽可能设置主从禁止自启动,或者主开启持久化功能。