记一次Redis Cluster Pipeline导致的死锁问题

作者:vivo 互联网服务器团队- Li Gang

本文介绍了一次排查Dubbo线程池耗尽问题的过程。通过查看Dubbo线程状态、分析Jedis连接池获取连接的源码、排查死锁条件等方面,最终确认是因为使用了cluster pipeline模式且没有设置超时时间导致死锁问题。

一、背景介绍

Redis Pipeline是一种高效的命令批量处理机制,可以在Redis中大幅度降低网络延迟,提高读写能力。Redis Cluster Pipeline是基于Redis Cluster的pipeline,通过将多个操作打包成一组操作,一次性发送到Redis Cluster中的多个节点,减少了通信延迟,提高了整个系统的读写吞吐量和性能,适用于需要高效处理Redis Cluster命令的场景。

本次使用到pipeline的场景是批量从Redis Cluster批量查询预约游戏信息,项目内使用的Redis Cluster Pipeline的流程如下,其中的JedisClusterPipeline是我们内部使用的工具类,提供Redis Cluster模式下的pipeline能力:

JedisClusterPipeline使用:

JedisClusterPipline jedisClusterPipline = redisService.clusterPipelined();
List<Object> response;
try {for (String key : keys) {jedisClusterPipline.hmget(key, VALUE1, VALUE2);}// 获取结果response = jedisClusterPipline.syncAndReturnAll();
} finally {jedisClusterPipline.close();
}

二、故障现场记录

某天,收到了Dubbo线程池耗尽的告警。查看日志发现只有一台机器有问题,并且一直没恢复,已完成任务数也一直没有增加。

图片

查看请求数监控,发现请求数归零,很明显机器已经挂了。

图片

使用arthas查看Dubbo线程,发现400个线程全部处于waiting状态。

图片

三、故障过程分析

Dubbo线程处于waiting状态这一点没有问题,Dubbo线程等待任务的时候也是waiting状态,但是查看完整调用栈发现有问题,下面两张图里的第一张是问题机器的栈,第二张是正常机器的栈,显然问题机器的这个线程在等待Redis连接池里有可用连接。

图片

图片

使用jstack导出线程快照后发现问题机器所有的Dubbo线程都在等待Redis连接池里有可用连接。

调查到这里,能发现两个问题。

  1. 线程一直等待连接而没有被中断。

  2. 线程获取不到连接。

3.1 线程一直等待连接而没有被中断原因分析

Jedis获取连接的逻辑在org.apache.commons.pool2.impl.GenericObjectPool#borrowObject(long)方法下。

public T borrowObject(long borrowMaxWaitMillis) throws Exception {...PooledObject<T> p = null;// 获取blockWhenExhausted配置项,该配置默认值为trueboolean blockWhenExhausted = getBlockWhenExhausted();boolean create;long waitTime = System.currentTimeMillis();while (p == null) {create = false;if (blockWhenExhausted) {// 从队列获取空闲的对象,该方法不会阻塞,没有空闲对象会返回nullp = idleObjects.pollFirst();// 没有空闲对象则创建if (p == null) {p = create();if (p != null) {create = true;}}if (p == null) {// borrowMaxWaitMillis默认值为-1if (borrowMaxWaitMillis < 0) {// 线程栈快照里所有的dubbo线程都卡在这里,这是个阻塞方法,如果队列里没有新的连接会一直等待下去p = idleObjects.takeFirst();    } else {// 等待borrowMaxWaitMillis配置的时间还没有拿到连接的话就返回nullp = idleObjects.pollFirst(borrowMaxWaitMillis,TimeUnit.MILLISECONDS);}}if (p == null) {throw new NoSuchElementException("Timeout waiting for idle object");}if (!p.allocate()) {p = null;}}...}updateStatsBorrow(p, System.currentTimeMillis() - waitTime);return p.getObject();
}

由于业务代码没有设置borrowMaxWaitMillis,导致线程一直在等待可用连接 ,该值可以通过配置jedis pool的maxWaitMillis属性来设置。

到这里已经找到线程一直等待的原因,但线程获取不到连接的原因还需要继续分析。

3.2 线程获取不到连接原因分析

获取不到连接无非两种情况:

  1. 连不上Redis,无法创建连接

  2. 连接池里的所有连接都被占用了,无法获取到连接

猜想一:是不是连不上Redis?

询问运维得知发生问题的时间点确实有一波网络抖动,但是很快就恢复了,排查时问题机器是能正常连上Redis的。那有没有可能是创建Redis连接的流程写的有问题,无法从网络抖动中恢复导致线程卡死?这一点要从源码中寻找答案。

