Jedis 参数异常引发服务雪崩案例分析
作者:vivo 互联网服务器团队 - Wang Zhi
Redis 作为互联网业务首选的远程缓存工具而被大面积使用,作为访问客户端的 Jedis 同样被大面积使用。本文主要分析 Redis3.x 版本集群模式发生主从切换场景下 Jedis 的参数设置不合理引发服务雪崩的过程。
一、背景介绍
Redis作为互联网业务首选的远程缓存工具而被被大家熟知和使用,在客户端方面涌现了Jedis、Redisson、Lettuce等,而Jedis属于其中的佼佼者。
目前笔者的项目采用Redis的3.x版本部署的集群模式(多节点且每个节点存在主从节点),使用Jedis作为Redis的访问客户端。
日前Redis集群中的某节点因为宿主物理机故障导致发生主从切换,在主从切换过程中触发了Jedis的重试机制进而引发了服务的雪崩。
本文旨在剖析Redis集群模式下节点发生主从切换进而引起服务雪崩的整个过程,希望能够帮助读者规避此类问题。
二、故障现场记录
消息堆积告警
【MQ-消息堆积告警】
告警时间:2022-11-29 23:50:21
检测规则: 消息堆积阈值:-》异常( > 100000)
告警服务:xxx-anti-addiction
告警集群:北京公共
告警对象:xxx-login-event-exchange/xxx-login-event-queue
异常对象(当前值): 159412
说明:
2022-11-29 23:50:21收到一条RMQ消息堆积的告警,正常情况下服务是不会有这类异常告警,出于警觉性开始进入系统排查过程。
排查的思路基本围绕系统相关的指标:系统的请求量,响应时间,下游服务的响应时间,线程数等指标。

说明:
排查系统监控之后发现在故障发生时段服务整体的请求量有大幅下跌,响应的接口的平均耗时接近1分钟。
服务整体出于雪崩状态,请求耗时暴涨导致服务不可用,进而导致请求量下跌。

说明:
排查服务的下游应用发现故障期间Redis的访问量大幅下跌,已趋近于0。
项目中较长用的Redis的响应耗时基本上在2s。

说明:
排查系统对应的线程数,发现在故障期间处于wait的线程数大量增加。

说明:
事后运维同学反馈在故障时间点Redis集群发生了主从切换,整体时间和故障时间较吻合。
综合各方面的指标信息,判定此次服务的雪崩主要原因应该是Redis主从切换导致,但是引发服务雪崩原因需要进一步的分析。
三、故障过程分析
在进行故障的过程分析之前,首先需要对目前的现象进行分析,需要回答下面几个问题:
接口响应耗时增加为何会引起请求量的陡增?
Redis主从切换期间大部分的耗时为啥是2s?
接口的平均响应时间为啥接近60s?
3.1 流量陡降

说明:
通过nginx的日志可以看出存在大量的connection timed out的报错,可以归因为由于后端服务的响应时间过程导致nginx层和下游服务之间的读取超时。
由于大量的读取超时导致nginx判断为后端的服务不可用,进而触发了no live upstreams的报错,ng无法转发到合适的后端服务。
通过nginx的日志可以将问题归因到后端服务异常导致整体请求量下跌。
3.2 耗时问题

说明:
通过报错日志定位到Jedis在获取连接的过程中抛出了connect timed out的异常。
通过定位Jedis的源码发现默认的设置连接超时时间 DEFAULT_TIMEOUT = 2000。

<redis-cluster name="redisCluster" timeout="3000" maxRedirections="6"> // 最大重试次数为6
<properties>
<property name="maxTotal" value="20" />
<property name="maxIdle" value="20" />
<property name="minIdle" value="2" />
</properties>
</redis-cluster>
说明:
通过报错日志定位Jedis执行了6次重试,每次重试耗时参考设置连接超时默认时长2s,单次请求约耗时12s。
排查部分对外接口,发现一次请求内部总共访问的Redis次数有5次,那么整体的响应时间会达到1m=60s。
结合报错日志和监控指标,判定服务的雪崩和Jedis的连接重试机制有关,需要从Jedis的源码进一步进行分析。
四、Jedis 执行流程
4.1 流程解析

