千家信息网

redis实践及思考

发表于:2024-11-23 作者:千家信息网编辑
千家信息网最后更新 2024年11月23日,导语:当面临存储选型时是选择关系型还是非关系型数据库? 如果选择了非关系型的redis,redis常用数据类型占用内存大小如何估算的? redis的性能瓶颈又在哪里?背景前段时间接手了一个业务,响应时
千家信息网最后更新 2024年11月23日redis实践及思考

导语:当面临存储选型时是选择关系型还是非关系型数据库? 如果选择了非关系型的redis,redis常用数据类型占用内存大小如何估算的? redis的性能瓶颈又在哪里?

背景
前段时间接手了一个业务,响应时间达到 10s左右 阅读源码后发现,每一次请求都是查询多个分表数据(task1,task2….),然后再join其他表(course,teacher..), 时间全部花在了大量磁盘I/O上 脑袋一拍,重构,上redis!
为什么选择redis
拍脑袋做技术方案肯定是不行的,得用数据和逻辑说服别人才可以。

时延

时延=后端发起请求db(用户态拷贝请求到内核态)+ 网络时延 + 数据库寻址和读取
如果想要降低时延,只能减少请求数(合并多个后端请求)和减少数据库寻址和读取得时间。 从降低时延的角度,基于 单线程和内存的redis,每秒10万次得读写性能肯定远远胜过磁盘读写性能。

数据规模

以redis一组K-V为例("hello" -> "world"),一个简单的set命令最终会产生4个消耗内存的结构。

关于Redis数据存储的细节,又要涉及到内存分配器(如jemalloc),简单说就是存储170字节,其实内存分配器会分配192字节存储。

那么总的花费就是

  • 一个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所有数据结构的内存大小,可以参考 这篇文章
笔者使用的是哈希结构,这个业务需求大概一年的数据量是200MB,从使用redis成本上考虑没有问题。

需求特点

笔者这个需求背景读多写少,冷数据占比比较大,但数据结构又很复杂(涉及多个维度数据总和),因此只要启动定时任务离线增量写入redis,请求到达时直接读取redis中的数据,无疑可以减少响应时间。

[ 最终方案 ]
redis瓶颈和优化

HGETALL

最终存储到redis中的数据结构如下图。

采用同步的方式对三个月(90天)进行HGETALL操作,每一天花费30ms,90次就是2700ms! redis操作读取应该是ns级别的,怎么会这么慢? 利用多核cpu计算会不会更快?

常识告诉我,redis指令执行速度 >> 网络通信(内网) > read/write等系统调用。 因此这里其实是I/O密集型场景,就算利用多核cpu,也解决不到根本的问题,最终影响redis性能, **其实是网卡收发数据 用户态内核态数据拷贝 **

pipeline

这个需求qps很小,所以网卡也不是瓶颈了,想要把需求优化到1s以内,减少I/O的次数是关键。 换句话说, 充分利用带宽,增大系统吞吐量。

于是我把代码改了一版,原来是90次I/O,现在通过redis pipeline操作,一次请求半个月,那么3个月就是6次I/O。 很开心,时间一下子少了1000ms。

pipeline携带的命令数

代码写到这里,我不经反问自己,为什么一次pipeline携带15个HGETALL命令,不是30个,不是40个? 换句话说,一次pipeline携带多少个HGETALL命令才会发起一次I/O?

我使用是golang的 redisgo 的客户端,翻阅源码发现,redisgo执行pipeline逻辑是 把命令和参数写到golang原生的bufio中,如果超过bufio默认最大值(4096字节),就发起一次I/O,flush到内核态。

redisgo编码pipeline规则 如下图, *表示后面参数加命令的个数,$表示后面的字符长度 ,一条HGEALL命令实际占45字节。

那其实90天数据,一次I/O就可以搞定了(90 * 45 < 4096字节)!

果然,又快了1000ms,耗费时间达到了1秒以内

对吞吐量和qps的取舍
笔者需求任务算是完成了,可是再进一步思考,redis的pipeline一次性带上多少HGETALL操作的key才是合理的呢? 换句话说,服务器吞吐量大了,可能就会导致qps急剧下降(网卡大量收发数据和redis内部协议解析,redis命令排队堆积,从而导致的缓慢),而想要qps高,服务器吞吐量可能就要降下来,无法很好的利用带宽。
对两者之间的取舍,同样是不能拍脑袋决定的,用压测数据说话!

简单写了一个压测程序,通过比较请求量和qps的关系,来看一下吞吐量和qps的变化,从而选择一个适合业务需求的值。