创建连接:

private PooledObject<T> create() throws Exception {int localMaxTotal = getMaxTotal();long newCreateCount = createCount.incrementAndGet();if (localMaxTotal > -1 && newCreateCount > localMaxTotal ||newCreateCount > Integer.MAX_VALUE) {createCount.decrementAndGet();return null;}final PooledObject<T> p;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);}createdCount.incrementAndGet();allObjects.put(new IdentityWrapper<T>(p.getObject()), p);return p;
}

可以看到,连接Redis超时时会抛出异常,调用create()函数的borrowObject()也不会捕获这个异常,这个异常最终会在业务层被捕获,所以连不上Redis的话是不会一直等待下去的,网络恢复后再次调用create()方法就能重新创建连接。

综上所诉,第一种情况可以排除,继续分析情况2,连接被占用了没问题,但是一直不释放就有问题。

猜想二:是不是业务代码没有归还Redis连接?

连接没有释放,最先想到的是业务代码里可能有地方漏写了归还Redis连接的代码,pipeline模式下需要在finally块中手动调用JedisClusterPipeline#close()方法将连接归还给连接池,而普通模式下不需要手动释放(参考redis.clients.jedis.JedisClusterCommand#runWithRetries,每次执行完命令后都会自动释放),在业务代码里全局搜索所有使用到了cluster pipeline的代码,均手动调用了JedisClusterPipeline#close()方法,所以不是业务代码的问题。

猜想三:是不是Jedis存在连接泄露的问题?

既然业务代码没问题,那有没有可能是归还连接的代码有问题,存在连接泄露?2.10.0版本的Jedis确实可能发生连接泄露,具体可以看这个issue:https://github.com/redis/jedis/issues/1920,不过我们项目内使用的是2.9.0版本,所以排除连接泄露的情况。

猜想四:是不是发生了死锁?

排除以上可能性后,能想到原因的只剩死锁,思考后发现在没有设置超时时间的情况下,使用pipeline确实有概率发生死锁,这个死锁发生在从连接池(LinkedBlockingDeque)获取连接的时候。

先看下cluster pipeline模式下的Redis和普通的Redis有什么区别。Jedis为每个Redis实例都维护了一个连接池,cluster pipeline模式下,先使用查询用的key计算出其所在的Redis实例列表,再从这些实例对应的连接池里获取到连接,使用完后统一释放。而普通模式下一次只会获取一个连接池的连接,用完后立刻释放。这意味着cluster pipeline模式在获取连接时是符合死锁的“占有并等待”条件的,而普通模式不符合这个条件。

JedisClusterPipeline使用:

