问题带来多少成长,看你挖得有多深多痛

2024-04-23 00:44

本文主要是介绍问题带来多少成长,看你挖得有多深多痛,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

原文: 一次Redis访问超时的“捉虫”之旅

力是相互的,成长与痛苦也是相互的。

01-引言

最近在对一个老项目使用的docker镜像版本升级过程中碰到一个奇怪的问题,发现项目升级到高版本镜像后,访问Redis会出现很多超时错误,而降回之前的镜像版本后问题也随之消失。经过排查,最终定位问题元凶是一个涉及到Lettuce、Redis、Netty等多块内容的代码bug
在问题解决过程中也对相关组件的工作方式有了更深一步的理解。以下就对“捉虫”过程中的问题分析和排查过程做一个详细的介绍。

03-问题现象

为了固定问题场景,我们进行了一番条件测试,发现了一些端倪:

  • 低版本镜像上RedisTemplete缓存框架访问Redis集群正常
  • 高版本镜像上RedisTemplete访问Redis集群正常,缓存框架访问Redis集群超时。项目启动一段时间后框架访问恢复正常。
  • 低版本和高版本镜像中RedisTemplete和缓存框架访问Redis单机正常

根据以上现象不难推断出,问题应似乎出现在缓存框架访问Redis集群的机制上。结合项目启动一段时间后会恢复正常的特点,猜测应该和缓存预热流程有关。

04-排查过程

复现case

查阅代码后发现自研的缓存框架没有通过Spring访问Redis,而是直接使用了Sping底层的Redis客户端——Lettuce

排查至此,我们发现缓冲区的数据积压很可能就是造成反查请求超时的原因,明白了这一点后,我们开始思考:

  • 连接缓冲区中的数据应该由谁来消费?
  • 每个连接的作用是什么?
  • 为什么只有一个连接出现了数据积压情况?
  • 为什么积压情况只在高版本的镜像中出现?
  • 为什么通过Spring访问Redis就不会出现超时问题?

深度分析

要回答以上问题,首先要了解Lettuce的工作原理,重点是其底层是如何访问Redis集群的。
Lettuce的工作原理
根据官网介绍,Lettuce 底层基于 Netty 的NIO模型实现,只用有限的线程支持更多的 Redis 连接,在高负载情况下能更有效地利用系统资源。

EventLoop机制的核心功能是多路复用,这意味着一个线程可以处理多个连接的读写事件。但是要实现这一点的前提是EventLoop线程不能被阻塞,否则注册在该线程上的各个连接的事件将得不到响应。由此我们可以推测,如果socket缓冲区出现积压,可能是某些原因导致socket连接对应的 EventLoop 线程被阻塞,使其无法正常响应可读事件并读取缓冲区数据。

为了验证猜测,我们在日志中打印线程信息做进一步观察。
在这里插入图片描述
结果发现大部分超时都发生在同一个EventLoop线程上(Lettuce的epollEventLoop-9-3线程),那这个线程此刻的状态是什么呢?我们可以通过诊断工具查看线程堆栈,定位阻塞原因。

Arthas排障

这里我们利用阿里Arthas排障工具的thread命令查看线程状态和堆栈信息。
在这里插入图片描述
从堆栈信息可以看出,Lettuce一共创建了3个Netty EventLoop线程,其中9-3处在TIMED_WAITTING状态,该线程亦是Pub/Sub消息的监听线程,阻塞在了RedisLettucePubSubListener对象接收消息更新热key的get方法上。

定位原因

通过Arthas排障我们了解到,原来Lettuce是在Netty的EventLoop线程中响应Pub/Sub事件的。由此我们也基本定位了缓冲区的积压原因,即在RedisLettucePubSubListener中执行了阻塞的future get方法,导致其载体EventLoop线程被阻塞,无法响应与其Selector关联连接的io事件。

