作者: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 参数异常引发服务雪崩案例分析的更多相关文章

  1. jedis参数不当引发的问题总结

    jedis参数不当引发dubbo服务线程池耗尽异常 现象:一个dubbo服务偶发性的出现个别机器甚至整个集群大量报线程池耗尽的问题.一开始对问题的处理比较粗暴,直接增加了10倍的线程数.但是问题依然偶 ...

  2. 一个 redis 异常访问引发 oom 的案例分析

    「推断的前提是以事实为依据.」 这两天碰到一个线上系统的偶尔出现突然堆内存暴涨,这倒不是个什么疑难杂症, 只是过程中有些思路觉得可以借鉴参考,故总结下并写下来. 现象 内存情况可以看看下面这张监控图. ...

  3. java dump 内存分析 elasticsearch Bulk异常引发的Elasticsearch内存泄漏

    Bulk异常引发的Elasticsearch内存泄漏 2018年8月24日更新: 今天放出的6.4版修复了这个问题. 前天公司度假部门一个线上ElasticSearch集群发出报警,有Data Nod ...

  4. keepalived主备节点都配置vip,vip切换异常案例分析

    原文地址:http://blog.51cto.com/13599730/2161622 参考地址:https://blog.csdn.net/qq_14940627/article/details/7 ...

  5. Form_通过Trace分析Concurrent和Form性能和异常详解(案例)

    2014-06-21 Created By BaoXinjian

  6. Windows Azure案例分析: 选择虚拟机或云服务?

    作者 王枫 发布于2013年6月27日 随着云计算技术和市场的日渐成熟,企业在考虑IT管理和运维时的选择也更加多样化,应用也从传统部署方式,发展为私有云.公有云.和混合云等部署方式.作为微软核心的公有 ...

  7. 《深入理解Java虚拟机》-----第5章 jvm调优案例分析与实战

    案例分析 高性能硬件上的程序部署策略 例 如 ,一个15万PV/天左右的在线文档类型网站最近更换了硬件系统,新的硬件为4个CPU.16GB物理内存,操作系统为64位CentOS 5.4 , Resin ...

  8. 5、JVM--调优案例分析

    5.1.案例分析 5.1.1.高性能硬件上的程序部署策略 假如一个15w/天左右的在线文档类型网站再准备更换硬件系统 新的硬件为4个CPU.16GB物理内存,操作系统为64为Cento是 Resin作 ...

  9. Salesforce学习之路-developer篇(五)一文读懂Aura原理及实战案例分析

    1. 什么是Lightning Component框架? Lightning Component框架是一个UI框架,用于为移动和台式设备开发Web应用程序.这是一个单页面Web应用框架,用于为Ligh ...

  10. 软工案例分析之OJ

    项目 内容 这个作业属于哪个课程 2021春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 案例分析作业要求 我在这个课程的目标是 和我的团队开发一个真正的软件,一起提升开发与合作的能力 这 ...

随机推荐

  1. 以SQLserver为例的Dapper详细讲解

    Dapper是一种轻量级的ORM(对象关系映射)工具,它提供了高效且易于使用的方式来执行数据库操作.Dapper是由Stack Overflow团队开发并维护的,它的主要目标是提供比EF更快.更直接的 ...

  2. Longformer详解——从Self-Attention说开去

    1.Longformer的应用场景 为了理解Longformer的原理,我们最好首先从为何需要使用Longformer开始说起.(这里默认各位已经对Self Attention等基础知识有一定的了解) ...

  3. 如何借助分布式存储 JuiceFS 加速 AI 模型训练

    传统的机器学习模型,数据集比较小,模型的算法也比较简单,使用单机存储,或者本地硬盘就足够了,像 JuiceFS 这样的分布式存储并不是必需品. 随着近几年深度学习的蓬勃发展,越来越多的团队开始遇到了单 ...

  4. 解决Kibana(OpenSearch)某些字段无法搜索问题

    背景 最近在OpenSearch查看线上日志的时候,发现某个索引下有些字段无法直接在界面上筛选,搜索到也不高亮,非常的不方便,就像下面这样 字段左侧两个筛选按钮禁用了无法点击,提示 Unindexed ...

  5. ts中接口

    前言:ts定义接口的任意一个属性 interface IPerson { name: string age: number family?: any[] // Error,因为不是任意类型的子集 [p ...

  6. 从不均匀性角度浅析AB实验

    作者:京东零售 路卫强 本篇的目的是从三个不均匀性的角度,对AB实验进行一个认知的普及,最终着重讲述AB实验的一个普遍的问题,即实验准确度问题. 一.AB实验场景 在首页中,我们是用红色基调还是绿色基 ...

  7. 关于java中的多态和对实例化对象的一些理解

    java面向对象三大特征即为:继承封装多态.而多态需要三大必要条件.分别是:继承.方法重写.父类引用指向子类对象.我们先一个一个来理解. 1.首先是继承和重写.这个很简单.因为多态就是建立在不同的重写 ...

  8. 波场(Tron) 网页版钱包开源

    之前做区块链项目太难了,很多组件.工具没有开源项目,需要自己写很麻烦. 我整理了几个自己给公司开发项目的时候,分离出来的几个工具,已经上传到 Gihub 了,感觉浏览量还行,在这里给园子里的朋友分享下 ...

  9. Lattics:一款简单易用、好看强大的「类脑式」知识管理工具,笔记应用与写作软件二合一

    如何选择一款适合自己的知识管理工具? 随着数字化时代的到来,越来越多的人意识到知识管理的重要性.笔记软件和写作软件作为一种常用的知识管理工具,一直以来备受关注.从最早的印象笔记.有道云.为知笔记.幕布 ...

  10. Prompt learning 教学[进阶篇]:简介Prompt框架并给出自然语言处理技术:Few-Shot Prompting、Self-Consistency等;项目实战搭建知识库内容机器人

    Prompt learning 教学[进阶篇]:简介Prompt框架并给出自然语言处理技术:Few-Shot Prompting.Self-Consistency等:项目实战搭建知识库内容机器人 1. ...