一、参常引背景介绍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 。
说明 : 通过报错日志定位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); } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.说明 : 以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); } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.说明: 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); } } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.说明 : 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); } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.说明 : 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); } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.说明 : Jedis执行Redis的命令时按照先获取connection后通过connection执行命令的顺序 。 在获取connection和通过connection执行命令的过程中如果发生异常会进行重试且在达到最大重试次数后抛出异常 。 以attempts=5为例 ,如果在获取connection过程中发生异常 ,那么最多重试5次后抛出异常。 综合上述的分析,在使用Jedis的过程中需要合理设置参数包括connectionTimeout & soTimeout & maxAttempts。 maxAttempts :出现异常最大重试次数。connectionTimeout:表示连接超时时间 。soTimeout :读取数据超时时间 。 |
2022年及以后的四个数据中心提示尼康18-300镜头全能之选(高性能、广角长焦一镜到底,满足你的各种摄影需求)苹果6s升级iOS11的体验(以苹果6s为例,探索iOS11的新特性和性能提升)DellInspiron5548(一款超值的笔记本电脑选择)谷歌 Chrome 零日漏洞遭广泛利用,可执行任意代码从“v我50”到“疯狂星期四”:HTTPS如何用47天寿命的证书挡住中间人神舟战神笔记本电脑质量如何?(探索神舟战神笔记本电脑的品质与性能)华硕K45EI323VD-SL(解析华硕K45EI323VD-SL的特点和优势)多款热门 Chrome 扩展程序存在明文传输风险,用户隐私安全受威胁企业通过谈判策略降低赎金支付的实战经验网站建设云服务器b2b信息平台企业服务器源码库亿华云香港物理机