首页 > 其他分享 >Jedis 参数异常引发服务雪崩案例分析

Jedis 参数异常引发服务雪崩案例分析

时间:2023-07-20 10:34:18浏览次数:32  
标签:Redis public 案例 connection Jedis 雪崩 key 连接

作者: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消息堆积的告警,正常情况下服务是不会有这类异常告警,出于警觉性开始进入系统排查过程。
  • 排查的思路基本围绕系统相关的指标:系统的请求量,响应时间,下游服务的响应时间,线程数等指标。

Jedis 参数异常引发服务雪崩案例分析_服务雪崩

说明

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

Jedis 参数异常引发服务雪崩案例分析_主从切换_02

说明

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

Jedis 参数异常引发服务雪崩案例分析_集群模式_03

说明

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

Jedis 参数异常引发服务雪崩案例分析_集群模式_04

说明

  • 事后运维同学反馈在故障时间点Redis集群发生了主从切换,整体时间和故障时间较吻合。

综合各方面的指标信息,判定此次服务的雪崩主要原因应该是Redis主从切换导致,但是引发服务雪崩原因需要进一步的分析。

三、故障过程分析

在进行故障的过程分析之前,首先需要对目前的现象进行分析,需要回答下面几个问题:

  • 接口响应耗时增加为何会引起请求量的陡增?
  • Redis主从切换期间大部分的耗时为啥是2s?
  • 接口的平均响应时间为啥接近60s?

3.1 流量陡降

Jedis 参数异常引发服务雪崩案例分析_主从切换_05

说明

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

3.2 耗时问题

Jedis 参数异常引发服务雪崩案例分析_Redis_06

说明

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

Jedis 参数异常引发服务雪崩案例分析_服务雪崩_07

<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 参数异常引发服务雪崩案例分析_服务雪崩_08

说明

  • 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,能够有效避免服务发生雪崩。

标签:Redis,public,案例,connection,Jedis,雪崩,key,连接
From: https://blog.51cto.com/u_14291117/6782998

相关文章

  • Redis的作用,数据类型,缓存穿透,击穿和雪崩,Redis的索引模式【杭州多测师_王sir】
     一、Redis的作用:1.缓存数据,存在内存当中,效率非常高,比存储型数据快上千倍2.计数器,比如用户访问了多少次,点赞数统计3.可以限制 IP 的访问频率4.可以设置失效时间,可以用来设置优惠券到期时间5.任务队列:比如到货通知,内容更新6.排行榜:redis 的有序集合类型非常适合处理榜单和排序......
  • R语言泊松Poisson回归模型分析案例|附代码数据
    原文链接:http://tecdat.cn/?p=2605最近我们被客户要求撰写关于泊松Poisson回归的研究报告,包括一些图形和统计输出。这个问题涉及马蹄蟹研究的数据。研究中的每只雌性马蹄蟹都有一只雄性螃蟹贴在她的巢穴中。这项研究调查了影响雌蟹是否有其他男性居住在她附近的因素。被认为影......
  • 案例:给房子置办家具
    1'''2给一套房子装修即房子里面添加家具31.家具的属性41.名称52.大小/面积62.房子的属性71.位置82.总面积93.空闲面积103.房子的行为:111.置办家具12'''131415classFurniture():#类定义有名称后面还......
  • JVM系统优化实践(20):GC生产环境案例(三)
    您好,这里是「码农镖局」51CTO博客,欢迎您来,欢迎您再来~某新手开发工程师接到了一个保存Elasticsearch日志的任务,以供后续分析之用。但写代码的时候,误将保存日志的代码段弄成了无限循环,程序启动后,没用多久就崩溃了。另一名工程师在动态创建类时,没有实现动态代理机制,也就没有缓存动态生......
  • 使用 Apache SeaTunnel 实现 Kafka Source 解析复杂Json 案例
    版本说明:SeaTunnel:apache-seatunnel-2.3.2-SNAPHOT引擎说明:Flink:1.16.2Zeta:官方自带前言近些时间,我们正好接手一个数据集成项目,数据上游方是给我们投递到Kafka,我们一开始的技术选型是SpringBoot+Flink对上游数据进行加工处理(下文简称:方案一),由于测试不到位,后来到线上,发现......
  • 特殊案例分享
    描述   配置 natstaticoutbound10.4.207.11172.19.11.2descriptionDX_GJinterfaceGigabitEthernet0/5portlink-moderouteipaddress172.19.11.2255.255.255.252natoutbound2010nathairpinenablenatstaticenableiproute-static192.168.69......
  • 转:springboot2.0 集成redis服务详解,以及 (Lettuce & Jedis)
    springboot2.0集成redis服务详解,以及(Lettuce&Jedis)   ......
  • 综合案例一-热词
    综合案例一-热词目录综合案例一-热词1、设计需求2、设计所需标签和CSS样式3、设计具体步骤4、遇到的问题设计图如下1、设计需求①需要鼠标放上去有显示透明②需要点击后跳转到相应页面且保留原页面2、设计所需标签和CSS样式所需标签:divCSS样式:伪类hover,颜色color,字大小fo......
  • 7.19-分摸 一枪2模(09案例分析)包括 分模-做虎口-做摸胚
      ......
  • 7.19-分模(接着上午那个案例 只不过多了开框,打螺丝,管理图层,顶针(丝筒针)中托司)这几个功能
    开框开在上下模核心的产品框位置(不开框的话打螺丝会穿透上下模的位置)正常情况下打螺丝会在上模框的位置打打在模仁位置的一半位置而不是直接打穿切记开框之后要移除参数螺丝打在上下模虎口的位置 ......