人为制造redis的热key、大key引发的线上事故

背景

Redis中间件,我们主要是用来做缓存,缓解数据库的访问压力,我们搭建的是redis集群

在一个风和日丽的下午,突然收到运维的报警信息

运维:小李,你们使用的redis中间件所在的服务器,有大量的流量流出,宽带快要占满了,网卡都冒烟了,严重影响其他服务,快速排查解决下,如果一时半会解决不了,我们只能kill掉redis的进程,避免影响其他服务

小李:我们立刻排查,有需要协助的,请你们帮忙

我们redis使用的是集群,三主三从,怎么才有一台流量这么大?大概率我估计是遇到了大key、热key,大key是存储的数据量大,热key访问频率高,分布还不均匀,才能导致单台机器流量非常大

先介绍下 热key、大key,让大家有个了解

问题的严重性

在使用Redis的过程中,如果未能及时发现并处理大Key与热Key,可能会导致服务性能下降、用户体验变差,甚至引发大面积故障。

大Key和热Key的定义

什么是BigKey

通常以Key的大小和Key中成员的数量来综合判定,例如:

  • Key本身的数据量过大:一个String类型的Key,它的值为5 MB。
  • Key中的成员数过多:一个ZSET类型的Key,它的成员数量为10,000个。
  • Key中成员的数据量过大:一个Hash类型的Key,它的成员数量虽然只有1,000个但这些成员的Value(值)总大小为100 MB。

什么是热key

通常以其接收到的Key被请求频率来判定,例如:

  • QPS集中在特定的Key:Redis实例的总QPS(每秒查询率)为10,000,而其中一个Key的每秒访问量达到了7,000。
  • 带宽使用率集中在特定的Key:对一个拥有上千个成员且总大小为1 MB的HASH Key每秒发送大量的HGETALL操作请求。
  • CPU使用时间占比集中在特定的Key:对一个拥有数万个成员的Key(ZSET类型)每秒发送大量的ZRANGE操作请求。

上述例子中的具体数值仅供参考,在实际业务中,您需要根据Redis的实际业务场景进行综合判断

大Key和热Key引发的问题

大key

  • 客户端执行命令的时长变慢。
  • Redis内存达到maxmemory参数定义的上限引发操作阻塞或重要的Key被逐出,甚至引发内存溢出(Out Of Memory)。
  • 集群架构下,某个数据分片的内存使用率远超其他数据分片,无法使数据分片的内存资源达到均衡。
  • 对大Key执行读请求,会使Redis实例的带宽使用率被占满,导致自身服务变慢,同时易波及相关的服务。
  • 对大Key执行删除操作,易造成主库较长时间的阻塞,进而可能引发同步中断或主从切换。

热key

  • 占用大量的CPU资源,影响其他请求并导致整体性能降低。
  • 集群架构下,产生访问倾斜,即某个数据分片被大量访问,而其他数据分片处于空闲状态,可能引起该数据分片的连接数被耗尽,新的连接建立请求被拒绝等问题。
  • 在抢购或秒杀场景下,可能因商品对应库存Key的请求量过大,超出Redis处理能力造成超卖。
  • 热Key的请求压力数量超出Redis的承受能力易造成缓存击穿,即大量请求将被直接指向后端的存储层,导致存储访问量激增甚至宕机,从而影响其他业务。

大Key和热Key产生的原因

未正确使用Redis、业务规划不足、无效数据的堆积、访问量突增等都会产生大Key与热Key

大key

  • 不适用的场景下使用Redis,易造成Key的value过大,如使用String类型的Key存放大体积二进制文件型数据;
  • 业务上线前规划设计不足,没有对Key中的成员进行合理的拆分,造成个别Key中的成员数量过多;
  • 未定期清理无效数据,造成如HASH类型Key中的成员持续不断地增加;
  • 使用LIST类型Key的业务消费侧发生代码故障,造成对应Key的成员只增不减。

热key

预期外的访问量陡增,如突然出现的爆款商品、访问量暴涨的热点新闻、直播间某主播搞活动带来的大量刷屏点赞、游戏中某区域发生多个工会之间的战斗涉及大量玩家等。

我们的事故案例

通过上面的介绍,应该对大key、热key,有个了解了,那下面介绍下,我们排查问题的过程

找出热key、大key

思路一

redis 4.0版本以上提供了分析大key、热key的分析工具,前提是:内存策略修改为LFU算法

js 复制代码
# 分析统计热key
redis-cli --hotkeys 

# 分析统计大key
redis-cli --bigkeys

以上两个命令,就可以分析线出线上的热key、大key。也是通过 scan 完成的,可能会对节点造成阻塞,同时bigkeys只能计算每种数据结构的 top1,如果有些数据结构有比较多的 Bigkey是查找不出来的,如下:

此方案暂时放弃,主要怕对节点造成阻塞

思路二

从业务角度出发,我们的系统有链路跟踪,使用的是pinpoint,通过pinpoint查看最近这段时间请求量高、响应慢的接口,找出top靠前的接口,找到接口对应的代码,进行分析,查找这些接口哪里调用了redis,使用的key,然后对key进行查看

通过接口代码分析,发现其中一个接口,for循环里面调用redis,获取值,伪代码如下:

