【Redis】Redis大key的危害及解决方案分享
文章目录
- 一、背景
- 二、什么是大key
- 三、大key评价标准
- 四、大key 产生的原因与场景
- 五、大key影响与危害
- 六、大key检查与发现
- 6.1 使用 --bigkeys参数
- 6.2 使用scan命令
- 6.3 使用 memory 命令查看 key 的大小
- 6.4 使用 Rdbtools 工具包
- 6.5 代码埋点
- 6.6 公有云的Redis分析服务
- 七、大key解决方式
- 7.1 增加监控告警
- 7.2 数据结构优化
- 7.3 清理过期数据
- 7.4 压缩数据
- 7.5 拆分存储
- 7.5.1 按时间/业务拆分
- 7.5.2 按哈希拆分
- 八、总结
一、背景
Redis作为后端开发中的一个常用组件,在开发过程中承担着非常重要的作用。在其实际使用过程中,我们常常会面临一些技术挑战,其中常见的问题就包括大key问题。当某些数据量较大的键值对(如富文本等)存储在Redis中时,这些大key的读写操作会明显影响系统的性能,严重时会导致系统出现卡顿的问题。因此,Redis大key的监控与治理是互联网开发中使用Redis时必须面对的一个课题,本文将按照redis大key的定义与评判标准、危害与解决方式的顺序来做个总结。
二、什么是大key
通常来说,Big Key指的就是某个key对应的value很大,占用的Redis空间很大,是value过大的问题。
这是很多文章中会提到的一点, 除此之外,这里想强调的是Redis的key的大小同样也会对性能存在隐患
,而为什么我们会比较少去讨论这一点呢,那是因为key是我们人为直接设置的,其相对来说更可控一点,但是它也同样不可忽视。其实我们从存储结构就不难分析得到上述结论:
根结构为RedisServer,其中包含RedisDB(数据库)。而RedisDB实际上是使用Dict(字典)结构对Redis中的kv进行存储的。这里的key即字符串,value可以是string/hash/list/set/zset这五种对象之一。
Dict字典结构中,存储数据的主题为DictHt,即哈希表。而哈希表本质上是一个DictEntry(哈希表节点)的数组,并且使用链表法解决哈希冲突问题(关于哈希冲突的解决方法可以参考大佬的文章 解决哈希冲突的常用方法分析)。
所以在实际存储时,key和value都是存储在DictEntry中的。所以基本上来说,大key和大value带来的内存不均和网络IO压力都是一致的,只是key相较于value还多一个做hashcode和比较的过程(链表中进行遍历比较key),会有更多的内存相关开销。
通过redis的key和value存储来看,其大小带来的影响基本一致,都会给系统性能带来不小的影响。
三、大key评价标准
一般认为string类型控制在10KB以内,hash、list、set、zset元素个数不要超过10000个。
但在实际业务中,大Key的判定仍然需要根据Redis的实际使用场景、业务场景来进行综合判断,通常都会以数据大小与成员数量来判定。在不同互联网公司中,其对Redis大Key的定义标准也略有差异,例如已知业内公司的评价标准如下:
- 阿里标准:字符推荐小于10KB,但是他们内网建议是小于1KB,集合个数推荐低于1000;
- 美团标准:字符推荐不能超过512KB,集合个数不能超过1W个,单个不能超于1M;
- 携程标准:字符类型大小控制在10KB以内,hash/list/set/zset等包含元素个数建议控制在1000以内,单个集合大小没有限制。
四、大key 产生的原因与场景
大 key 通常是由于下面这些原因产生的:
redis数据结构使用不恰当
- 将Redis用在并不适合其能力的场景,造成key的value过大,如使用String类型的key存放大体积二进制文件型数据(富文本类型数据)
未及时清理垃圾数据
- 没有对无效数据进行定期清理,造成如Hash类型key中的成员持续不断的增加
对业务预估不准确
- 业务上线前规划设计考虑不足没有对key中的成员进行合理的拆分,造成个别key中的成员数量过多
明星、网红的粉丝列表、某条热点新闻的评论列表
- 用List数据结构保存热点新闻的评论列表,因为粉丝数量巨大,热点新闻因为点击率、评论数会很多,这样List集合中存放的元素就会很多,可能导致value过大,进而产生Big Key问题。
引发大Key问题的几个常见场景如下:
队列型应用
- 将Redis用作队列处理任务,如果队列消费速度跟不上生产速度,队列会越来越大,最终形成"大key"
统计型应用
- 按天存储某项功能或网站的用户集合信息,如果用户量较大,这类"统计型"数据容易形成"大key"
缓存型应用
- 业务上线前规划设计考虑不足没有对key中的成员进行合理的拆分,造成个别Key中的成员数量过多
五、大key影响与危害
性能下降
- 大key的读写操作可能会消耗更多的CPU和内存资源,导致redis的响应时间变慢
内存使用不均衡
- 在集群环境中,大key可能导致某些数据分片的内存使用率远高于其他分片,造成内存资源分配不均匀
带宽占用
- 如果大key被频繁访问,可能会占用大量的网络带宽,影响其他服务的性能
阻塞问题
- 由于redis是单线程的,操作大key可能会造成redis服务阻塞,尤其是在执行清除操作或者过期时间到达时
内存溢出
- 如果redis实例的内存达到maxmemory参数定义的上限,大key的存在可能导致操作阻塞或者重要key被逐出,甚至引发内存溢出
缓存穿透
- 大key的频繁清理可能导致缓存穿透问题,影响缓存效率
数据倾斜
- 在集群架构下,大key可能导致访问倾斜,即扣个数据分片被大量访问,而其他分片处于空闲状态,可能会引起链接数耗尽问题
缓存击穿
- 热key的请求压力超出redis的承受能力,可能导致缓存击穿,大量请求直接指向后端存储层,造成存储层访问量激增
六、大key检查与发现
6.1 使用 --bigkeys参数
–bigkeys 是 redis 自带的命令,对整个 Key 进行扫描,统计 string,list,set,zset,hash 这几个常见数据类型中每种类型里的最大的 key。String 类型统计的是 value 的字节数,另外 4 种复杂结构的类型统计的是元素个数,不能直观的看出 value 占用字节数,所以 --bigkeys 对分析 string 类型的大 key 是有用的,而复杂结构的类型还需要一些第三方工具(注:元素个数少,不一定 value 不大;元素个数多,也不一定 value 就大)。
–bigkeys 是以 scan 延迟计算的方式扫描所有 key,因此执行过程中不会阻塞 redis,但实例存在大量的 keys 时,命令执行的时间会很长,这种情况建议在 slave 上扫描。
–bigkeys 其实就是找出类型中最大的 key,最大的 key 不一定是大 key,最大的 key 都不超过 10kb 的话,说明不存在大 key。但某种类型如果存在较多的大key (>10kb),只会统计 top1 的那个 key,如果要统计所有大于 10kb 的 key,需要用第三方工具扫描 rdb 持久化文件。
redis-cli -h 127.0.0.1 -p 6379 -a "password" --bigkeys
6.2 使用scan命令
使用 Redis 自带的 scan命令,这个命令会以遍历的方式分析 Redis 实例中的所有 key,并返回整体统计信息,结合其他命令(如List 类型:LLEN
命令;Hash 类型:HLEN
命令;Set 类型:SCARD
命令;Sorted Set 类型:ZCARD
命令)我们可以识别出大Key。scan命令的优势在于它可以在不阻塞Redis实例的情况下进行遍历。
redis-cli --scan --pattern '*' --count 1000
除此之外,其实也可以通过脚本等定期主动扫描Redis键值,当键值超过阈值则记录为BigKey。
import redis
import time
# Redis连接
redis_client = redis.Redis(host='localhost', port=6379)
# 大键阈值 - 100MB
BIG_KEY_THRESHOLD = 100 * 1024 * 1024
# 扫描间隔 - 1小时
SCAN_INTERVAL = 3600
def scan_big_keys():
cursor = '0'
big_keys = []
while cursor != 0:
cursor, keys = redis_client.scan(cursor=cursor, count=100)
for key in keys:
size = redis_client.debug_object(key)['serializedlength']
if size > BIG_KEY_THRESHOLD:
big_keys.append({'key': key, 'size': size})
return big_keys
while True:
big_keys = scan_big_keys()
if big_keys:
print(f'Found {len(big_keys)} big keys')
for bk in big_keys:
print(f' {bk["key"]} {bk["size"]}')
time.sleep(SCAN_INTERVAL)
主要步骤包括:
1.使用SCAN命令渐进式扫描键空间
2.调用DEBUG OBJECT命令获取键值大小
3.比较大小判断是否为大键
4.定期循环扫描这个脚本可以灵活调整大键阈值、扫描间隔等参数。
可以将它部署为持续运行的任务,自动发现Redis中的大键,但是这种方式结合了DEBUG OBJECT命令,其运行代价较大,在其运行时,进入Redis的其余请求将会被阻塞直到其执行完毕。因此,如果使用这种方式进行检测,最好让其在对用户体验影响最小的时候运行(如凌晨两三点)。
6.3 使用 memory 命令查看 key 的大小
在Redis4.0之前,只能通过DEBUG OBJECT命令估算key的内存使用(字段serializedlength),但DEBUG OBJECT命令是存在误差的。Redis4.0以后的版本中可以使用 memory 命令查看 key 的大小。如果当前key存在,则返回key的value实际使用内存估算值,如果key不存在,则返回nil。
redis-cli -h 127.0.0.1 -p 6379 -a password
MEMORY USAGE keyname1
(integer) 157481
MEMORY USAGE keyname2
(integer) 312583
MEMORY USAGE keyname3
(nil)
6.4 使用 Rdbtools 工具包
Rdbtools 是 Python写的 一个第三方开源工具,用来解析 Redis 快照文件,除了解析 rdb 文件,其还提供了统计单个 key 大小的工具。使用Rdbtools 离线分析工具来扫描RDB持久化文件,优点在于无性能损耗、获取的key信息详细、可选参数多、支持定制化需求,结果信息可选择json或csv格式,后续处理方便,但其缺点是需要离线操作,获取结果时间较长。
①安装
git clone https://github.com/sripathikrishnan/redis-rdb-tools
cd redis-rdb-tools sudo && python setup.py install
②使用
从 dump.rdb 快照文件统计, 将所有 > 10kb 的 key 输出到一个 csv 文件
rdb dump.rdb -c memory --bytes 10240 -f live_redis.csv
6.5 代码埋点
如果是代码写入Redis,例如在Java代码中将数据写入到Redis里,可以直接在代码里进行埋点统计。
6.6 公有云的Redis分析服务
基于某些公有云或者公司内部架构的Redis一般都会有可视化的页面和分析工具,来帮助我们定位大key,当然页面底层也可能是基于bigkeys或者rdb文件离线分析的结果。如阿里云社区提供了一款基于Python编写的大key定位工具:https://developer.aliyun.com/article/117042
七、大key解决方式
实际上解决大key问题的核心就是减小value的占用空间,大多数方法都集中在对存量的大key进行改造,如清理过期数据等方式,除此之外,也有通用的方案,在开始存储时就预先设计好,如压缩数据及拆分存储等方式。
7.1 增加监控告警
通过监控系统并设置合理的Redis内存报警阈值来提醒我们此时可能有大Key正在产生,如:Redis内存使用率超过70%,Redis内存1小时内增长率超过20%等。如果是应用代码里写入数据到Redis,那么做好埋点,然后加监控告警也是非常重要的,既可以对历史数据进行监控,定位大key,又可以避免新增的逻辑代码中出现大key这一隐患。因此,埋点监控是基石。
7.2 数据结构优化
优化 redis 的数据结构,使用合适的数据结构来存储数据,避免出现 redis 大 key 的情况。例如,文件二进制数据不使用 String 保存、使用 HyperLogLog 统计页面 UV、Bitmap 保存状态信息。
7.3 清理过期数据
如果某个Key有业务不断以增量方式写入大量的数据,并且忽略了其时效性,这样会导致大量的失效数据堆积。可以通过定时任务的方式,对失效数据进行清理。设置合理的过期时间。为每个key设置过期时间,并设置合理的过期时间,以便在数据失效后自动清理,避免长时间累积的大Key问题。
- Redis 4.0及之后版本:可以通过UNLINK命令安全地删除大Key甚至特大Key,该命令能够以非阻塞的方式,逐步地清理传入的Key。 Redis UNLINK 命令类似与 DEL 命令,表示删除指定的 key,如果指定 key 不存在,命令则忽略。 UNLINK 命令不同与 DEL
命令在于它是异步执行的,因此它不会阻塞。 UNLINK 命令是非阻塞删除,非阻塞删除简言之,就是将删除操作放到另外一个线程去处理。- Redis 4.0之前的版本:建议先通过SCAN命令读取部分数据,然后进行删除,避免一次性删除大量key导致Redis阻塞。 Redis Scan 命令用于迭代数据库中的数据库键。 SCAN 命令是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标,
用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。
启用内存淘汰策略。启用Redis的内存淘汰策略,例如LRU(Least Recently Used,最近最少使用),以便在内存不足时自动淘汰最近最少使用的数据,防止大Key长时间占用内存。
7.4 压缩数据
在向Redis中写数据时,可以使用序列化、压缩算法将key的大小控制在合理范围内,但是需要注意序列化、反序列化都会带来一定的消耗。
Redis 支持多种压缩算法,例如 LZF、QUICKLZ 和 GZIP 等算法。不过经过推荐和测试,本文推荐ZSTD为压缩工具,因为相比其他主流压缩算法ZSTD提供了更优秀的压缩比。另外在使用随机字符串本地循环测试后,发现处理较小规模的随机字符串时,ZSTD压缩效果并不明显,如下表格所示,对于0.1KB的数据压缩后的文件大小甚至会有所增加。这可能是由于小数据量的特点导致压缩算法无法充分发挥其优势,也有可能是该压缩算法不适用于小数据量压缩。因此,为了避免浪费无谓压缩带来的CPU损耗和耗时,可以引入阈值判断机制,只有当数据大小超过10KB时才触发ZSTD压缩操作。
压缩前(KB) | 压缩后(KB) | 压缩率 |
---|---|---|
0.01 | 0.022 | -89% |
0.1 | 0.122 | -9% |
1 | 0.784 | 21% |
10 | 7.47 | 25% |
100 | 74.6 | 25% |
1000 | 746 | 25% |
7.5 拆分存储
压缩技术确实可以有效减小value的占用空间,从而解决大部分key问题,但是如果压缩后,value还是很大,那么可以进一步对key进行拆分。key拆分的具体思路是:将原先的单个大key拆分成一个主key和多个子key,其中主key主要保存子key之间的关联信息,但不包含具体的值,而子key负责存储拆分过后的值信息。通过拆分大幅降低单个key的value大小,从而有效解决原有的大key问题。
往Redis中写的拆分方案如下图所示:
具体步骤:
首先,假设有个key 为key:user,然后需要判断key:user对应的值的size是否超过指定阈值,例如10KB,如果size小于10KB,则之间将其值写入到Redis中;如果size小大于10KB,则需要将其值按照一定方式进行分解,生成一个key和多个子key。在拆分完key之后,就需要保证主key和子key列表能够批量原子写入到Redis中,这一操作非常重要,如果不能保证其原子性就可能会出现部分key写入失败的情况,进而导致并发读取数据的不一致问题。
从Redis中读取的操作其实就是写入的逆操作,其主要就是将生成的主key和子key重新合并起来,方案如下图所示:
具体步骤:
首先,根据key:user从Redis中获取到子key列表,根据子key列表再利用mget命令从Redis中获取子key的值,然后按一定方式进行合并还原,最终校验length。
7.5.1 按时间/业务拆分
如果 Big Key 包含的是按时间顺序排列的数据,可以考虑按时间范围拆分一下,但是拆分后的每个key对应的值应该进行二次校验,确认其大小是否低于大key的阈值判断标准。
7.5.2 按哈希拆分
如果要存储的数据不是按时间顺序排列的数据,那么可以考虑哈希的方式进行拆分。而这一步首先我们需要先确定拆分的子key的数量:可以通过size(value) / 10KB计算出子key数量,然后每次存取的时候,先在本地计算field的hash值,模除 10000, 就确定了该field落在哪个key上。
八、总结
大key和大value的危害是一致的:内存不均、阻塞请求、阻塞网络等。本文主要详细介绍了大value产生的原因、影响、检测方法和解决方案。通过优化数据结构设计、压缩存储、拆分存储等方法,我们可以有效地解决和预防大key问题,从而提高Redis系统的稳定性和性能。