大家好,感谢邀请,今天来为大家分享一下记一次Redis Cluster Pipeline导致的死锁问题-51CTO.COM的问题,以及和的一些困惑,大家要是还不太明白的话,也没有关系,因为接下来将为大家分享,希望可以帮助到大家,解决大家的问题,下面就开始吧!
一、背景介绍
Redis Pipeline是一种高效的命令批处理机制,可以大大降低网络延迟,提高Redis的读写能力。 Redis Cluster Pipeline是基于Redis Cluster的管道。通过将多个操作打包成一组操作,一次性发送到Redis Cluster中的多个节点,减少通信延迟,提高整个系统的读写吞吐量和性能。适用于需要高效处理Redis Cluster命令的场景。
本次使用pipeline的场景是从Redis Cluster中批量查询预约游戏信息。项目中使用的Redis Cluster Pipeline流程如下。 JedisClusterPipeline是我们内部使用的一个工具类,用于在Redis Cluster模式下提供Pipeline能力:
JedisClusterPipeline使用
JedisClusterPipline jedisClusterPipline=redisService.clusterPipelined();Listresponse;try { for (String key : 个键) { jedisClusterPipline.hmget(key, VALUE1, VALUE2); } //获取结果response=jedisClusterPipline.syncAndReturnAll();} 最后{ jedisClusterPipline. close();}
二、故障现场记录
有一天,我收到警告,说Dubbo线程池耗尽。查看日志,发现只有一台机器出现了问题,而且还没有恢复,完成的任务数量也没有增加。
查看请求数监控,发现请求数已经归零。很明显机器已经挂掉了。
使用arthas查看Dubbo线程,发现400个线程全部处于等待状态。
三、故障过程分析
Dubbo线程处于等待状态没有问题。 Dubbo线程在等待任务时也是处于等待状态,但是查看完整的调用栈,发现有问题。下面两张图第一张是问题机器的堆栈,第二张是正常机器的堆栈。显然,问题机器的线程正在等待Redis连接池中的可用连接。
使用jstack导出线程快照后,发现问题机器上的所有Dubbo线程都在等待Redis连接池中的可用连接。
经过这里排查,可以发现两个问题。
线程一直等待连接而不会被中断。线程无法获取连接。
3.1 线程一直等待连接而没有被中断原因分析
Jedis获取连接的逻辑是
在org.apache.commons.pool2.impl.GenericObjectPool#borrowObject(long) 方法下。
公共T 借用对象(长借用MaxWaitMillis)抛出异常{ . PooledObjectp=null; //获取blockWhenExhausted配置项,默认值为true boolean blockWhenExhausted=getBlockWhenExhausted();布尔创建;长waitTime=System.currentTimeMillis(); while (p==null) { 创建=false; if (blockWhenExhausted) { //从队列中获取空闲对象。该方法不会阻塞。如果没有空闲对象,则返回null。 p=idleObjects.pollFirst(); //没有空闲对象。然后创建if (p==null) { p=create(); if (p !=null) { 创建=true; } } if (p==null) { //BorrowMaxWaitMillis 默认值为-1 if (borrowMaxWaitMillis 0 ) { //线程栈快照中的所有dubbo 线程都卡在这里。这是一种阻塞方法。如果队列中没有新的连接,它将永远等待p=idleObjects.takeFirst(); } else { //等待borrowMaxWaitMillis配置如果时间尚未获取连接,则返回null p=idleObjects.pollFirst(borrowMaxWaitMillis, TimeUnit.MILLISECONDS); } } if (p==null) { throw new NoSuchElementException( '等待空闲对象超时'); } if (!p.allocate()) { p=null; updateStatsBorrow(p, System.currentTimeMillis() - waitTime); } } . } updateStatsBorrow(p, System.currentTimeMillis() - waitTime); return p.getObject();} 由于业务代码没有设置borrowMaxWaitMillis,线程一直在等待可用连接,所以可以通过配置jedis池的maxWaitMillis属性来设置这个值。
线程一直等待的原因到这里已经找到了,但是线程无法获取连接的原因还需要继续分析。
3.2 线程获取不到连接原因分析
无法获取连接只有两种情况:
无法连接到Redis,无法创建连接。连接池中所有连接都被占用,无法获取连接猜想一:是不是连不上Redis?。询问运维后得知,问题发生时确实有一波网络抖动,但很快就恢复了。故障处理问题机器可以正常连接Redis。是否有可能是创建Redis连接的过程出现了问题,无法从网络抖动中恢复,导致线程卡住?这一点需要从源码中寻找答案。
创建连接
private PooledObjectcreate() 抛出异常{ int localMaxTotal=getMaxTotal(); long newCreateCount=createCount.incrementAndGet(); if (localMaxTotal -1 newCreateCount localMaxTotal || newCreateCount Integer.MAX_VALUE) { createCount.decrementAndGet();返回空值;最终PooledObjectp ; try { //创建redis连接,超时会抛出异常//默认connectionTimeout和soTimeout都是2秒p=factory.makeObject(); } catch (Exception e) { createCount.decrementAndGet(); //这里异常会继续抛出throw e; } AbandonedConfig ac=this.abandonedConfig; if (ac !=null ac.getLogAbandoned()) { p.setLogAbandoned(true); }创建计数.incrementAndGet(); allObjects.put(new IdentityWrapper(p.getObject()), p); return p;} 可以看到,当与Redis的连接超时时,会抛出异常。 create() 函数的BorrowObject() 不会捕获此异常。这个异常最终会在业务层被捕获,所以如果你无法连接到Redis,你也不会永远等待。网络恢复后,再次调用create()方法重新创建连接。
综上,第一种情况可以排除,我们继续分析情况2,连接被占用了没问题,但不释放就有问题了。
猜想二:是不是业务代码没有归还Redis连接?连接未释放。首先想到的是,业务代码中的某个地方可能缺少返回Redis连接的代码。管道模式下,需要在finally块中手动调用JedisClusterPipeline#close()方法将连接返回到连接池。普通模式下,不需要手动释放(参考redis.clients.jedis.JedisClusterCommand#runWithRetries,每执行完一条命令就会自动释放)。全局搜索业务代码中所有使用集群Pipeline的代码,手动调用JedisClusterPipeline。 #close()方法,所以不是业务代码问题。
猜想三:是不是Jedis存在连接泄露的问题?既然业务代码没有问题,那么是否有可能是返回连接的代码有问题,出现了连接泄漏? Jedis 2.10.0版本确实可能出现连接泄漏的情况。详情请参见本期:https://github.com/redis/jedis/issues/1920。不过我们的项目使用的是2.9.0版本,所以排除了连接泄漏的情况。
猜想四:是不是发生了死锁?排除以上可能性后,我能想到的唯一原因就是死锁。想了想,发现在不设置超时的情况下使用pipeline确实存在死锁的概率。这种死锁发生在从连接池(LinkedBlockingDeque)获取连接时。
我们先看一下集群管道模式的Redis和普通Redis的区别。 Jedis为每个Redis实例维护一个连接池。集群管道模式下,首先使用查询key计算出其所在的Redis实例列表,然后从这些实例对应的连接池中获取连接,使用后进行统一。发布。正常模式下,一次只会获取一个连接池连接,使用后会立即释放。这意味着集群管道模式在获取连接时满足死锁的“保持并等待”条件,而普通模式则不满足该条件。
JedisClusterPipeline使用
JedisClusterPipline jedisClusterPipline=redisService.clusterPipelined();Listresponse;try { for (String key : keys) { //申请连接,内部会调用JedisClusterPipeline.getClient(String key) 方法获取连接jedisClusterPipline.hmget(键、值1、值2) ; //获取连接并缓存到poolToJedisMap } //获取结果response=jedisClusterPipline.syncAndReturnAll();} finally { //返回所有连接jedisClusterPipline.close();} JedisClusterPipeline 部分源码
公共类JedisClusterPipline 扩展PipelineBase 实现Closeable { 私有静态最终Logger log=LoggerFactory.getLogger(JedisClusterPipline.class); //用于记录redis命令的执行顺序private final QueueorderedClients=new LinkedList(); //redis 连接缓存private final MappoolToJedisMap=new HashMap();私有最终JedisSlotBasedConnectionHandler 连接处理程序;私有最终JedisClusterInfoCache clusterInfoCache;公共JedisClusterPipline(JedisSlotBasedConnectionHandler 连接处理程序, JedisClusterInfoCache clusterInfoCache) { this.connectionHandler=连接处理程序; this.clusterInfoCache=clusterInfoCache; } @Override protected Client getClient(String key) { return getClient(SafeEncoder.encode(key)); } @Override protected Client getClient(byte[] key) { 客户端client; //计算key所在的slot int slot=JedisClusterCRC16.getSlot(key); //获取solt对应的连接Pool JedisPool pool=clusterInfoCache.getSlotPool(slot); //从缓存中获取连接JedisboredJedis=poolToJedisMap.get(pool); //如果缓存中没有连接,则从连接池中获取并缓存if (null==BoredJedis) { BoredJedis=pool.getResource(); poolToJedisMap.put(池, 借用Jedis); } 客户端=BoredJedis.getClient(); orderClients.add(客户端);返回客户; } @Override public void close() { for (Jedis jedis : poolToJedisMap.values()) { //清除连接中的残留数据,防止连接返回时数据泄漏try { jedis.getClient().getAll() ; } catch (Throwable throwable) { log.warn('遍历时发生关闭jedis遍历异常,遍历的目的是清除连接中的残留数据,防止连接返回时数据泄露'); } 尝试{ jedis.close(); } catch (Throwable throwable) { log.warn('关闭jedis异常'); } } //返回连接clean(); orderClients.clear(); poolToJedisMap.clear(); /** * 遍历所有响应并生成正确的响应类型(警告: * 通常这是浪费时间)。 * * @return 按顺序排列的所有响应的列表*/public ListsyncAndReturnAll() { Listformatted=new ArrayList(); ListthrowableList=new ArrayList(); for (Client client :orderedClients) { try { 响应response=generateResponse(client.getOne()); if(response==null){ 继续; } formatted.add(response.get()); } catch (Throwable e) { throwableList.add(e); slotCacheRefreshed(throwableList);返回格式化; }}图片
例如:
假设有一个集群,有两个Redis主节点(集群模式下主节点最少为3个,这里只是举例),记为节点1/2,并且有一个java程序,有4个Dubbo线程,记为线程1/2/3/4,每个Redis实例有一个大小为2的连接池。
线程1和线程2先获取Redis1的连接,然后获取Redis2的连接。线程3和线程4先获取Redis2的连接,然后获取Redis1的连接。假设这四个线程在获得第一个连接后等待一段时间,那么在获得第二个连接时就会出现死锁。 (等待时间越长,触发概率越大)。
图片
因此,管道可能会造成死锁。这种死锁条件很容易被破坏。只需设置等待连接时的超时时间即可。您还可以增加连接池的大小。如果资源充足,就不会出现死锁。
四、死锁证明
以上只是推测。为了证明死锁确实发生,需要满足以下条件:
线程当前获得了哪些连接池连接?线程当前正在等待哪个连接池连接?每个连接池中还剩下多少个连接?已知问题:本机的Dubbo线程池大小为400,Redis集群主节点数量为12,Jedis Pool size配置的连接数为20。
4.1 步骤一:获取线程在等待哪个连接池有空闲连接
第一步:首先通过jstack和jmap分别导出栈和堆
第二步:通过分析堆栈,可以知道线程正在等待的锁的地址。可以看到Dubbo线程383正在等待锁对象0x6a3305858。这个锁属于某个连接池。您需要找出它是哪个连接池。
第三步:使用mat(Eclipse Memory Analyzer Tool)工具分析堆,通过锁地址找到对应的连接池。
使用mat的带传入参考文献功能逐层查找参考文献。
参考关系:
条件对象链接阻塞双端队列
参考关系:
LinkedBlockingDeque-通用对象池
参考关系:GenericObjectPool-JedisPool。这里的ox6a578ddc8就是这个锁所属的连接池地址。
这样我们就可以知道Dubbo线程383当前正在等待连接池0x6a578ddc8的连接。
通过这组流程,我们可以知道每个Dubbo线程正在哪些连接池中等待可用连接。
4.2 步骤二:获取线程当前持有了哪些连接池的连接
第一步:使用mat找到堆中所有的JedisClusterPipeline类(正好400个,每个Dubbo线程一个),然后查看里面的poolToJedisMap,里面保存了当前
JedisClusterPipeline已经持有该连接及其所属的连接池。
在下图中我们可以看到
JedisClusterPipeline(0x6ac40c088)对象当前的poolToJedisMap中有3个Node对象。
(0x6ac40dd40、0x6ac40dd60、0x6ac40dd80),这意味着它保存来自三个连接池的连接。 JedisPool的地址可以从Node对象中找到。
第二步:首先要找出JedisClusterPipeline持有哪个连接池,然后再找出JedisClusterPipeline持有哪个连接池。
JedisClusterPipeline的Dubbo线程,这样就可以获取Dubbo线程当前持有哪些连接池连接。
4.3死锁分析
通过流程1我们可以发现,虽然有12个Redis主节点,但所有Dubbo线程都只是在等待以下5个节点对应的连接池之一:
0x6a578e0c80x6a578e0480x6a578ddc80x6a578e5380x6a578e838 通过流程2我们可以知道当前有哪些线程占用了这5个连接池的连接:
已知每个连接池的大小配置为20,这5个连接池的所有连接已经被100个Dubbo线程占用,所有400个Dubbo线程都在等待这5个连接池的连接,而它的连接是waiting 当前未被自身占用。通过这些条件,我们可以判断发生了死锁。
五、总结
这篇文章给大家聊聊关于记一次Redis Cluster Pipeline导致的死锁问题-51CTO.COM,以及对应的知识点,希望对各位有所帮助,不要忘了收藏本站哦。
本文采摘于网络,不代表本站立场,转载联系作者并注明出处:https://www.iotsj.com//kuaixun/6370.html
用户评论
突然想起之前项目遇到过类似的问题,好吓人啊!
有17位网友表示赞同!
这篇文章挺详细的,把pipeline带来的死锁问题解释得很透彻。
有11位网友表示赞同!
我平常用的Redis不多,所以不太了解Cluster的具体用法,这下受益了!
有19位网友表示赞同!
学习笔记:Pipeline操作要注意死锁问题,尤其是在分布式环境下。
有17位网友表示赞同!
之前听过Redis Cluster,但没深入了解过,这篇文章很有帮助。
有6位网友表示赞同!
文章案例分析很清晰,我终于明白pipeline的操作逻辑了。
有16位网友表示赞同!
这种死锁现象确实容易发生,需要小心应对!
有19位网友表示赞同!
遇到类似问题的时候,该怎么排查调试呢?有方法可以分享吗?
有9位网友表示赞同!
这篇文章提醒我们要在使用Pipeline的时候更加谨慎操作。
有9位网友表示赞同!
Redis Cluster越来越常用,学习它的相关知识很重要啊!
有10位网友表示赞同!
看了下评论区,好像有人遇到过同样的问题,不过我还没机会用过Pipeline...
有9位网友表示赞同!
原来pipeline可以这样做,以后可以用在项目中试试看。
有5位网友表示赞同!
文章的例子和解决方案都很有帮助,感谢作者的分享!
有6位网友表示赞同!
这种死锁现象可能在大型系统里更容易出现吧?
有19位网友表示赞同!
Redis 的特性还是蛮强大的,可惜我还没深入学习过呢...
有6位网友表示赞同!
以后使用Redis的时候要注意避免pipeline引起的死锁问题,要查阅相关资料!
有10位网友表示赞同!
这篇文章让我认识到了Cluster的潜力,我也想去实践一下!
有5位网友表示赞同!
分享这个文章给同事看看,他也在学习Redis Cluster呢!
有10位网友表示赞同!
如果遇到这种情况怎么办?作者有没有提供解决方案?
有10位网友表示赞同!