Jedis第一讲-集群断连引发的超时问题

 

Jedis是开源的面向javaRedis数据库客户端工具包之一,目前github版本是3.1.0 (https://github.com/xetorthio/jedis/releases)。对于Spring Boot项目来说,starter-data-redis:1.5.12.release  版本所依赖的redis Client包是2.9.0版本。从maven的依赖引用上看,官方引用最多的也是此版本。所以本文在源码讲解上也是依据2.9.0版本进行说明的。

众所周知,所有项目对于redis数据库的依赖无非出于以下两种原因:一方面,依赖其内存数据库特性,将redis作为分布式缓存使用,同时以降低对关系型数据库的OPS操作;另一方面,依赖其单线程执行的特性,以实现分布式锁的功能。大多数业务场景,我们还是将redis作为了文本型的缓存服务器在使用,以提高请求的相应效率,降低平响时间,增加服务的吞吐。但是,今天我们聊一下在集群环境下,集群整体不可用时,产生的redis超时过长,最终拖慢请求的场景。
Jedis第一讲-集群断连引发的超时问题-LMLPHP
5主5从 Redis集群
在大多数应用中,我们都会采取redis集群的方式,以保证服务的高可用和高吞吐,有时也会多个应用服务连接一个redis集群。已达到资源的高利用率。在正常情况下集群方式提高了redis整体的吞吐能力,同时通过主-从方式可以实现故障偏移(主节点挂掉,由从节点升级为主节点)。在集群模式下,redis16384slot会被平摊到5个主节点上,此时也起到了负载均衡的效果,将流量进行了分流。(这也添加了另外一个名词:热key问题)。已上为集群模式相比单例模式带来的优势。然而,今天我们要聊的就是这种集群模式下,由于集群整体不可用时,Jedis一次请求操作超时远远超出设置的 timeout 时间问题。
Jedis第一讲-集群断连引发的超时问题-LMLPHP
Redis集群不可用

正常情况下,如果集群中某一台机器挂掉,redis集群会执行故障偏移策略,将其对应的从节点升级为主节点,超时时间将为一个timeout,。然而,当整个集群与应用服务之间出现了网络断链时,超时时间=timeout* 集群节点数(主+从)。这是为什么呢?是因为在集群模式下,如果某个节点挂掉,集群会执行故障偏移,偏移过程中涉及到新的主节点选举以及新的从节点加入。Jedis为了能够获取到新的集群信息,会从原始的集群信息列表中某一个节点开始遍历,如果能够连接该节点,就从该节点获取整个集群的配置信息和slot分布。如果当前节点已经断连,Jedis会获取另外一个节点信息获取集群配置,直到遍历到最后一个节点为止。所以在整个过程中如果整个集群断连,那么一次请求timeout时间 >= timeout * 节点数。

Jedis Client(2.9.0)版本关于上述业务处理的代码主要在JedisClusterCommand类的runWithRetries方法中。
Jedis第一讲-集群断连引发的超时问题-LMLPHP

该方法有四个参数:

Key :  要进行操作的key

Attempts : 请求重试次数

TryTrandomNode : 是否尝试随机选择一个节点进行

Asking  :  是否处理asking 链接(可以了解一下asking  moved 区别)

具体方法体如下:
Jedis第一讲-集群断连引发的超时问题-LMLPHP
如果重试次数attempts 则直接抛异常,该操作是用来做递归程序终止的判定条件。如果不满足条件,则判断是否支持asking方式获取链接。否则执行else操作:
Jedis第一讲-集群断连引发的超时问题-LMLPHP
否则判断是否尝试随机获取一个节点获取链接 tryRandomNode,否则通过当前key获取slot,再通过slot获取slot所在节点的connection
Jedis第一讲-集群断连引发的超时问题-LMLPHP

如果在获取链接时或者执行execute操作时出现异常,则执行异常处理机制,此处的异常处理分为:

1、JedisNoReachableClusterNodeException  说明Jedis在初始化时没有获取到任何集群信息,即在服务启动时集群就是不可用的,所以这种状态下,必须检查redis集群的可用性,并重启应用服务。

2、JedisConnectionException   说明Jedis在获取链接时出现异常,主要原因是因为Jedis在获取连接时默认会ping一下连接以保证目标节点是可用的,如果ping过程失败,则抛出该异常。
Jedis第一讲-集群断连引发的超时问题-LMLPHP
这个异常是今天我们要研究的重点内容,主要说明为什么集群不可用时,提效缓存,引发了超时问题?图中展示代码是在JedisConnectionException的时候,Jedis Client会执行如下步骤:
1、
释放connection链接

2、判断设置的重试次数是否,如果为false,则调用runWithRetries方法递归调用进行重试访问,如果为true,则会调用connectionHandler对象的renewSlotCache()方法:
Jedis第一讲-集群断连引发的超时问题-LMLPHP
该类是一个abstract类,内部属性JedisClusterInfoCache类对象。renewSlotCache()方法内部调用cache对象的renewClusterSlots(null)方法:
Jedis第一讲-集群断连引发的超时问题-LMLPHP
该方法内部才是真正超时根源。从方法内容可以看出存在一个for循环操作,循环内部是通过遍历缓存中redis节点列表(包含主节点+从节点),通过与节点通信的方式获取最新的集群信息,如果访问某个节点出现异常,则访问下一个节点,直到最后一个节点。要知道在一个集群整体都挂断时,此处的for循环一定会遍历所有节点,最终在都访问不同的状态下返回。那为什么会导致超时呢?大家想一想,Jedis的配置,一般状态下我们对于线上应用配置的redis maxWaitTimes参数一般都是50ms ,如果整个集群不可用时,如果是5+5从的集群,那么 50*12 = 600ms (为什么是12呢,12 = 10(节点数) + 2 attempts),所以一个请求最低在redis这一层就报销了600ms。所以redis集群整体的不可用对服务带来的影响是非常大的。希望在以后的开发中牢记这点。
3、JedisRedirectionException  该异常主要原因有两个。一种是由于Moved操作导致的(slot不在被访问节点,在另外一个节点时,当前节点返回Moved错误,并附带上目标节点IP+端口),另外一种是asking导致的(在节点进行迁移过程中,如果被访问的slot属于正在迁移节点,并且此时,访问的key不在当前节点,会返回asking错误,让client访问目标节点获取数据)。这两种情况虽然结果一直,即都需要跳转到另外一个节点获取最终结果,但是原因是截然不同的。如果客户端使用Jedis时,并且在集群环境下,一般不会出现Moved错误,因为Jedisclient端 就已经通过CRC16(key)计算好了slot以及slot具体分布在哪个节点上,所以一般不会出现Moved问题。当然只有一种情况就是当被访问的key所在主节点挂掉时,集群进行故障偏移,Jedis需要通过其他节点重新获取最新集群信息时,有可能会出现Moved问题。再有2就是通过命令行链接集群时,redis-cli  -c  ip:port  时,如果访问的key不在该节点上时,server会返回 Moved错误。
Jedis第一讲-集群断连引发的超时问题-LMLPHP
所以从源码中大家会发现,集群模式下,如果server端给Jedis返回JedisMovedDataException错误时,Jedis什么都没有执行(do nothing),这就很符合上面提到的结论。
11-30 00:00