多大叫大key
超过10kb
什么是大 key
所谓的大 key 问题是某个 key 的 value 比较大,所以本质上是大 value 问题。
“key 往往是程序可以自行设置的,value 往往不受程序控制,因此可能导致 value 很大。
设想一种场景:
“ 在线音乐 app 中,某个歌单有很多用户收藏,假如有这样的数据结构:
- 歌单和用户之间的映射关系采用 redis 存储
- redis 的 key 是歌单 ID,长度可控且很小
- redis 的 value 是个 list,list 包含了用户 ID
- 用户可能很多,就导致 list 长度不可控
redis 中有常见的几种数据结构,每种结构对大 key 的定义不同,比如:
- value 是 String 类型时,size 超过 10KB
- value 是 ZSET、Hash、List、Set 等集合类型时,它的成员数量超过 1w 个
上述的定义并不绝对,主要是根据 value 的成员数量和字节数来确定,业务可以根据自己的场景也确定标准。
大 key 有什么影响
redis 核心工作线程是单线程,单线程中请求任务的处理是串行的,前面完不成,后面处理不了,同时也导致分布式架构中内存数据和 CPU 的不平衡。
- 执行大 key 命令的客户端本身,耗时明显增加,甚至超时
- 执行大 key 相关读取或者删除操作时,会严重占用带宽和 CPU,影响其他客户端
- 大 key 本身的存储带来分布式系统中分片数据不平衡,CPU 使用率也不平衡
- 大 key 有时候也是热 key,读取操作频繁,影响面会很大
- 执行大 key 删除时,在低版本 redis 中可能阻塞线程
这样看来大 key 的影响还是很明显的,最典型的就是阻塞线程,并发量下降,导致客户端超时,服务端业务成功率下降。
造成这些大 key 慢查询的原因很多。如果这些大 key 占总体数据的比例很小,存 Mc,对应的 slab 较少,导致很容易被频繁剔除,DB 反复加载,从而导致查询较慢。如果业务中这种大 key 很多,而这种 key 被大量访问,缓存组件的网卡、带宽很容易被打满,也会导致较多的大 key 慢查询。另外,如果大 key 缓存的字段较多,每个字段的变更都会引发对这个缓存数据的变更,同时这些 key 也会被频繁地读取,读写相互影响,也会导致慢查现象。最后,大 key 一旦被缓存淘汰,DB 加载可能需要花费很多时间,这也会导致大 key 查询慢的问题。
大 key 是如何产生的
大 key 的产生往往是业务方设计不合理,没有预见 vaule 的动态增长问题:
- 一直往 value 塞数据,没有删除机制,迟早要爆炸
- 数据没有合理做分片,将大 key 变成小 key
如何找到大 key
- 增加内存 & 流量 & 超时等指标监控
由于大 key 的 value 很大,执行读取时可能阻塞线程,这样 Redis 整体的 qps 会下降,并且客户端超时会增加,网络带宽会上涨,配置这些报警可以让我们发现大 key 的存在。
- bigkeys 命令
使用 bigkeys 命令以遍历的方式分析 Redis 实例中的所有 Key,并返回整体统计信息与每个数据类型中 Top1 的大 Key
- redis-rdb-tools
使用 redis-rdb-tools 离线分析工具来扫描 RDB 持久化文件,虽然实时性略差,但是完全离线对性能无影响。
redis-rdb-tools 是由 Python 写的用来分析 Redis 的 rdb 快照文件用的工具,它可以把 rdb 快照文件生成 json 文件或者生成报表用来分析 Redis 的使用详情。
- 集成化可视化工具
基于某些公有云或者公司内部架构的 redis 一般都会有可视化的页面和分析工具,来帮助我们定位大 key,当然页面底层也可能是基于 bigkeys 或者 rdb 文件离线分析的结果。
如何解决大 key 问题
根据大 key 的实际用途可以分为两种情况:可删除和不可删除。
删除大 key
如果发现某些大 key 并非热 key 就可以在 DB 中查询使用,则可以在 Redis 中删掉:
- 当 Redis 版本大于 4.0 时,可使用 UNLINK 命令安全地删除大 Key,该命令能够以非阻塞的方式,逐步地清理传入的 Key。
Redis UNLINK 命令类似与 DEL 命令,表示删除指定的 key,如果指定 key 不存在,命令则忽略。
UNLINK 命令不同与 DEL 命令在于它是异步执行的,因此它不会阻塞。
UNLINK 命令是非阻塞删除,非阻塞删除简言之,就是将删除操作放到另外一个线程去处理。
- 当 Redis 版本小于 4.0 时,避免使用阻塞式命令 KEYS,而是建议通过 SCAN 命令执行增量迭代扫描 key,然后判断进行删除。
Redis Scan 命令用于迭代数据库中的数据库键。
SCAN 命令是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。
压缩和拆分 key
- 当 vaule 是 string 时,比较难拆分,则使用序列化、压缩算法将 key 的大小控制在合理范围内,但是序列化和反序列化都会带来更多时间上的消耗。
- 当 value 是 string,压缩之后仍然是大 key,则需要进行拆分,一个大 key 分为不同的部分,记录每个部分的 key,使用 multiget 等操作实现事务读取。
- 当 value 是 list/set 等集合类型时,根据预估的数据规模来进行分片,不同的元素计算后分到不同的片。
版本2
业务场景
大 key 的业务场景也比较常见。比如互联网系统中需要保存用户最新 1万 个粉丝的业务,比如一个用户个人信息缓存,包括基本资料、关系图谱计数、发 feed 统计等。微博的 feed 内容缓存也很容易出现,一般用户微博在 140 字以内,但很多用户也会发表 1千 字甚至更长的微博内容,这些长微博也就成了大 key,如下图。
解决方案
对于大 key,给出 3 种解决方案。
- 第一种方案,如果数据存在 Mc 中,可以设计一个缓存阀值,当 value 的长度超过阀值,则对内容启用压缩,让 KV 尽量保持小的 size,其次评估大 key 所占的比例,在 Mc 启动之初,就立即预写足够数据的大 key,让 Mc 预先分配足够多的 trunk size 较大的 slab。确保后面系统运行时,大 key 有足够的空间来进行缓存。
- 第二种方案,如果数据存在 Redis 中,比如业务数据存 set 格式,大 key 对应的 set 结构有几千几万个元素,这种写入 Redis 时会消耗很长的时间,导致 Redis 卡顿。此时,可以扩展新的数据结构,同时让 client 在这些大 key 写缓存之前,进行序列化构建,然后通过 restore 一次性写入,如下图所示。
- 第三种方案时,如下图所示,将大 key 分拆为多个 key,尽量减少大 key 的存在。同时由于大 key 一旦穿透到 DB,加载耗时很大,所以可以对这些大 key 进行特殊照顾,比如设置较长的过期时间,比如缓存内部在淘汰 key 时,同等条件下,尽量不淘汰这些大 key。