以redis一组K-V为例(”hello”-“world”),一个简单的set命令最终会产生4个消耗内存的结构。
关于Redis数据存储的细节,又要涉及到内存分配器(如jemalloc),简单说就是存储字节,其实内存分配器会分配字节存储。
那么总的花费就是
一个dictEntry,24字节,jemalloc会分配32字节的内存块
一个redisObject,16字节,jemalloc会分配16字节的内存块
一个key,5字节,所以SDS(key)需要5+9=14个字节,jemalloc会分配16字节的内存块
一个value,5字节,所以SDS(value)需要5+9=14个字节,jemalloc会分配16字节的内存块综上,一个dictEntry需要32+16+16+16=80个字节。
上面这个算法只是举个例子,想要更深入计算出redis所有数据结构的内存大小,可以参考这篇文章。笔者使用的是哈希结构,这个业务需求大概一年的数据量是MB,从使用redis成本上考虑没有问题。需求特点笔者这个需求背景读多写少,冷数据占比比较大,但数据结构又很复杂(涉及多个维度数据总和),因此只要启动定时任务离线增量写入redis,请求到达时直接读取redis中的数据,无疑可以减少响应时间。
[最终方案]redis瓶颈和优化HGETALL最终存储到redis中的数据结构如下图。
采用同步的方式对三个月(90天)进行HGETALL操作,每一天花费30ms,90次就是ms!redis操作读取应该是ns级别的,怎么会这么慢?利用多核cpu计算会不会更快?
常识告诉我,redis指令执行速度网络通信(内网)read/write等系统调用。因此这里其实是I/O密集型场景,就算利用多核cpu,也解决不到根本的问题,最终影响redis性能,**其实是网卡收发数据和用户态内核态数据拷贝**。pipeline这个需求qps很小,所以网卡也不是瓶颈了,想要把需求优化到1s以内,减少I/O的次数是关键。换句话说,充分利用带宽,增大系统吞吐量。于是我把代码改了一版,原来是90次I/O,现在通过redispipeline操作,一次请求半个月,那么3个月就是6次I/O。很开心,时间一下子少了ms。
pipeline携带的命令数代码写到这里,我不经反问自己,为什么一次pipeline携带15个HGETALL命令,不是30个,不是40个?换句话说,一次pipeline携带多少个HGETALL命令才会发起一次I/O?我使用是golang的redisgo的客户端,翻阅源码发现,redisgo执行pipeline逻辑是把命令和参数写到golang原生的bufio中,如果超过bufio默认最大值(字节),就发起一次I/O,flush到内核态。
redisgo编码pipeline规则如下图,*表示后面参数加命令的个数,$表示后面的字符长度,一条HGEALL命令实际占45字节。那其实90天数据,一次I/O就可以搞定了(90*45字节)!
果然,又快了ms,耗费时间达到了1秒以内
对吞吐量和qps的取舍笔者需求任务算是完成了,可是再进一步思考,redis的pipeline一次性带上多少HGETALL操作的key才是合理的呢?换句话说,服务器吞吐量大了,可能就会导致qps急剧下降(网卡大量收发数据和redis内部协议解析,redis命令排队堆积,从而导致的缓慢),而想要qps高,服务器吞吐量可能就要降下来,无法很好的利用带宽。对两者之间的取舍,同样是不能拍脑袋决定的,用压测数据说话!简单写了一个压测程序,通过比较请求量和qps的关系,来看一下吞吐量和qps的变化,从而选择一个适合业务需求的值。packagemainimport("crypto/rand""fmt""math/big""strconv""time""github.