package mainimport (    "crypto/rand"    "fmt"    "math/big"    "strconv"    "time"    "github.com/garyburd/redigo/redis")const redisKey = "redis_test_key:%s"func main() {    for i := 1; i < 10000; i++ {        testRedisHGETALL(getPreKeyAndLoopTime(i))    }}func testRedisHGETALL(keyList [][]string) {    Conn, err := redis.Dial("tcp", "127.0.0.1:6379")    if err != nil {        fmt.Println(err)        return    }    costTime := int64(0)    start := time.Now().Unix()    for _, keys := range keyList {        for _, key := range keys {            Conn.Send("HGETALL", fmt.Sprintf(redisKey, key))        }        Conn.Flush()    }    end := time.Now().Unix()    costTime = end - start    fmt.Printf("cost_time=[%+v]ms,qps=[%+v],keyLen=[%+v],totalBytes=[%+v]",        1000*int64(len(keyList))/costTime, costTime/int64(len(keyList)), len(keyList), len(keyList)*len(keyList[0])*len(redisKey))}//根据key的长度,设置不同的循环次数,平均计算,取除网络延迟带来的影响func getPreKeyAndLoopTime(keyLen int) [][]string {    loopTime := 1000    if keyLen < 10 {        loopTime *= 100    } else if keyLen < 100 {        loopTime *= 50    } else if keyLen < 500 {        loopTime *= 10    } else if keyLen < 1000 {        loopTime *= 5    }    return generateKeys(keyLen, loopTime)}func generateKeys(keyLen, looTime int) [][]string {    keyList := make([][]string, 0)    for i := 0; i < looTime; i++ {        keys := make([]string, 0)        for i := 0; i < keyLen; i++ {            result, _ := rand.Int(rand.Reader, big.NewInt(100))            keys = append(keys, strconv.FormatInt(result.Int64(), 10))        }        keyList = append(keyList, keys)    }    return keyList}
windows上单机版redis结果如下:

扩展 (分布式方案下pipeline操作)
需求最终是完成了,可是转念一想,现在都是集群版的redis,pipeline批量请求的key可能分布在不同的机器上,但pipeline请求最终可能只被一台redis server处理,那不就是会读取数据失败吗? 于是,笔者查找几个通用的redis 分布式方案,看看他们是如何处理这pipeline问题的。

redis cluster

redis cluster 是官方给出的分布式方案。 Redis Cluster在设计中没有使用一致性哈希,而是使用数据分片(Sharding)引入哈希槽(hash slot)来实现。 一个 Redis Cluster包含16384(0~16383)个哈希槽,存储在Redis Cluster中的所有键都会被映射到这些slot中,集群中的每个键都属于这16384个哈希槽中的一个,集群使用公式slot=CRC16 key/16384来计算key属于哪个槽。 比如redis cluster有5个节点,每个节点就负责一部分哈希槽, 如果参数的多个key在不同的slot,在不同的主机上,那么必然会出错。

因此redis cluster分布式方案是不支持pipeline操作,如果想要做,只有客户端缓存slot和redis节点的关系,在批量请求时,就通过key算出不同的slot以及redis节点,并行的进行pipeline。

github.com/go-redis就是这样做的,有兴趣可以阅读下源码。

codis

市面上还流行着一种在客户端和服务端之间增设代理的方案,比如codis就是这样。 对于上层应用来说,连接 Codis-Proxy 和直接连接 原生的 Redis-Server 没有的区别,也就是说codis-proxy会帮你做上面并行分槽请求redis server,然后合并结果在一起的操作,对于使用者来说无感知。
总结
在做需求的过程中,发现了很多东西不能拍脑袋决定,而是前期做技术方案的时候,想清楚,调研好,用数据和逻辑去说服自己。
数据 字节 内存 需求 命令 方案 就是 时间 分配 哈希 存储 不同 吞吐量 结构 吞吐 分布式 多个 性能 笔者 脑袋 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 网络安全宣传教育大纲 数据库的三级模式物理结构和 全球网络安全企业50强名单 程序员网络技术 重庆嵌入式软件开发定制 大丰农商行网络安全职位 政府类软件开发 专业财务代账软件开发 网络安全等级保护的内容流程图 福建省事业单位网络安全招聘 南山网络安全建设哪家好 红盟网络安全工作 ibm3650服务器故障灯黄色 保健软件开发定制 天津hp服务器虚拟化部署云空间 厦门快快网络技术有限公司 网络安全法缺陷 国内金融领域最大的软件开发公司 教育系统网络安全管理方案 计算机网络技术专接本学校 银河麒麟系统服务器设置 学好网络安全的计划 孤岛惊魂5场景什么软件开发 手游可以改服务器时间吗 苏州喔噻互联网科技怎么样 南京软件开发实习生 海康服务器管理口默认ip 定义数据库模式是dba职责吗 电脑病毒与网络安全的危害 延庆区推广网络技术咨询
0