Redis 热 Key 发现以及解决办法

哈喽,大家好,我是了不起。 最近一直在处理Redis的一些问题,给大家分享一下看到的一篇关于Redis热点key的文章。

热 Key 带来问题

其实,按我理解来说,没有什么所谓的热 Key 问题,所谓的热 Key 问题都是由于对某个 Key 的访问量过大所产生的一些衍生问题。

由于某个 Key 的数据一定是存储到后端某台服务器的 Redis 单个实例上,如果对这个 Key 突然出现大量的请求操作,这样就会造成流量过于集中,达到 Redis 单个实例处理上限,可能会导致 Redis 实例 CPU 使用率 100%,或者是网卡流量达到上限等,对系统的稳定性和可用性造成影响,或者更为严重出现服务器宕机,无法对外提供服务;更有甚者在出现 Redis 服务不可用之后,大量的数据请求全部落地数据库查询上,Redis 都已经顶不住了,数据库也是分分钟挂掉的节奏,这个是所谓的热 Key 问题。

  • 流量集中,达到服务器处理上限(CPU、网络 IO 等);
  • 会影响在同一个 Redis 实例上其他 Key 的读写请求操作;
  • Key 请求落到同一个 Redis 实例上,无法通过扩容解决;
  • 大量 Redis 请求失败,查询操作可能打到数据库,拖垮数据库,导致整个服务不可用。

通过上面的描述,其实热 key 问题不仅是对某个 Key 请求流量过于集中的问题,也和服务器性能有很大的关联,如果是一个普通的 Docker 容器(4C8G4CPU8G内存,大家不要笑,我们系统好多这种规格服务器),和一台性能强劲的物理机,这两种不同的服务器规格配置对热 Key 的认定估计也不会相同,这个可能是我对热 key 问题的一点不同理解。

如何发现热 Key

通过上面的分析,出现热 Key 的危害还是很大的,我们不可能等到热 Key 出现已经拖垮了服务再去处理,那个时候业务一定已经收到影响,损失也是不言而喻的;那么能够在热 Key 出现前通过一些手段提前监控到热 Key 的出现,对于保证业务系统的稳定性是非常重要的,那么我们都有哪些手段提前观测到热 Key 的出现呢?

凭借业务经验,预估热 Key 出现

根据业务系统上线的一些活动和功能,我们是可以在某些场景下提前预估热 Key 的出现的,比如业务需要进行一场商品秒杀活动,秒杀商品信息和数量一般都会缓存到 Redis 中,这种场景极有可能出现热 Key 问题的。

  • 优点:简单,凭经验发现热 Key,提早发现提早处理;
  • 缺点:没有办法预测所有热 Key 出现,比如某些热点新闻事件,无法提前预测。

客户端进行收集

一般我们在连接 Redis 服务器时都要使用专门的 SDK(比如:Java 的客户端工具 JedisRedisson),我们可以对客户端工具进行封装,在发送请求前进行收集采集,同时定时把收集到的数据上报到统一的服务进行聚合计算。

  • 优点:方案简单
  • 缺点:
    • 对客户端代码有一定入侵,或者需要对 SDK 工具进行二次开发;
    • 没法适应多语言架构,每一种语言的 SDK 都需要进行开发,后期开发维护成本较高。

在代理层进行收集

如果所有的 Redis 请求都经过 Proxy(代理)的话,可以考虑改动 Proxy 代码进行收集,思路与客户端基本类似。

img

  • 优点:对使用方完全透明,能够解决客户端 SDK 的语言异构和版本升级问题;
  • 缺点:
    • 开发成本会比客户端高些;
    • 并不是所有的 Redis 集群架构中都有 Proxy 代理(使用这种方式必须要部署 Proxy)。

使用 Redis 自带命令

hotkeys 参数

Redis4.0.3 版本中添加了 hotkeys 查找特性,可以直接利用 redis-cli --hotkeys 获取当前 keyspace 的热点 key,实现上是通过 scan + object freq 完成的。

  • 优点:无需进行二次开发,能够直接利用现成的工具;
  • 缺点:
    • 由于需要扫描整个 keyspace,实时性上比较差;
    • 扫描时间与 key 的数量正相关,如果 key 的数量比较多,耗时可能会非常长。

