业务场景中经常会有各种热key或大key的问题,如果未能及时处理,可能会导致服务性能下降、用户体验变差,甚至引发大面积故障。所以本文针对这两个问题进行讲解,提供发现/监控的方法以及处理的解决方案。
热Key问题
什么是热key?
热key是服务端的常见问题,指一段时间内某个key的访问量远远超过其他的key,导致大量访问流量落在某一个redis实例中;或者是带宽使用率集中在特定的key(例如,对一个包含2000个field的hash key每秒发送大量的hgetall操作请求);又或者是cpu使用时间占比集中在特定的key(例如,对一个包含10000个field的key每秒发送大量的zrange操作请求)。
以被请求频率来定义是否是热key,没有固定经验值。某个key被高频访问导致系统稳定性变差,都可以定义为热key。
可能造成的问题
热点缓存会导致流量集中,redis缓存与数据库被击穿,从而引发系统雪崩。详情可以看《 快速了解缓存穿透与缓存雪崩 》。
请求分配不均,存在热key的节点面临较大的访问压力,可能出现该数据分片的连接数被耗尽甚至宕机。(即使采取扩容也会对资源有很大的浪费)
发现方法
由于热key发生对系统稳定性有巨大危害,所以需要上线前设立故障预案、建立监控和报警机制,以便快速响应故障。
优点:简单直接。
缺点:但并不是所有业务都能预估出哪些key是热key。
根据业务经验,预估哪些是热key。
在客户端收集。在操作redis之前,加上统计频次的逻辑,然后将统计数据发送给一个聚合计算的服务进行统计。
优点:方案简单。
缺点:无法支持大公司多语言环境的SDK,或者说多语言SDK对齐比较困难。此外SDK的维护升级成本会很高。
在proxy层收集。有些服务在请求redis之前会请求一个proxy服务,这种场景可以使用在proxy层收集热key数据,收集机制类似于在客户端收集。
优点:方案对使用方完全透明;没有SDK多语言异构和升级成本高的问题。(不理解这个地方的话,可以查看小辉之前的博客《 通用能力抽象选择SDK组件还是API服务? 》)
缺点:并不是所有场景都会有proxy层。
redis集群监控。如果出现某个实例qps倾斜,说明可能存在热key。
优点:不需要额外开发。
缺点:每次发生状况需要人工排查,因为热key只是导致qps倾斜的一种可能。
redis 4.0版本之后热点key发现功能。执行redis-cli时加上 –-hotkeys 选项即可。
优点:不需要额外开发。
缺点:该参数在执行的时候,如果key比较多,执行耗时会非常长,由此导致查询结果的实时性并不好。
redis客户端使用TCP协议与服务端进行交互。通过脚本监听端口,解析网络包并进行分析。
优点:对原有的业务系统没有改造。
缺点:开发成本高,维护困难,有丢包可能性。
常用的处理方法
如果对所有热key进行本地缓存,那么本地缓存是否会过大,从而影响应用程序本身的性能开销。
可能需要保证本地缓存和redis数据的一致性。
热key统计可以使用LFU数据结构并结合上面的发现方法,将最热topN的key进行统计,然后在client端使用本地缓存,从而降低redis集群对热key的访问量,但这种方法带来两个问题:
将热key加上前缀或者后缀,把热key的数量从1个变成实例个数,利用分片特性将这n个key分散在不同节点上,这样就可以在访问的时候,采用客户端负载均衡的方式,随机选择一个key进行访问,将访问压力分散到不同的实例中。这个方案有个明显的缺点,就是缓存的维护成本大:假如有n为100,则更新或者删除key的时候需要操作100个key。
利用读写分离,通过主从复制的方式,增加slave节点来实现读请求的负载均衡。这个方案明显的缺点就是使用机器硬抗热key的数据,资源耗费严重;而且引入读写分离架构,增加节点数量,都会增加系统的复杂度降低稳定性。
大Key问题
什么是大key?
大key是指当redis的字符串类型占用内存过大或非字符串类型元素数量过多。
生产环境中,综合衡量运维和环境的情况,给大key定义参考值如下:
string类型的key超过10KB
hash/set/zset/list等数据结构中元素个数大于5k/整体占用内存大于10MB
不同系统性能条件不同,所以建议这个标准设置保守些,以系统稳定性为第一考量
可能造成的问题
内存使用不均匀。例如在redis集群模式中,某个数据分片的内存使用率远超其他数据分片,无法使数据分片的内存资源达到均衡。另外也可能造成redis内存达到 maxmemory 参数定义的上限导致重要的Key被逐出,甚至引发内存溢出。
响应时间上升、超时阻塞。由于redis是单线程架构,操作大key耗时较长,有可能造成redis阻塞。
过期时可能阻塞。大key设定了过期时间,当过期时这个key会被删除。假如redis版本低于4.0没有非同步删除机制,就会存在阻塞redis的可能性,并且慢查询查不到;同样,内存不足时的key驱逐或者是rename一个大key也会阻塞redis服务。长时间阻塞主库,可能会引发同步中断或主从切换。
慢查询为什么查不到。举例,如果请求进来且redis服务器正在进行过期键扫描,需要等待100毫秒。当客户端设置的超时时间小于100毫秒,那就会导致连接因为超时而关闭,就会造成异常,这些现象并不能从慢查询日志中查询到(因为慢查询只记录逻辑处理过程,不包括等待时间)。
网络拥塞。例如:一个大key占用空间是1MB,每秒访问1000次,就有1000MB的流量,可能造成机器或局域网的带宽被打满,同时波及其他服务。
发现方法
使用工具定期扫描,并建立好监控和通知机制。
优点:不阻塞服务
缺点:信息较少(只有各类型最大的key信息),内容不够精确(例如hash/list/set/zset都是以元素个数衡量大key,但实际上元素个数多不代表占用内存大)。
redis-cli --bigkeys 命令。可以用来找到某个实例5种数据类型(string、hash、list、set、zset)最大的key。
redis-rdb-tools 工具。redis实例上执行bgsave,然后对dump出来的rdb文件进行分析。
优点:获取信息更详细
缺点:需要离线操作,获取结果时间较长
Redis4.0之后,新增 memory usage 命令,通过随机抽样field的方式估算key的大小(样本越大,循环次数越多,计算结果越精确,性能消耗也越多)。编写python脚本,利用 scan 和 memory usage 命令,可以在集群低峰的时候扫描redis,排查大key。
优点:获取信息较准确且及时
缺点:python脚本需要注意不能影响线上正常服务,设置好监控和熔断。
常用的处理方法
大key非热key,如果不是必要的信息,可以直接删除del或者unlink都可以。
如果是redis4.0之前的版本,建议对于key使用(scan/sscan/hscan/zscan),将大key逐步删除(ltrim/zremrangebyscore/hdel/srem)。redis4.0之后,直接使用unlink替换del,会有后台线程将大key异步删除。
业务拆分,将key的含义更细粒度化,避免大key出现。
数据结构上拆分。如果大key是个大json,可以通过mset的方式,将这个key的内容打散到各个实例中,减小大key对数据量倾斜的影响;如果是大list,可以拆成 list_1,list_2,list_N ;其他数据结构同理。(可以考虑增加单独key存储大key被拆分的个数或元数据信息)
在redis没有开启非同步删除机制的场景下,设置过期时间时,一定要避免大批量键同时过期的现象,所以如果有这种情况,最好给过期时间加个随机范围,缓解大量键同时过期,造成客户端等待超时的现象。
对于长文本,更建议使用文档型数据库例如MongoDB等。
对一致性要求不高的场景,尝试使用客户端缓存。(只解决了redis的阻塞问题,但机器或局域网的带宽问题没有改善)
对大key的压缩。相当于用cpu资源来降低网络io,其中google提出的snappy算法较常用。
对于hash等数据结构,需要注意业务是否可以引入定期清理无效field的机制。