JedisClusterPipline jedisClusterPipline = redisService.clusterPipelined();
List<Object> response;
try {for (String key : keys) {// 申请连接,内部会先调用JedisClusterPipeline.getClient(String key)方法获取连接jedisClusterPipline.hmget(key, VALUE1, VALUE2);// 获取到了连接,缓存到poolToJedisMap}// 获取结果response = jedisClusterPipline.syncAndReturnAll();
} finally {// 归还所有连接jedisClusterPipline.close();
}

JedisClusterPipeline部分源码:

public class JedisClusterPipline extends PipelineBase implements Closeable {private static final Logger log = LoggerFactory.getLogger(JedisClusterPipline.class);// 用于记录redis命令的执行顺序private final Queue<Client> orderedClients = new LinkedList<>();// redis连接缓存private final Map<JedisPool, Jedis> poolToJedisMap = new HashMap<>();private final JedisSlotBasedConnectionHandler connectionHandler;private final JedisClusterInfoCache clusterInfoCache;public JedisClusterPipline(JedisSlotBasedConnectionHandler connectionHandler, JedisClusterInfoCache clusterInfoCache) {this.connectionHandler = connectionHandler;this.clusterInfoCache = clusterInfoCache;}@Overrideprotected Client getClient(String key) {return getClient(SafeEncoder.encode(key));}@Overrideprotected Client getClient(byte[] key) {Client client;// 计算key所在的slotint slot = JedisClusterCRC16.getSlot(key);// 获取solt对应的连接池JedisPool pool = clusterInfoCache.getSlotPool(slot);// 从缓存获取连接Jedis borrowedJedis = poolToJedisMap.get(pool);// 缓存中没有连接则从连接池获取并缓存if (null == borrowedJedis) {borrowedJedis = pool.getResource();poolToJedisMap.put(pool, borrowedJedis);}client = borrowedJedis.getClient();orderedClients.add(client);return client;}@Overridepublic void close() {for (Jedis jedis : poolToJedisMap.values()) {// 清除连接内的残留数据,防止连接归还时有数据漏读的现象try {jedis.getClient().getAll();} catch (Throwable throwable) {log.warn("关闭jedis时遍历异常,遍历的目的是:清除连接内的残留数据,防止连接归还时有数据漏读的现象");}try {jedis.close();} catch (Throwable throwable) {log.warn("关闭jedis异常");}}// 归还连接clean();orderedClients.clear();poolToJedisMap.clear();}/*** go through all the responses and generate the right response type (warning :* usually it is a waste of time).** @return A list of all the responses in the order*/public List<Object> syncAndReturnAll() {List<Object> formatted = new ArrayList<>();List<Throwable> throwableList = new ArrayList<>();for (Client client : orderedClients) {try {Response response = generateResponse(client.getOne());if(response == null){continue;}formatted.add(response.get());} catch (Throwable e) {throwableList.add(e);}}slotCacheRefreshed(throwableList);return formatted;}
}

图片

举个例子:

假设有一个集群有两台Redis主节点(集群模式下最小的主节点数量是3,这里只是为了举例),记为节点1/2,有个java程序有4个Dubbo线程,记为线程1/2/3/4,每个Redis实例都有一个大小为2的连接池。

线程1和线程2,先获取Redis1的连接再获取Redis2的连接。线程3和线程4,先获取Redis2的连接再获取Redis1的连接,假设这四个线程在获取到连接第一个连接后都等待了一会,在获取第二个连接的时候就会发生死锁(等待时间越长,触发的概率越大)。

图片

所以pipeline是可能导致死锁的,这个死锁的条件很容易破坏,等待连接的时候设置超时时间即可。还可以增大下连接池的大小,资源够的话也不会发生死锁。

四、死锁证明

以上只是猜想,为了证明确实发生了死锁,需要以下条件:

  1. 线程当前获取到了哪些连接池的连接

  2. 线程当前在等待哪些连接池的连接

  3. 每个连接池还剩多少连接

已知问题机器的Dubbo线程池大小为400,Redis集群主节点数量为12,Jedis配置的连接池大小为20。

4.1 步骤一:获取线程在等待哪个连接池有空闲连接

第一步:先通过jstack和jmap分别导出栈和堆

第二步:通过分析栈可以知道线程在等待的锁的地址,可以看到Dubbo线程383在等待0x6a3305858这个锁对象,这个锁属于某个连接池,需要找到具体是哪个连接池。

图片

第三步:使用mat(Eclipse Memory Analyzer Tool)工具分析堆,通过锁的地址找到对应的连接池。

图片

使用mat的with incoming references功能顺着引用一层层的往上找。

图片

引用关系:ConditionObject->LinkedBlockingDeque

图片

引用关系:LinkedBlockingDeque->GenericObjectPool

图片

引用关系:GenericObjectPool->JedisPool。这里的ox6a578ddc8就是这个锁所属的连接池地址。

图片

这样我们就能知道Dubbo线程383当前在等待0x6a578ddc8这个连接池的连接。

通过这一套流程,我们可以知道每个Dubbo线程分别在等待哪些连接池有可用连接。

4.2 步骤二:获取线程当前持有了哪些连接池的连接

第一步:使用mat在堆中查找所有JedisClusterPipeline类(正好400个,每个Dubbo线程都各有一个),然后查看里面的poolToJedisMap,其中保存了当前JedisClusterPipeline已经持有的连接和其所属的连接池。

下图中,我们可以看到JedisClusterPipeline(0x6ac40c088)对象当前的poolToJedisMap里有三个Node对象(0x6ac40dd40, 0x6ac40dd60, 0x6ac40dd80),代表其持有三个连接池的连接,可以从Node对象中找到JedisPool的地址。

图片

第二步:第一步拿到JedisClusterPipeline持有哪个连接池的连接后,再查找持有此JedisClusterPipeline的Dubbo线程,这样就能得到Dubbo线程当前持有哪些连接池的连接。

图片

4.3 死锁分析

通过流程一可以发现虽然Redis主节点有12个,但是所有的Dubbo线程都只在等待以下5个节点对应的连接池之一:

  • 0x6a578e0c8

  • 0x6a578e048

  • 0x6a578ddc8

  • 0x6a578e538

  • 0x6a578e838

通过流程二我们可以得知这5个连接池的连接当前被哪些线程占用:

图片

已知每个连接池的大小都配置为了20,这5个连接池的所有连接已经被100个Dubbo线程占用完了,而所有的400个Dubbo线程又都在等待这5个连接池的连接,并且其等待的连接当前没被自己占用,通过这些条件,我们可以确定发生了死锁。

五、总结

这篇文章主要展现了一次系统故障的分析过程。在排查过程中,作者使用jmap和jstack保存故障现场,使用arthas分析故障现场,再通过阅读和分析源码,从多个可能的角度一步步的推演故障原因,推测是死锁引起的故障。在验证死锁问题时,作者使用mat按照一定的步骤来寻找线程在等待哪个连接池的连接和持有哪些连接池的连接,再结合死锁检测算法最终确认故障机器发生了死锁。

排查线上问题并非易事,不仅要对业务代码有足够的了解,还要对相关技术知识有系统性的了解,推测出可能导致问题的原因后,再熟练运用好排查工具,最终确认问题原因。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/107087.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

掌握 Scikit-Learn: Python 中的机器学习库入门

机器学习 第二课 Sklearn 入门 概述机器学习与 Python 的完美结合Scikit-Learn 的核心组件与结构安装与配置验证安装 数据表示与预处理特征矩阵和目标向量数据处理 估计器模型的选择思考问题的本质研究数据的分布判断任务的复杂性分类问题回归问题 监督学习分类算法回归算法 无…

常见的8个JMeter压测问题

为什么在JMeter中执行压力测试时&#xff0c;出现连接异常或连接重置错误&#xff1f; 答案&#xff1a;连接异常或连接重置错误通常是由于服务器在处理请求时出现问题引起的。这可能是由于服务器过载、网络故障或配置错误等原因导致的。 解决方法&#xff1a; 确定服务器的负载…

智能警用装备管理系统-科技赋能警务

警用物资装备管理系统&#xff08;智装备DW-S304&#xff09;是依托互云计算、大数据、RFID技术、数据库技术、AI、视频分析技术对警用装备进行统一管理、分析的信息化、智能化、规范化的系统。 &#xff08;1&#xff09;感知智能化 装备感知是整个方案的基础&#xff0c;本方…

基于YOLO算法的单目相机2D测量(工件尺寸和物体尺寸)三

1.简介 1.1 2D测量技术 基于单目相机的2D测量技术在许多领域中具有重要的背景和意义。 工业制造&#xff1a;在工业制造过程中&#xff0c;精确测量是确保产品质量和一致性的关键。基于单目相机的2D测量技术可以用于检测和测量零件尺寸、位置、形状等参数&#xff0c;进而实…

PyTorch入门教学——使用PyCharm创建一个PyTorch项目

首先需要创建好PyTorch的虚拟环境&#xff0c;步骤&#xff1a;PyTorch入门教学——简介与环境配置-CSDN博客打开PyCharm&#xff0c;新建项目&#xff0c;选择项目的存放位置。选择先前配置的解释器&#xff0c;也就是虚拟环境中的解释器。&#xff08;记住创建的虚拟环境所在…

年龄越大,越要小心逢九年

老话有云&#xff1a;年龄逢九&#xff0c;灾祸频有。在我国的许多农村地区&#xff0c;至今还流传着这么句话&#xff0c;这句话的大概意思是说&#xff1a;每个人命理年龄逢九&#xff0c;就会有个坎&#xff0c;年龄越大&#xff0c;坎就越厉害&#xff0c;所以&#xff0c;…

【Linux】线程安全问题①——如何实现资源访问互斥(附图解与代码实现)

线程安全主要分为两个方面&#xff0c;分别是资源访问互斥与线程同步&#xff08;线程协同配合&#xff09; 本篇博客&#xff0c;我们主要来讲解资源访问互斥这一方面 目录 为什么要实现资源访问互斥&#xff1f; 实现资源访问互斥&#xff08;原子访问&#xff09;的经典…

Nginx正向代理配置(http)

前言 在工作中我们经常使用nginx进行反向代理,今天介绍下怎么进行正向代理,支持http请求,暂不支持https 首先先介绍下正向代理和反向代理。 正向代理 在客户端&#xff08;浏览器&#xff09;配置代理服务器&#xff0c;通过代理服务器进行互联网访问。 反向代理 客户端只…

ThinkPHP5小语种学习平台

有需要请加文章底部Q哦 可远程调试 ThinkPHP5小语种学习平台 一 介绍 此小语种学习平台基于ThinkPHP5框架开发&#xff0c;数据库mysql&#xff0c;前端bootstrap。平台角色分为学生&#xff0c;教师和管理员三种。学生注册登录后可观看学习视频&#xff0c;收藏视频&#xf…

关于图像分割SDK的一些基础认识

随着科技的不断发展&#xff0c;图像分割SDK已经成为了一个备受关注的话题。而在众多图像分割SDK中&#xff0c;美摄图像分割SDK以其独特的功能和优势脱颖而出。本文将从美摄图像分割SDK的企业价值和互联网娱乐方面&#xff0c;介绍其宣传文章的具体写作规范。 在企业价值方面&…

从一到无穷大 #18 时序数据库运营SLI思考

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 本作品 (李兆龙 博文, 由 李兆龙 创作)&#xff0c;由 李兆龙 确认&#xff0c;转载请注明版权。 公有云时序数据库SLA 运营商产品每服务周期服务可用率不低于99.9%衡量服务不可用数据指标从采…

202、RabbitMQ 之 使用 fanout 类型的Exchange 实现 Pub-Sub 消息模型---fanout类型就是广播类型

目录 ★ 使用 fanout 类型的Exchange 实现 Pub-Sub 消息模型代码演示&#xff1a;生产者&#xff1a;producer消费者&#xff1a;Consumer01消费者&#xff1a;Consumer02测试结果 完整代码ConnectionUtilPublisherConsumer01Consumer02pom.xml ★ 使用 fanout 类型的Exchange …

[Swift]同一个工程管理多个Target

1.准备 先创建一个测试工程“ADemo”&#xff0c;右键其Target选择Duplicate&#xff0c;再复制一个Target为“ADemo2”。 再选择TARGETS下方的“”&#xff0c;添加一个APP到项目中&#xff0c;这个命名为“BDemo”。 2、管理多个Target 可以对三个target分别导入不同的框…

iWall:支持自定义的Mac动态壁纸软件

iWall Mac是一款动态壁纸软件&#xff0c;它可以使用任何格式的漂亮视频&#xff08;无须转换&#xff09;、图片、动画、Flash、gif、swf、程序、网页、网站做为您的动态壁纸、动态桌面&#xff0c;并且可以进行交互。 这款软件功能多、使用简单、体积小巧、不占用资源、运行…

微信小程序引入阿里巴巴iconfont图标并使用

介绍 在小程序里&#xff0c;使用阿里巴巴的图标&#xff0c;如下所示: 使用方式 搜索自己需要的图标&#xff0c;然后将需要用到的图标加入购物车&#xff0c;如下图所示&#xff1a; 去右上角&#xff0c;点击购物车按钮&#xff1b;这里第一次使用&#xff0c;会有三个提…

Redis缓存穿透、缓存击穿、缓存雪崩详解

目录 缓存处理流程 一、缓存穿透 1、概念 2、解决办法 二、缓存击穿 1、概念 2、解决办法 三、缓存雪崩 1、概念 2、解决办法 缓存处理流程 接收到查询数据请求时&#xff0c;优先从缓存中查询&#xff0c;若缓存中有数据&#xff0c;则直接返回&#xff0c;若缓存中查不到则从…

Steam余额红锁的原因,及红锁后申诉办法

安全的余额一般是通过充值卡充值获得&#xff0c;再加上交易手续费再转卖给你。一般便宜不到哪去。 但你别以为余额是安全的&#xff0c;就万事大吉了。照样有被红锁的可能性&#xff0c;比如这三种&#xff1a; 1、Steam市场巡查机制&#xff0c;红锁 平台的巡查机制和原理…

在逍遥模拟器上安装LPSosed模块以及其Manager管理器

环境&#xff1a;win7 64位&#xff0c;python3.8.10&#xff0c;逍遥模拟器9.0.6&#xff0c;安卓版本9 参考我的文章&#xff1a; 在雷电模拟器9上安装magisk并安装LSPosed模块以及其Manager管理器&#xff08;一&#xff09;-CSDN博客 前置工作&#xff1a;先开启模拟器的…

一文讲解图像梯度

简介&#xff1a; ​ 图像梯度计算的是图像变化的幅度。对于图像的边缘部分&#xff0c;其灰度值变化较大&#xff0c;梯度值变化也较大&#xff1b;相反&#xff0c;对于图像中比较平滑的部分&#xff0c;其灰度值变化较小&#xff0c;相应的梯度值变化也较小。一般情…

测试培训机构「某峰」测评调查报告

测试培训机构「某峰」测评调查报告 ⭐文章简介一、机构测评之受访者介绍二、老师傅-机构测评10问&#xff01;第1问 你从哪里了解到这个培训机构&#xff1f;第2问 你为什么选择这家培训机构&#xff0c;它吸引你掏毛爷爷的点有哪些&#xff1f;第3问 回顾一下&#xff0c;从咨…