大家好,今天来为大家解答Jedis参数配置错误导致服务崩溃的案例分析这个问题的一些问题点,包括也一样很多人还不知道,因此呢,今天就来为大家分析分析,现在让我们一起来看看吧!如果解决了您的问题,还望您关注下本站哦,谢谢~
一、背景介绍
Redis作为互联网业务首选的远程缓存工具而被被大家熟知和使用,在客户端方面涌现了Jedis、Redisson、Lettuce等,而Jedis属于其中的佼佼者。
目前笔者的项目采用Redis的3.x版本部署的集群模式(多节点且每个节点存在主从节点),使用Jedis作为Redis的访问客户端。
日前Redis集群中的某节点因为宿主物理机故障导致发生主从切换,在主从切换过程中触发了Jedis的重试机制进而引发了服务的雪崩。
二、故障现场记录
消息堆积告警
【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的日志可以将问题归因到后端服务异常导致整体请求量下跌。
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参数配置错误导致服务崩溃的案例分析的一些知识,其中也会对进行解释,文章篇幅可能偏长,如果能碰巧解决你现在面临的问题,别忘了关注本站,现在就马上开始吧!
本文采摘于网络,不代表本站立场,转载联系作者并注明出处:https://www.iotsj.com//kuaixun/6312.html
用户评论
真是让人深有感触啊,数据访问やっぱり是关键核心吧!
有13位网友表示赞同!
以前听说过雪崩情况,但具体的案例分析确实能让我们更好地理解问题所在。
有10位网友表示赞同!
Jedis作为一款常用的Redis库,它的参数设置确实很关键,一点小错误就可能引发大麻烦。
有8位网友表示赞同!
这个案例提醒我们要加强对程序的测试和监控,避免类似的问题发生。
有12位网友表示赞同!
服务雪崩真是太可怕了,影响用户体验不说,还会造成巨大的经济损失。
有12位网友表示赞同!
希望能看到更多详细的操作步骤和解决方案,这样才能更好地学习和预防这类问题。
有15位网友表示赞同!
案例分析做得很好,可以直观地看到数据异常是如何导致服务崩溃的。
有16位网友表示赞同!
对Jedis使用注意事项了解得更深入了,以后要注意参数设置细节。
有11位网友表示赞同!
数据存储和访问确实需要高度关注,防止出现安全隐患。
有18位网友表示赞同!
这个案例分析很有价值,能帮助我们更全面地理解服务的运行机制。
有11位网友表示赞同!
技术人员一定要重视细节,确保每个环节都运作正常。
有14位网友表示赞同!
希望能定期了解一些最新的分布式系统技术和应对策略。
有12位网友表示赞同!
分享这种案例确实很不错,可以让我们互相学习,共同进步。
有8位网友表示赞同!
对程序架构设计也有些新的认识了,需要更加重视系统的可扩展性和容错机制。
有6位网友表示赞同!
文章内容很实,很有指导意义,能够帮助我们更好地应对技术挑战。
有7位网友表示赞同!
希望以后能分享更多关于类似案例的分析和总结。
有15位网友表示赞同!
对服务故障的处理流程也了解得更深了,下次遇到类似情况可以更有针对性地解决问题。
有20位网友表示赞同!
やっぱり,系统稳定性才是最重要的!
有15位网友表示赞同!
这个案例提醒我们要不断学习新的技术,才能更有效地应对挑战。
有8位网友表示赞同!
文章分析很客观,真实反映了实际的项目经验。
有8位网友表示赞同!