monitor 命令

monitor 命令可以实时抓取出 Redis 服务器接收到的命令,通过 redis-cli monitor 抓取数据,同时结合一些现成的分析工具,比如 redis-faina,统计出热 Key。

  • 优点:无需进行二次开发,能够直接利用现成的工具;
  • 缺点:该命令在高并发的条件下,有内存增暴增的隐患,还会降低 Redis 的性能。

Redis 节点抓包分析

Redis 客户端使用 TCP 协议与服务端进行交互,通信协议采用的是 RESP 协议。自己写程序监听端口,按照 RESP 协议规则解析数据,进行分析。或者我们可以使用一些抓包工具,比如 tcpdump 工具,抓取一段时间内的流量进行解析。

  • 优点:对 SDK 或者 Proxy 代理层没有入侵;
  • 缺点:
    • 有一定的开发成本;
    • Key 节点的网络流量和系统负载已经比较高了,抓包可能会导致情况进一步恶化。

热 Key 问题解决方案

增加 Redis 实例副本数量

对于出现热 KeyRedis 实例,我们可以通过水平扩容增加副本数量,将读请求的压力分担到不同副本节点上。

二级缓存(本地缓存)

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

使用本地缓存需要注意两个问题:

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

热 Key 备份

通过前面的分析,我们可以了解到,之所以出现热 Key,是因为有大量的对同一个 Key 的请求落到同一个 Redis 实例上,如果我们可以有办法将这些请求打散到不同的实例上,防止出现流量倾斜的情况,那么热 Key 问题也就不存在了。

那么如何将对某个热 Key 的请求打散到不同实例上呢?我们就可以通过热 Key 备份的方式,基本的思路就是,我们可以给热 Key 加上前缀或者后缀,把一个热 Key 的数量变成 Redis 实例个数 N 的倍数 M,从而由访问一个 Redis Key 变成访问 N * MRedis KeyN * MRedis Key 经过分片分布到不同的实例上,将访问量均摊到所有实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// N 为 Redis 实例个数,M 为 N 的 2倍
const M = N * 2
//生成随机数
random = GenRandom(0, M)
//构造备份新 Key
bakHotKey = hotKey + "_" + random
data = redis.GET(bakHotKey)
if data == NULL {
    data = redis.GET(hotKey)
    if data == NULL {
        data = GetFromDB()
        // 可以利用原子锁来写入数据保证数据一致性
        redis.SET(hotKey, data, expireTime)
        redis.SET(bakHotKey, data, expireTime + GenRandom(0, 5))
    } else {
        redis.SET(bakHotKey, data, expireTime + GenRandom(0, 5))
    }
}

在这段代码中,通过一个大于等于 1 小于 M 的随机数,得到一个 bakHotKey,程序会优先访问 bakHotKey,在得不到数据的情况下,再访问原来的 hotkey,并将 hotkey 的内容写回 bakHotKey。值得注意的是,bakHotKey 的过期时间是 hotkey 的过期时间加上一个较小的随机正整数,这是通过坡度过期的方式,保证在 hotkey 过期时,所有 bakHotKey 不会同时过期而造成缓存雪崩。

总结

在这一篇文章中我们首先分析了在 Redis 中热 Key 带来的一些问题,同时也介绍了在海量的 Redis Key 中找到热 Key 的一些方法,最后也提到了在解决热 Key 问题中我们常用的一些办法;总结来说,RedisKey 问题首先是请求流量过大造成的,但是更深层次原因还是出现了流量倾斜,单个 Redis 实例承担的流量过大造成的,了解到了本质原因,解决的思路也就简单了,就是要想尽一切办法将单个实例承担的流量打散,让每个机器均衡承担热 Key 的流量,不要出现流量倾斜,保证系统的稳定性。

Java Geek Tech wechat
欢迎订阅 Java 技术指北,这里分享关于 Java 的一切。