Redis存储的现状
Redis 是一个开源的,支持网络的,基于内存的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如字符串(strings)、散列(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。平时我们使用最多的命令应该是GET和SET,操作最多的结构类型应该是字符串类型了。
对于大多数的项目,应用程序产生的数据量并不是很多,使用字符串类型足以解决应用的性能瓶颈 或 临时性数据存储的需求。然而,对于海量数据的需求场景,使用字符串数据类型进行存储,会极大的占用redis的存储空间,导致运维成本增高,且当发生扩容、备份、灾备转移等操作时,会导致这类操作时间极为漫长和集群不稳定的情况发生。
今天逛技术网站时,看到一篇关于Redis存储压缩方案的文章描述,觉得很实用也很贴切,感谢作者的分享,特此学习和记录一下,参考链接:信也项目实战-Redis存储压缩方案 。
先来看一下,优化Redis存储之前后项目的情况:
- 项目初期调研时预估只需要存储大约6亿条目的数据量,当时的测算是300G左右的容量是足够的;
- 当项目上线半个月后,发现当初预估的数据量级与实际数据量级有严重偏差,实际的数据量到后续已增长到50亿+,且每天保持3000W左右的增长量,Redis集群从最初的200G扩容到了1.5T;
- 随之而来的是项目成本飙升,Redis集群自动备份失败,因为需要备份的数据量级过大导致拉取超时,扩容时间超长,超过3小时+,同时还容易偶发长查询等问题。
这些问题就是制定Redis大数据量级存储压缩方案的原因了。
Redis存储压缩方案
方案对比如下:
- 原存储方案:key(strings数据结构),value(多字段,采用json序列化);
- 新存储方案:key(hashs数据结构,key-feild —— ziplist编码方式),value(采用protobuf序列化),节省存储空间:80%+;
画张图来分析下两个方案的思路:
优化分析:
strings类型中,key使用int编码的话,一个字符串占用8个字节,而embstr编码要占用32个字节,所以实际中int编码方式要比embstr编码节省75%的内存空间,当然这里不是重点,接下来分析hash类型。hash类型中,配置文件默认了ziplist编码的一些参数(hash-max-ziplist-entries=512; hash-max-ziplist-value=64;):有key的最大数量(即field-value域值对的数量不超过512),其中value的字符串长度不超过64个字节,否则ziplist编码将转换为hashtable编码方式存储。因此,实际使用中要预估好key的数量和内层field-value域值对的value长度。
这里,我们把key称为bucket,使用ziplist编码方式时,怎么计算需要bucket的数量呢?假如要往Redis存储100W条数据,100W/512 = 1953,即至少要有bucket。然而,如果所有的key要将进行哈希计算,尽量保证key均摊到所有bucket里。但由于哈希函数的不确定性,未必能完全平均分配,以及还要考虑未来数据的增长量,所以我们要预留出足够的空间,比如这里分配3000个bucket。
那如何通过选定哈希算法,将key放到bucket里呢?这里,可以采用高效而且均衡的知名算法crc32,该哈希算法可以将一个字符串变成一个long型的数字,通过获取这个MD5型的key的crc32后,再对bucket的数量进行取余,就可以确定该key要被放到哪个bucket中。
在确定了key的哈希算法后,对于内层的field,我们则选用另一个hash算法,这样可以避免field和key出现hash碰撞而导致覆盖的情况发生。field选用bkdr哈希算法 或直接通过Java的hashCode计算,结果也会得到一个long整型的数字,这里value暂时不做优化。至此,替换方案大致完成了,看下效果:
- 原存储方案:key-value : 2c0c903b2df742be86f8264c13780225 → 2c0c903b2df742be86f8264c13780225;
- 新存储方案:key-field-value :key为778,field是17276529,value是2c0c903b2df742be86f8264c13780225;
经过实测,采用字符串key-value方式直接存储,100W条数据占用了大概146MB的存储空间;而换成hash+ziplist存储方式,100W条数据占用了大概47MB的存储空间,大约减少了68%的存储空间。另外,将100W数据存入3000个bucket后,整体hash比较均衡,每个bucket下大概分布了300多个field-value域值对,目前一个key内的元素数量还只有339个,没有超过配置值hash-max-ziplist-entries=512。此时,我们可以尝试再多写入100W的数据,因为200W/512=3906,它已经超过了当时我们设计的3000个bucket,所以单个bucket中的元素个数已经超过了默认配置hash-max-ziplist-entries=512,Redis会自动改为hashtable编码,不再使用ziplist编码了。如果按ziplist编码存储100W数据占用47MB内存空间来计算,理论上200W的数据量只会占用不到100MB的内存空间,但编码被改成hashtable后,经测试内存占用却增长到了256MB,进一步实际证明了hashtable编码存储比ziplist编码存储会占用更多的存储空间。
采用hash的重点在于,理论上只要不发生两次hash算法后,均产生相同的值,那么就可以完全依靠key-field来找到原始的value,这一点可以通过计算总量进行确认。实际上,在bucket数量较多时,且每个bucket下,value数量不是很多,发生连续碰撞概率极低,实测在存储50亿情况下,未发生明显碰撞。但我们在经过110亿+的数据验证后,发现有6个碰撞情况产生,所以对于数据量级极大且精度要求极高的场景,这一点需要注意!!
通过改变数据结构类型和内部编码方式优化了key 和 field, 接下来,该是考虑对大字符串的value做优化了。实际中,存储的value是个多字段属性的对象,为了使用方便,设计初期直接采用了json序列化成字符串进行了存储,这样会额外浪费较多的存储空间,该如何对value也进行存储优化呢?
具体的优化方式有:压缩 或 序列化,通过一系列的调研和比对,选用了Google的protobuf序列化进行value处理,可以达到极致性能和节省大量空间的目的。同一个对象经过不同序列化方式生成字符串后的效果:
- 原存储方案:JSON序列化,占用约178字节;
- 新存储方案:protobuf序列化,只占用了约53字节;
经过对比可知:protobuf序列化节省了约70%的存储空间,而采用hash数据结构时,配置文件默认value不能超过64个字节,否则会将ziplist编码方式转换为hashtable编码,而protobuf序列化没有超过,刚好匹配了ziplist编码。即使以后还需要在此对象上扩展字段,而value可能会超过64字节,我们也可以去调整hash-max-ziplist-value的配置改到128字节,即使再多扩展几个字段,相信也不会超过此配置值的。值得注意的是,像hash-max-ziplist-value、hash-max-ziplist-entries等参数配置,不宜改得太大,这样对cpu的压力会更大,对查询时的性能影响肯定更大,需要在存储空间和性能上取其平衡。
对于压缩算法,在对象较小的场景下,压缩算法的压缩比不高,且性能上也有一定的损耗;如果在有较大字符串的场景下,推荐使用google的snappy压缩算法,经过实测后发现此压缩算法在性能、压缩比、资源消耗等上有明显优势。以下是作者调研的压缩算法实际情况,可以在实际项目中参考使用:
方案缺点:
- hash结构不支持对单个field的超时设置,但可以通过代码来控制删除,对于那些不需要超时的长期存放的数据,则没有这种顾虑;
- 还是会存在较小的hash冲突概率,对于数据量极大且要求极其精确的场合,应谨慎采用该压缩方案。
方案整体效果:
- 字符串key-value存储方案的redis集群情况:50亿数据时,redis集群占用了1.2T的内存;
- hash+ziplist+protobuf存储方案的redis集群情况:使用ziplist的方式存储1亿的数据预估仅使用了1G的内存空间,即使是需要存储50亿的数据,100G的内存也是足够的,比当时的1.2T节省约90%的存储空间。
整个集群从1.5T降到了100G,且目前仅使用了30G不到,效果还是很显著的!!!