likes
comments
collection
share

记一次redis热key、大key引发的线上事故

作者站长头像
站长
· 阅读数 7

背景

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

思路一

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

# 分析统计热key
redis-cli --hotkeys 

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

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

记一次redis热key、大key引发的线上事故 此方案暂时放弃,主要怕对节点造成阻塞

思路二

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

记一次redis热key、大key引发的线上事故 通过接口代码分析,发现其中一个接口,for循环里面调用redis,获取值,伪代码如下:

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优化方案

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

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

  • 使用本地二级缓存

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

记一次redis热key、大key引发的线上事故

这里有两个缺点:

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

统一解决方案

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

记一次redis热key、大key引发的线上事故

写作不易,刚好你看到,刚好对你有帮助,麻烦点点赞,有疑问的欢迎留言或者私信讨论。