地址服务优化

losetowin 发布于:2017-10-4 20:20 分类:Java  有 989 人浏览,获得评论 0 条  

本文地址:http://www.dutycode.com/post-147.html
除非注明,文章均为 www.dutycode.com 原创,欢迎转载!转载请注明本文地址,谢谢。


问题:

地址服务使用Redis做数据缓存,原本的技术方案是将地址对象转化成json格式字符串进行存储,使用数据格式为String类型。 

但随着数据的增长,出现了两个问题:

1、内存占用达到4G,并成增长趋势。 超出原始估算值4G。

2、查看值分布,发现存在特别大的value值,有些超过200k。

内存增长超出预期,需要找到内存增长过快的原因; 值过大可能会导致请求过多时将网卡占满,导致无法服务,需要分析部分值过大的原因


分析和解决过程:

现有的存储数据主要在业务上包含两部分, 一部分为用户ID纬度的,会记录当前用户下的地址列表信息(json格式)。一部分为用户ID和地址ID纬度的, 用于记录当前地址ID所对应的地址信息(json格式)。

格式如下图所示:


初步优化:

1.优化缓存时间:减少缓存的周期。 

     原始数据会缓存2天的时间,将缓存时间缩减到1天, 在业务上也可以满足正常访问需求。 

     缓存时间调整后,整体内存占用有所下降,但下降幅度不大。 (猜测原因为数据天重合比例比较高)

     

2.数据存储前压缩。减少空间占用。 

     缓存时间的调整,带来了内存的占用降低,但并未降低单个value值占空间过大的问题,依旧可能会导致网卡被打满。

     所以,选择使用了Snappy的压缩算法,对数据进行压缩。 存入时压缩,取出时解压缩。 压缩算法有很多种,这里选择使用的是Snappy。原因为,Snappy在压缩速度和压缩率上都有个比较好的折中,比较符合当前的一个使用场景(可参考各种压缩算法的对比)

 

3.选择性压缩,非全部压缩。 

     启动压缩的时候,会带来两个问题, 一个是压缩和解压缩需要增加CPU使用,耗时会相应增加。另外一个是字符串长度偏小的时候,压缩后占用的空间反而会大于原有的字符串占用的空间。

     所以,需要在压缩的时候做下处理,仅压缩字符串较长的数据。 

     我们测试的情况下, 长度在300字节以上的压缩才有效果,小于300字节的压缩后反而空间占比更大。当然这个数据也和数据内容有关系。

     在地址服务的场景下,单地址ID的数据对应一般都比较小,所以不做压缩。转而,压缩的是一个用户下的地址列表。 

     

效果:

     启用压缩后的前后内存占比情况:

     7.27 4924M

     8.19 3530M

     内存减少占用 (4924-3530)/ 4924 = 28.3%

     

     大value值的占用情况对比:

压缩前:

压缩后:

200-500k的数据访问少了,转而50-100k的数据访问多了,说明大部分大的value值,被压缩到了50-100k


继续优化:

     添加压缩之后,发现服务的响应处理时间变长,通过服务管理平台看到的执行时间如下图:

     服务调用耗时基本在10ms+, 但是服务执行耗时却仅为0.5ms左右。说明,框架在消耗任务队列的时候,有部分的响应速度过慢导致整个队列的时间变长。 

     初步猜测原因为:缓存key的变更,导致大量的数据在缓存中失效,所以需要大量数据从DB中读取,然后放到缓存中,这个过程增加了耗时。所以,随着时间的推移,这种超时情况理论上应该变好。

为了快速印证这个猜测,我们把可能访问到的数据预热到了缓存中,但是情况依旧没有变好。

     于是,对比优化前和优化后的变化,仅有未压缩和压缩的区别,所以,猜测问题可能为压缩和解压缩过程耗时较长导致。

     我们在耗时超过10ms以上的数据上添加了日志打印,记录了当前的UID和value大小情况。

     发现了一个非常大的value值,长度超过了1000k,但是uid却为0。 并且比较稳定的是, 这个uid=0的查询一直在稳定的被查询到。 所以猜测,原因可能是因为这个数据的频繁访问带来的解压缩和初始化String的过程导致了框架的整体超时情况。

      uid为0的数据实际为异常的脏数据,理论上不会出现uid=0的地址服务访问,于是,对服务的数据过滤之后,去掉uid=0的访问, 服务归于正常。访问耗时变为如下情况:

整体的时间在0.5ms左右。 

     这里其实引伸出来几个项目中没有考虑到的点:

          1)、更换Key值时,必然会带来大量的key值同时失效,也就意味着会有大量的DB访问, 这本身就是一个很危险的过程,容易导致系统乃至DB出现不可用的情况,所以,类似这种key值需要变更的请款下,如何保证系统的平稳过度,保证缓存不大批量的失效?

                    我们这次操作没有带来很大的问题的一个原因是我们仅压缩了列表数据,单个地址ID的数据并未做压缩。并且,单个地址ID的访问量是最大的,所以压缩列表数据本质上并没有产生特别多的数据库访问,所以服务和DB还算稳定,没有出现宕机的情况,也算是这次的幸运。 

          2)、对外提供服务时,对数据入参的校验是很有必要的。非法数据不需要访问缓存,不需要访问DB,可减少缓存和DB之间的压力。 

               像我们这次的访问耗时过长的情况,原因在数据库中有uid=0的脏数据,并且数据量特别大。而且,uid=0的数据还会有调用方直接调用,并且频率不低,所以导致了系统做了一些无用功:查询缓存,解压缩,生成字符串。 这部分本身是有耗时的,而且数据量越大,耗时越长。

               所以在开发中,一定要关注入参,控制入参质量。  服务调用方传入的数据一定是不能保证完全可靠的,需要服务本身保证数据可靠。 

     关于Snappy的压缩时间和解压缩时间的测试, 这个我这边是使用了本机做的测试, 解压缩耗时会跟随字符串的长度而变化, 具体可以自行测试。 


另外一种优化思路:

     地址服务的场景下 ,查询一定会有uid这个条件, 所以会存在两种, 一种是uid的查询, 一种是uid和地址ID的查询。 

     那么如果是这样, redis中的HASH数据结构会比较适合地址服务的存储。 

     现在UID会存储一份数据, UID+WorkID也会存储一份,所以极端情况下uid和workid的存储会占用两份相同的空间,这时候,使用Hash数据结构可以只保存一份空间即可,最多减少1/2的内存占用。 

     同时,数据量少的时候, Redis的Hash存储,是会采用ziplist来做数据压缩的,那么存储的空间占用上会低于现有的1/2。

     Redis在存储Hash结构的数据的时候,如果满足一下两个条件,则会使用ziplist压缩数据,否则会使用hashtable的方式。 

     

  • 试图往列表新添加一个字符串值,且这个字符串的长度超过 server.list_max_ziplist_value (默认值为 64 )。

  • ziplist 包含的节点超过 server.list_max_ziplist_entries (默认值为 512 )。

     地址服务满足第二点,我们要求每个UID下的地址个数最多为200个(PS:之前提到的脏数据0除外)。所以满足ziplist节点个数的要求,但是不满足字符串长度的大小要求 。 地址数据采用json格式存储,其中有个地址描述部分,这部分不限定长度,所以整体下来的数据长度不确定是否满足64字节这个最大限制,而且,即使调整list_max_ziplist_value的大小,也很难保证cover所有的数据。

     所以,在地址服务中,HASH的数据结构,并不能保证数据会被压缩。

     另外一点, 在地址服务中需要解决的一点是,从Redis中拿数据时,不能过大,否则可能会导致网卡占用过高,这时候,Hash的数据本质上没有压缩,所以当取用户所有的数据的时候, 可能会产生较大的数据。 所以不能解决网卡可能被打满的问题。 

     对于地址服务,不太适合用Hash来做数据结构存储进行优化。 

但Hash的这种数据结构在项目中还是比较常见的,可以借助Redis本身对他的一些优化来进行一些数据的处理。 


关于这个项目优化的另外一个思路。 

未实践,仅供参考。

数据分片+Redis分片, 分治。


系统还存在什么问题?

数据虽然被压缩了,但是仍旧存在某些value值比较大的情况,比如依旧存在100k以上的数据,甚至在不久的将来可能存在压缩后依旧大于200k的数据,那么这部分数据仍然会是个隐患。

那该如何解决呢?

有一种不成熟的解决方案,未实践,仅供参考。

本质上从业务属性来讲,这种value值过大的被访问的可能性也不大,所以与其将精力放在如何减少value大小上,而不如转而放在如何控制这部分的数据的访问,换句话说,限制大value的访问频度,减少出现网卡被打满的风险。

同时,上面提到的分片的思路也能解决这种情况。


版权所有:《攀爬蜗牛》 => 《地址服务优化
本文地址:https://www.dutycode.com/post-147.html
除非注明,文章均为 《攀爬蜗牛》 原创,欢迎转载!转载请注明本文地址,谢谢。