Java 复制代码
public List getData(){
    /**
     * 1、通过条件分页查询数据
     * 2、对查询出来的数据,for循环去查询redis 的数据,然后对查询出来的数据,进行转换,再组装数据
     *
     */
    //查询业务数据
    List<AccessLogEntity> list = new ArrayList<>();
    for (AccessLogEntity accessLog : list) {
        String str = redisTemplate.opsForValue().get("key");
        //json字符转换为对象
        //对list的数据进行匹配 关联
        // 组装数据
    }
    return list;
}

代码分析:

  1. key是固定的,应该放在循环外面调用一次,遍历的时候直接使用即可,这种人为的造成了热key
  2. 对key进行分析,这个key到底多大,连接上reids 使用命令 MEMORY usage key 查看大小,结果发现这个key竟然有100Mb,再看这个key值的写入,是整张表的数据直接存在redis中,这是redis当数据库用了,人才啊真是人才,人为制造大key
  3. 根据业务场景,也不应该使用String,这种场景 使用hash是比较适合的

解决方案

临时解决线上问题

  1. 把获取调用redis的获取值的方法,放在循环体外,减少请求量
  2. redis获取的数据,放在本地缓存(比如:使用Map),持续减少请求量

通过以上两个修改,发布线上,持续观察,运维反馈:流量降下来了,服务正常了

后续优化

通过项目代码分析,这并不存在热key,是人为放在分页循环里面导致的,所以不考虑热key的问题,只需要把大key优化掉即可

我们使用hash替换String,并且key拆分为多个hash key,并确保每个Key的成员数量在合理范围

通过这个事故,也让我深究大key、热key的解决方案

优化大Key与热Key

大key优化方案

  • 对大Key进行拆分

    例如将含有数万成员的一个HASH Key拆分为多个HASH Key,并确保每个Key的成员数量在合理范围。在Redis集群架构中,拆分大Key能对数据分片间的内存平衡起到显著作用。

  • 对大Key进行清理

    将不适用Redis能力的数据存至其它存储,并在Redis中删除此类数据。

  • 对过期数据进行定期清理

    堆积大量过期数据会造成大Key的产生,例如在HASH数据类型中以增量的形式不断写入大量数据而忽略了数据的时效性。可以通过定时任务的方式对失效数据进行清理。

  • 监控Redis的内存水位 定期对redis内存分析redis内存可视化分析,这里使用的是rdb,redis内存大于5G,建议使用redis-rdb-tools,根据自己的业务实际情况对大key的定义,做二次开发,超过设置的阈值,就报警提醒

热key优化方案

  • 在Redis集群架构中对热Key进行复制

    在Redis集群架构中,由于热Key的迁移粒度问题,无法将请求分散至其他数据分片,导致单个数据分片的压力无法下降。此时,可以将对应热Key进行复制并迁移至其他数据分片,例如将热Key foo复制出3个内容完全一样的Key并名为foo2、foo3、foo4,将这三个Key迁移到其他数据分片来解决单个数据分片的热Key压力。

  • 使用本地二级缓存

    当出现热 Key 以后,把热 Key 加载到系统的 JVM 中。后续针对这些热 Key 的请求,会直接从 JVM 中获取,而不会走到 Redis 层。这些本地缓存的工具很多,比如 Ehcache,或者 Google GuavaCache 工具,或者直接使用 HashMap 作为本地缓存工具都是可以的。

这里有两个缺点:

  • 如果对热 Key 进行本地缓存,需要防止本地缓存过大,影响系统性能;
  • 需要处理本地缓存和 Redis 集群数据的一致性问题。

统一解决方案

京东hotkey 对任意突发性的无法预先感知的热点数据,包括并不限于热点数据(如突发大量请求同一个商品)、热用户(如恶意爬虫刷子)、热接口(突发海量请求同一个接口)等,进行毫秒级精准探测到。然后对这些热数据、热用户等,推送到所有服务端JVM内存中,以大幅减轻对后端数据存储层的冲击,并可以由使用者决定如何分配、使用这些热key(譬如对热商品做本地缓存、对热用户进行拒绝访问、对热接口进行熔断或返回默认值)。这些热数据在整个服务端集群内保持一致性,并且业务隔离

相关推荐
程序员阿鹏2 分钟前
SpringBoot自动装配原理
java·开发语言·spring boot·后端·spring·tomcat·maven
Andy工程师3 分钟前
一个接口可以有多个实现类
java
程序员爱钓鱼3 分钟前
Node.js 编程实战:CSV&JSON &Excel 数据处理
前端·后端·node.js
老华带你飞9 分钟前
工会管理|基于springboot 工会管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring
自在极意功。9 分钟前
MyBatis配置文件详解:environments、transactionManager与dataSource全面解析
java·数据库·tomcat·mybatis
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ14 分钟前
配置springdoc swagger开关
java
Echo flower17 分钟前
Spring Boot WebFlux 实现流式数据传输与断点续传
java·spring boot·后端
没有bug.的程序员23 分钟前
微服务中的数据一致性困局
java·jvm·微服务·架构·wpf·电商
鸽鸽程序猿28 分钟前
【Redis】Java客户端使用Redis
java·redis·github
悦悦子a啊28 分钟前
使用 Java 集合类中的 LinkedList 模拟栈以此判断字符串是否是回文
java·开发语言