为什么Pub/Sub事件会和其他连接的io事件由同一个EventLoop处理呢?通过查阅资料,发现Netty对连接进行多路复用时,只会启动有限个EventLoop线程(默认是CPU数*2)进行连接管理,每个连接是轮询注册到 EventLoop上的,所以当EventLoop数量不多时,多个连接就可能会注册到同一个io线程上。

针对这种应用场景Lettuce官网上也有专门提醒:https://lettuce.io/core/release/reference/index.html

  • 即不要在Pub/Sub的回调函数中执行阻塞操作。
    在这里插入图片描述

解决方案

原因定位后,解决方案也呼之欲出。有两种方法:

增加io线程

异步化

比较优雅的方式是不要在nio线程中执行阻塞操作,即将处理Pub/Sub消息的过程异步化,最好放到独立的线程中执行,以尽早释放Netty的EventLoop资源。我们熟悉的spring-data-redis框架就是这么做的。

  • Spring-data-redis的做法是每次收到消息时都新启动新线程处理。
    在这里插入图片描述

思考

尽管问题已经解决,但之前还有几个遗留的疑问没有解答。经过一番研究,我们也找到了答案。

为什么低版本镜像没问题?

在之前的分析中,我们提到了因为 EventLoop 线程数量过少导致线程阻塞。高版本的实例中 EventLoop 线程数量为 3,那么低版本的情况呢?通过Arthas 查看,发现低版本 Lettuce 的 EventLoop 数量是 13,远远超过了高版本的数量。这表示在低版本环境中,Pub/Sub 连接和其他连接会注册到不同的 EventLoop 上,即使 Pub/Sub 处理线程被阻塞,也不会影响到其他连接读写事件的处理

