一、需求背景
1.1背景
项目历史悠久,迭代多年,经过众多同事接手。对于缓存的使用存在许多不合理之处,导致了许多keys冗余:
编码层面:序列化方式不统一、数据结构使用不合理造成存储空间冗余、用户数据膨胀导致数据堆积产生的大key、没有设置过期时间的key等等。
业务层面:历史需求、运营活动等,存在一批已经停止使用但一直存储在实例中的key。
1.2优化的原因
1.2.1昂贵的费用
目前项目采用了128G的集群,八台实例,平均内存占用70-80G,大概2亿个key。Redis的费用非常昂贵,贴一张阿里云相近规格收费的图。
1.2.2大key存在的隐患
大key会带来的问题如下:
1、集群模式在slot分片均匀情况下,会出现数据和查询倾斜情况,部分有大key的Redis节点占用内存多,QPS高。
2、大key相关的删除或者自动过期时,会出现qps突降或者突升的情况,极端情况下,会造成主从复制异常,Redis服务阻塞无法响应请求。
基于以上原因,需要对项目的缓存进行优化。
二、优化思路
2.1分析key的使用情况
谈到缩容,最直接的方法肯定是分析出占用内存最大的key,对其进行优化。从此处开始,文章中提到的大key不单指单个大key,类似几百万数据的hash集合;也包含了一类key的占用了较大的空间,例如实例存在1000万个 user:info:{userid} 的string类型的key,我们也将它称为大key。
2.1.1key的采集与分析
列举分析常用的样本采集方案
方案 | 优缺点 | 是否选用 |
---|---|---|
bigkeys命令 | 优点:使用简单 缺点:耗时较长,只能统计五种基础数据,无法对一类key名称进行匹配,只能找出单个大key | 否 |
在单台实例执行bgSave,dump下来对应的RDB文件,使用对应的分析工具进行分析。我们使用的是rdb_bigkeys | 优点:对业务不造成影响,本地数据,随时可以进行分析 缺点:时效性较弱 | 是 |
扫描脚本,使用scan命令配合扫描整台实例的keys | 优点:支持任意数据类型,时效性高 缺点:虽然scan命令不阻塞主线程,但是大范围扫描keys还是会加重实例的IO,可以闲时再执行。 | 否 |
分析keys的使用,对于时效性要求肯定是不高的,所以选用了方案2,分析RDB文件,并定位key的影响范围。截取部分分析结果图:
2.1.2优化方案
分析出了大key后,就可以按keys的占用空间来排序,从前往后按优先级归纳需要优化的keys。项目中keys存在的问题在最开始已经总结过。
对于不合理的keys,我们分为两类:
- 需要优化改进的keys
- 可以直接删除的keys
一、需要优化改进的keys
这就需要针对业务场景做针对性处理了,只能给出一些典型例子的建议:
选择合理的过期时间,结合业务能短尽量短。
-
选择合适的数据结构:例如我们现在设计一个key用于存储一些信息。假定key和value的大小用了16个字节,但是由于RedisObject32b(元数据 8b+SDS指针8b+16b key value数据)、dictEntry 32b(三个指针24b,jellmoc分配出32b)。导致一个string的key需要存64b,冗余了很多数据。
优化方式:将每个Key取hash code,然后按照业务对指定的业务分成n片,用hash code对n取模。此时我们使用hset info:{hash code%n} {key} {value}存储,通过修改hash-max-ziplist-entries(用压缩列表保存时哈希集合中的最大元素个数)和hash-max-ziplist-value(用压缩列表保存时哈希集合中单个元素的最大长度)来控制hash使用ziplist存储,节省空间。 这样可以节省dictEntry开销,剩下32b的空间。
具体详情可以阅读 Redis核心技术实战 的第11章,讲得非常清晰。
使用一定的序列化方式压缩存储空间,例如protobuf。
二、可以直接删除的keys
对于可以直接删除的keys,建议使用scan命令+unlink命令删除,避免主线程阻塞。我们采用的删除方式是:scan轮流扫描各个实例,匹配需要删除的Keys。
风险点:
大家都知道对于客户端命令的执行,Redis是单线程处理的,所以存在一些阻塞主线程的操作风险:
-
对于一个占用内存非常大的key,不可直接使用del命令。
解决方案:Redis4.0以上支持unlik命令异步删除。如果Redis版本小于4.0,建议对集合类采用hscan、zscan等命令分段删除。直接删除耗时可以参考:
-
Redis具有定时清理过期keys的策略,若有大批key在同一时间过期,会导致每次采样过期的keys都超过采样数量的25%,循环进行删除操作,影响主线程性能。可参考:定期删除策略
解决方案:避免对大量key使用expiret加指定过期时间,需要将过期时间+随机数打散过期时机;由于我们的集群使用5.0版本,定位到定时清除过期Keys的代码serverCron()->databasesCron()->activeExprireCycle()→dbSyncDelete() ,内部删除方式采用unlink异步删除,不影响主线程。
-
评估删除了keys对业务的影响
解决方案:需要结合业务进行风险点评估;做好数据监控,例如异常告警、MySQL的QPS等。
三、优化了许多keys,内存占用还是很高,怎么回事?
Redis日常的使用是会存在内存碎片的,可在客户端执行info memory命令。如果mem_fragmentation_ratio值大于1.5,那么说明内存碎片超过50%,需要进行碎片整理了
解决方案:
重启Redis实例,暴力解决,不推荐
使用 config set activedefrag yes 命令,调整以下参数进行自动清理
三、优化成果
删除掉一批弃用的keys后,Redis内存占用已经减少了10G。代码上的优化由于业务需求,有些key零散地设置了2-3个月的过期时间,两三个月过后刚好又迎来了淡季,无法准确预估优化了多少空间,但是总归还是有一定的成果。过程的思路和方案仅供参考。