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

2024-01-09 17:26:57 浏览数 (1)

背景

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**算法

代码语言:shell复制
# 分析统计热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 Guava 中 Cache 工具,或者直接使用 HashMap 作为本地缓存工具都是可以的。

这里有两个缺点:

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

0 人点赞