为什么低版本的镜像会创建更多的 EventLoop 呢?这其实是 JDK 的一个坑。早期的 JDK 8 版本(8u131 之前)存在docker环境下Java获取cpu核心数不准确的问题,会导致程序拿到的是宿主机的核数。
(https://blogs.oracle.com/java/post/java-se-support-for-docker-cpu-and-memory-limits)

查看低版本镜像的jdk版本是8u101,应用宿主机的核数是16,也就是说,低版本应用误拿到了宿主机的核数16,因此会将每个连接注册到一个独立的EventLoop上,从而避免了阻塞的发生。换句话说,之所以低版本镜像没问题,其实是程序在错误的环境下获取到错误的数值,却得到了正确的结果,负负得正了。至于为什么最大线程号是 13 ,这是由于我们的 Redis 集群配置了两个域名,如下图所示。

在 RedisClusterClient 初始化时,会分别对域名(2)、所有集群节点(6)、Pub/Sub 通道(1)、集群主连接(1)、副连接(3)进行连接创建,加起来一共正好是 13 个。

为什么高版本通过Spring访问Redis不会出现超时问题?

原始项目访问Redis有Spring和缓存框架两种方式。前文中提到的所有 EventLoop 都是由自研缓存框架维护的 RedisClusterClient 对象创建的。而Spring 容器会使用单独的 RedisClusterClient 对象来创建Redis连接。在 Lettuce 中,每个 RedisClusterClient 对象底层都对应着不同的 EventLoopGroup。也就是说,Spring 创建的Redis连接一定不会和缓存框架的连接共用同一个 EventLoop。因此即使缓存框架所在的 EventLoop 线程被阻塞,也不会影响到 Spring 连接的事件响应。

为什么高版本镜像访问单机Redis没问题?

与RedisClusterClient访问Redis集群时会创建多个主副连接不同,访问单机Redis时Lettuce使用的RedisClient只会创建1个连接。再加上独立的Pub/Sub连接,相当于是2个连接注册到3个EventLoop上,避免了冲突。

05-总结

本文从实际工作中遇到的一个Redis访问超时问题出发,探究背后Spring、Lettuce和Netty的工作原理,并利用Arthas等调试工具,分析了EventLoop线程对连接处理的重要性,以及在处理Pub/Sub事件时避免阻塞操作的必要性。通过观察不同版本环境下的行为差异,加深了对JDK版本和程序环境适配的理解,为今后排查类似问题积累了宝贵经验。

这篇关于问题带来多少成长,看你挖得有多深多痛的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/927322

相关文章

SpringBoot启动报错的11个高频问题排查与解决终极指南

《SpringBoot启动报错的11个高频问题排查与解决终极指南》这篇文章主要为大家详细介绍了SpringBoot启动报错的11个高频问题的排查与解决,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一... 目录1. 依赖冲突:NoSuchMethodError 的终极解法2. Bean注入失败:No qu

MySQL新增字段后Java实体未更新的潜在问题与解决方案

《MySQL新增字段后Java实体未更新的潜在问题与解决方案》在Java+MySQL的开发中,我们通常使用ORM框架来映射数据库表与Java对象,但有时候,数据库表结构变更(如新增字段)后,开发人员可... 目录引言1. 问题背景:数据库与 Java 实体不同步1.1 常见场景1.2 示例代码2. 不同操作

如何解决mysql出现Incorrect string value for column ‘表项‘ at row 1错误问题

《如何解决mysql出现Incorrectstringvalueforcolumn‘表项‘atrow1错误问题》:本文主要介绍如何解决mysql出现Incorrectstringv... 目录mysql出现Incorrect string value for column ‘表项‘ at row 1错误报错

如何解决Spring MVC中响应乱码问题

《如何解决SpringMVC中响应乱码问题》:本文主要介绍如何解决SpringMVC中响应乱码问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Spring MVC最新响应中乱码解决方式以前的解决办法这是比较通用的一种方法总结Spring MVC最新响应中乱码解

pip无法安装osgeo失败的问题解决

《pip无法安装osgeo失败的问题解决》本文主要介绍了pip无法安装osgeo失败的问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 进入官方提供的扩展包下载网站寻找版本适配的whl文件注意:要选择cp(python版本)和你py

解决Java中基于GeoTools的Shapefile读取乱码的问题

《解决Java中基于GeoTools的Shapefile读取乱码的问题》本文主要讨论了在使用Java编程语言进行地理信息数据解析时遇到的Shapefile属性信息乱码问题,以及根据不同的编码设置进行属... 目录前言1、Shapefile属性字段编码的情况:一、Shp文件常见的字符集编码1、System编码

Spring MVC使用视图解析的问题解读

《SpringMVC使用视图解析的问题解读》:本文主要介绍SpringMVC使用视图解析的问题解读,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Spring MVC使用视图解析1. 会使用视图解析的情况2. 不会使用视图解析的情况总结Spring MVC使用视图

Redis解决缓存击穿问题的两种方法

《Redis解决缓存击穿问题的两种方法》缓存击穿问题也叫热点Key问题,就是⼀个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击,本文给大家介绍了Re... 目录引言解决办法互斥锁(强一致,性能差)逻辑过期(高可用,性能优)设计逻辑过期时间引言缓存击穿:给

Java程序运行时出现乱码问题的排查与解决方法

《Java程序运行时出现乱码问题的排查与解决方法》本文主要介绍了Java程序运行时出现乱码问题的排查与解决方法,包括检查Java源文件编码、检查编译时的编码设置、检查运行时的编码设置、检查命令提示符的... 目录一、检查 Java 源文件编码二、检查编译时的编码设置三、检查运行时的编码设置四、检查命令提示符

Jackson库进行JSON 序列化时遇到了无限递归(Infinite Recursion)的问题及解决方案

《Jackson库进行JSON序列化时遇到了无限递归(InfiniteRecursion)的问题及解决方案》使用Jackson库进行JSON序列化时遇到了无限递归(InfiniteRecursi... 目录解决方案‌1. 使用 @jsonIgnore 忽略一个方向的引用2. 使用 @JsonManagedR