说明:
Jedis处理Redis的命令请求如上图所示,整体在初始化连接的基础上根据计算的slot槽位获取连接后发送命令进行执行。
在获取连接失败或命令发送失败的场景下触发异常重试,重新执行一次命令。
异常重试流程中省略了重新获取Redis集群分布的逻辑,避免复杂化整体流程。
4.2 源码解析
(1)整体流程
public class JedisCluster extends BinaryJedisCluster implements JedisCommands,
MultiKeyJedisClusterCommands, JedisClusterScriptingCommands {
@Override
public String set(final String key, final String value, final String nxxx, final String expx,
final long time) {
return new JedisClusterCommand<String>(connectionHandler, maxAttempts) {
@Override
public String execute(Jedis connection) {
// 真正发送命令的逻辑
return connection.set(key, value, nxxx, expx, time);
}
}.run(key); // 通过run触发命令的执行
}
}
public abstract class JedisClusterCommand<T> {
public abstract T execute(Jedis connection);
public T run(String key) {
// 执行带有重试机制的方法
return runWithRetries(SafeEncoder.encode(key), this.maxAttempts, false, false);
}
}
public abstract class JedisClusterCommand<T> {
private T runWithRetries(byte[] key, int attempts, boolean tryRandomNode, boolean asking) {
Jedis connection = null;
try {
if (asking) {
// 省略相关的代码逻辑
} else {
if (tryRandomNode) {
connection = connectionHandler.getConnection();
} else {
// 1、尝试获取连接
connection = connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));
}
}
// 2、执行JedisClusterCommand封装的execute命令
return execute(connection);
} catch (JedisNoReachableClusterNodeException jnrcne) {
throw jnrcne;
} catch (JedisConnectionException jce) {
// 省略代码
} finally {
releaseConnection(connection);
}
}
}
说明:
以JedisCluster执行set命令为例,封装成JedisClusterCommand对象通过run触发runWithRetries进而执行set命令的execute方法。
runWithRetries方法封装了具体的重试逻辑,内部通过connectionHandler.getConnectionFromSlot
获取对应的Redis节点的连接。
(2)计算槽位
public final class JedisClusterCRC16 {
public static int getSlot(byte[] key) {
int s = -1;
int e = -1;
boolean sFound = false;
for (int i = 0; i < key.length; i++) {
if (key[i] == '{' && !sFound) {
s = i;
sFound = true;
}
if (key[i] == '}' && sFound) {
e = i;
break;
}
}
if (s > -1 && e > -1 && e != s + 1) {
return getCRC16(key, s + 1, e) & (16384 - 1);
}
return getCRC16(key) & (16384 - 1);
}
}
说明:
Redis集群模式下通过计算slot槽位来定位具体的Redis节点的连接,Jedis通过JedisClusterCRC16.getSlot(key)来获取slot槽位。
Redis的集群模式的拓扑信息在Jedis客户端同步维护了一份,具体的slot槽位计算在客户端实现。
(3)连接获取
public class JedisSlotBasedConnectionHandler extends JedisClusterConnectionHandler {
@Override
public Jedis getConnectionFromSlot(int slot) {
JedisPool connectionPool = cache.getSlotPool(slot);
if (connectionPool != null) {
// 尝试获取连接
return connectionPool.getResource();
} else {
renewSlotCache();
connectionPool = cache.getSlotPool(slot);
if (connectionPool != null) {
return connectionPool.getResource();
} else {
return getConnection();
}
}
}
}
class JedisFactory implements PooledObjectFactory<Jedis> {
@Override
public PooledObject<Jedis> makeObject() throws Exception {
// 1、创建Jedis连接
final HostAndPort hostAndPort = this.hostAndPort.get();
final Jedis jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort(), connectionTimeout,
soTimeout, ssl, sslSocketFactory, sslParameters, hostnameVerifier);
try {
// 2、尝试进行连接
jedis.connect();
} catch (JedisException je) {
jedis.close();
throw je;
}
return new DefaultPooledObject<Jedis>(jedis);
}
}
public class Connection implements Closeable {
public void connect() {
if (!isConnected()) {
try {
socket = new Socket();
socket.setReuseAddress(true);
socket.setKeepAlive(true); // Will monitor the TCP connection is
socket.setTcpNoDelay(true); // Socket buffer Whetherclosed, to
socket.setSoLinger(true, 0); // Control calls close () method,
// 1、设置连接超时时间 DEFAULT_TIMEOUT = 2000;
socket.connect(new InetSocketAddress(host, port), connectionTimeout);
// 2、设置读取超时时间
socket.setSoTimeout(soTimeout);
outputStream = new RedisOutputStream(socket.getOutputStream());
inputStream = new RedisInputStream(socket.getInputStream());
} catch (IOException ex) {
broken = true;
throw new JedisConnectionException(ex);
}
}
}
}
说明:
Jedis通过connectionPool维护和Redis的连接信息,在可复用的连接不够的场景下会触发连接的建立和获取。
创建连接对象通过封装成Jedis对象并通过connect进行连接,在Connection的connect的过程中设置连接超时connectionTimeout和读取超时soTimeout。
建立连接过程中如果异常会抛出JedisConnectionException异常,注意这个异常会在后续的分析中多次出现。
(4)发送命令
public class Connection implements Closeable {
protected Connection sendCommand(final Command cmd, final byte[]... args) {
try {
// 1、必要时尝试连接
connect();
// 2、发送命令
Protocol.sendCommand(outputStream, cmd, args);
pipelinedCommands++;
return this;
} catch (JedisConnectionException ex) {
broken = true;
throw ex;
}
}
private static void sendCommand(final RedisOutputStream os, final byte[] command,
final byte[]... args) {
try {
// 按照redis的命令格式发送数据
os.write(ASTERISK_BYTE);
os.writeIntCrLf(args.length + 1);
os.write(DOLLAR_BYTE);
os.writeIntCrLf(command.length);
os.write(command);
os.writeCrLf();
for (final byte[] arg : args) {
os.write(DOLLAR_BYTE);
os.writeIntCrLf(arg.length);
os.write(arg);
os.writeCrLf();
}
} catch (IOException e) {
throw new JedisConnectionException(e);
}
}
}
说明:
Jedis通过sendCommand向Redis发送Redis格式的命令。
发送过程中会执行connect连接动作,逻辑和获取连接时的connect过程一致。
发送命令异常会抛出JedisConnectionException的异常信息。
(5)重试机制
public abstract class JedisClusterCommand<T> {
private T runWithRetries(byte[] key, int attempts, boolean tryRandomNode, boolean asking) {
Jedis connection = null;
try {
if (asking) {
} else {
if (tryRandomNode) {
connection = connectionHandler.getConnection();
} else {
// 1、尝试获取连接
connection = connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));
}
}
// 2、通过连接执行命令
return execute(connection);
} catch (JedisNoReachableClusterNodeException jnrcne) {
throw jnrcne;
} catch (JedisConnectionException jce) {
releaseConnection(connection);
connection = null;
// 4、重试到最后一次抛出异常
if (attempts <= 1) {
this.connectionHandler.renewSlotCache();
throw jce;
}
// 3、进行第一轮重试
return runWithRetries(key, attempts - 1, tryRandomNode, asking);
} finally {
releaseConnection(connection);
}
}
}
说明:
Jedis执行Redis的命令时按照先获取connection后通过connection执行命令的顺序。
在获取connection和通过connection执行命令的过程中如果发生异常会进行重试且在达到最大重试次数后抛出异常。
以attempts=5为例,如果在获取connection过程中发生异常,那么最多重试5次后抛出异常。
综合上述的分析,在使用Jedis的过程中需要合理设置参数包括connectionTimeout & soTimeout & maxAttempts。
maxAttempts:出现异常最大重试次数。
connectionTimeout:表示连接超时时间。
soTimeout:读取数据超时时间。
五、总结
本文通过线上故障现场记录和分析,并最终引申到Jedis源码的底层逻辑分析,剖析了Jedis的不合理参数设置包括连接超时和最大重试次数导致服务雪崩的整个过程。
在Redis本身只作为缓存且后端的MySQL等DB能够承载非高峰期流量的场景下,建议合理设置Jedis超时参数进而减少Redis主从切换访问Redis的耗时,避免服务雪崩。
线上环境笔者目前的连接和读取超时时间设置为100ms,最大重试次数为2,按照现有的业务逻辑如遇Redis节点故障访问异常最多耗时1s,能够有效避免服务发生雪崩。
Jedis 参数异常引发服务雪崩案例分析的更多相关文章
- jedis参数不当引发的问题总结
jedis参数不当引发dubbo服务线程池耗尽异常 现象:一个dubbo服务偶发性的出现个别机器甚至整个集群大量报线程池耗尽的问题.一开始对问题的处理比较粗暴,直接增加了10倍的线程数.但是问题依然偶 ...
- 一个 redis 异常访问引发 oom 的案例分析
「推断的前提是以事实为依据.」 这两天碰到一个线上系统的偶尔出现突然堆内存暴涨,这倒不是个什么疑难杂症, 只是过程中有些思路觉得可以借鉴参考,故总结下并写下来. 现象 内存情况可以看看下面这张监控图. ...
- java dump 内存分析 elasticsearch Bulk异常引发的Elasticsearch内存泄漏
Bulk异常引发的Elasticsearch内存泄漏 2018年8月24日更新: 今天放出的6.4版修复了这个问题. 前天公司度假部门一个线上ElasticSearch集群发出报警,有Data Nod ...
- keepalived主备节点都配置vip,vip切换异常案例分析
原文地址:http://blog.51cto.com/13599730/2161622 参考地址:https://blog.csdn.net/qq_14940627/article/details/7 ...
- Form_通过Trace分析Concurrent和Form性能和异常详解(案例)
2014-06-21 Created By BaoXinjian
- Windows Azure案例分析: 选择虚拟机或云服务?
作者 王枫 发布于2013年6月27日 随着云计算技术和市场的日渐成熟,企业在考虑IT管理和运维时的选择也更加多样化,应用也从传统部署方式,发展为私有云.公有云.和混合云等部署方式.作为微软核心的公有 ...
- 《深入理解Java虚拟机》-----第5章 jvm调优案例分析与实战
案例分析 高性能硬件上的程序部署策略 例 如 ,一个15万PV/天左右的在线文档类型网站最近更换了硬件系统,新的硬件为4个CPU.16GB物理内存,操作系统为64位CentOS 5.4 , Resin ...
- 5、JVM--调优案例分析
5.1.案例分析 5.1.1.高性能硬件上的程序部署策略 假如一个15w/天左右的在线文档类型网站再准备更换硬件系统 新的硬件为4个CPU.16GB物理内存,操作系统为64为Cento是 Resin作 ...
- Salesforce学习之路-developer篇(五)一文读懂Aura原理及实战案例分析
1. 什么是Lightning Component框架? Lightning Component框架是一个UI框架,用于为移动和台式设备开发Web应用程序.这是一个单页面Web应用框架,用于为Ligh ...
- 软工案例分析之OJ
项目 内容 这个作业属于哪个课程 2021春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 案例分析作业要求 我在这个课程的目标是 和我的团队开发一个真正的软件,一起提升开发与合作的能力 这 ...
随机推荐
- GDOU-CTF-2023新生赛Pwn题解与反思
第一次参加CTF新生赛总结与反思 因为昨天学校那边要进行天梯模拟赛,所以被拉过去了.16点30分结束,就跑回来宿舍开始写.第一题和第二题一下子getshell,不用30分钟,可能我没想那么多,对比网上 ...
- windows系统git使用ssh方式和gitee/github进行同步
前言 在从github/gitee远程仓库获取代码时,除了使用https方式,我们还可以使用ssh连接的方式与远程仓库服务器通信,其好处是有时会比https更方便.稳定.快速. 和与普通的linux服 ...
- Docker构建镜像踩坑日记
从Github上拉取python项目后,运行dockerfile构建镜像失败,一步步查找原因 主要原因就是国内下载各种依赖超时,以下提供pip.apt.pipenv镜像解决方案 pip更换国内镜像 这 ...
- Linux云计算运维工程师day29软件安装
1. diff(文本比较) [root@guosaike ~]# cp /etc/passwd{,.ori}备份 [root@guosaike ~]# diff /etc/passwd{,.ori} ...
- 使用GitHub当博客图床提升博客访问速度
前言 作为一个穷逼来说站长来说,只有一个1M宽带这样的小水管服务器,如果博客稍微放一点图片到本地,然后人多点访问网站基本就很卡了,但又不想去吧图片放到图床里然后复制链接到文章里面那么麻烦 如何解决这个 ...
- Grafana系列-统一展示-6-Zabbix仪表板
系列文章 Grafana 系列文章 Notes: 关于 Grafana系列-统一展示-6-Zabbix 数据源, 其实已经在之前的文章: 使用 Grafana 统一监控展示 - 对接 Zabbix 里 ...
- 2022-03-31:有一组 n 个人作为实验对象,从 0 到 n - 1 编号,其中每个人都有不同数目的钱, 以及不同程度的安静值(quietness) 为了方便起见,我们将编号为 x 的人简称为
2022-03-31:有一组 n 个人作为实验对象,从 0 到 n - 1 编号,其中每个人都有不同数目的钱, 以及不同程度的安静值(quietness) 为了方便起见,我们将编号为 x 的人简称为 ...
- 2021-12-08:扑克牌中的红桃J和梅花Q找不到了,为了利用剩下的牌做游戏,小明设计了新的游戏规则: 1) A,2,3,4....10,J,Q,K分别对应1到13这些数字,大小王对应0; 2) 游
2021-12-08:扑克牌中的红桃J和梅花Q找不到了,为了利用剩下的牌做游戏,小明设计了新的游戏规则: A,2,3,4-10,J,Q,K分别对应1到13这些数字,大小王对应0; 游戏人数为2人,轮流 ...
- java中this的内存原理以及成员变量和局部变量
this的内存原理 1.this的作用: 区分局部变量和成员变量 eg: public class Student{ private int age; public void method(){ in ...
- Mysql- DDL/DML/DQL/DCL 数据库基本操作语句(持续更新中)
Mysql基本语法 前言: 在测试项目中经常需要使用到简单的Mysql语句,但是不知道语句结构是什么,经常在百度查来查去: 以下就是总结Mysql常用的基础操作语句: 只需要执行从创建开始执行示例中的 ...