Redis 相关数据结构的容量评估

作者:LeiLv
链接:https://www.jianshu.com/p/5c5dc0d7d776
来源:简书

1.jemalloc 内存分配模型

jemalloc基于申请内存的大小把内存分配分为三个等级:small,large,huge:

Small Object 的size以8字节,16字节,32字节等分隔开,小于页大小;

Large Object 的size以分页为单位,等差间隔排列,小于chunk的大小;

Huge Object 的大小是chunk大小的整数倍。

对于64位系统,一般chunk大小为4M,页大小为4K,内存分配的具体规则如下:
在这里插入图片描述

所以说 内存分配都需要符合上面的大小,不会因为这个内存结构占20个字节,就会分配20字节的内存,而是分配32字节

2.Redis容量评估

redis容量评估模型根据value类型而有所不同,因为key在Redis中都是字符串对象

2.1 string

一个简单的set命令最终会产生4个消耗内存的结构,中间free掉的不考虑:

1个dictEntry结构,24字节,负责保存具体的键值对;

1个redisObject结构,16字节,用作val对象;

1个SDS结构,(key长度 + 9)字节,用作key字符串;

1个SDS结构,(val长度 + 9)字节,用作val字符串;

当key个数逐渐增多,redis还会以rehash的方式扩展哈希表节点数组,即增大哈希表的bucket个数,每个bucket元素都是个指针(dictEntry*),占8字节,bucket个数是超过key个数向上求整的2的n次方。

真实情况下,每个结构最终真正占用的内存还要考虑jemalloc的内存分配规则,综上所述,string类型的容量评估模型为:

总内存消耗 = (dictEntry大小 + redisObject大小 +key_SDS大小 + val_SDS大小)×key个数 + bucket个数 ×指针大小

#!/bin/bash
redis_path=~/java/redis-6.0.3/src

old_memory=`${redis_path}/redis-cli  info|grep used_memory:|awk -F: '{printf "%d", $2}'`

echo "before test, memory used: $old_memory"

for((i=1000; i<3000; i++))

do

    ${redis_path}/redis-cli set test_key_$i test_value_$i > /dev/null

done

new_memory=`${redis_path}/redis-cli info|grep used_memory:|awk -F: '{printf "%d", $2}'`

echo "after test, memory used: $new_memory"

测试用例中,key长度为 13,value长度为15,key个数为2000,根据上面总结的容量评估模型,容量预估值为2000 ×(32 + 16 + 32 + 32) + 2048× 8 = 240384

运行测试脚本,得到结果如下:
在这里插入图片描述

发现和我们预估的容量相差14030字节(13KB),相比于原先226352字节(221KB),误差在5%左右,模型预估还是蛮精确的。

2.2 hash

哈希对象的底层实现数据结构可能是zipmap或者hashtable,当同时满足下面这两个条件时,哈希对象使用zipmap这种结构(此处列出的条件都是redis默认配置,可以更改):

哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;

哈希对象保存的键值对的数量都小于512个;

可以看出,业务侧真实使用场景基本都不能满足这两个条件,所以哈希类型大部分都是hashtable结构,因此本篇文章只讲hashtable,对zipmap结构感兴趣的同学可以私下咨询我。

与string类型不同的是,hash类型的值对象并不是指向一个SDS结构,而是指向又一个dict结构,dict结构保存了哈希对象具体的键值对,hash类型结构关系如图4所示:

在这里插入图片描述
图4. hash类型结构关系图

一个hmset命令最终会产生以下几个消耗内存的结构:

1个dictEntry结构,24字节,负责保存当前的哈希对象;

1个SDS结构,(key长度 + 9)字节,用作key字符串;

1个redisObject结构,16字节,指向当前key下属的dict结构;

1个dict结构,88字节,负责保存哈希对象的键值对;

n个dictEntry结构,24×n字节,负责保存具体的field和value,n等于field个数;

n个redisObject结构,16×n字节,用作field对象;

n个redisObject结构,16×n字节,用作value对象;

n个SDS结构,(field长度 + 9)× n字节,用作field字符串;

n个SDS结构,(value长度 + 9)× n字节,用作value字符串;

因为hash类型内部有两个dict结构,所以最终会有产生两种rehash,一种rehash基准是field个数,另一种rehash基准是key个数,结合jemalloc内存分配规则,hash类型的容量评估模型为:

总内存消耗 = [(redisObject大小 ×2 +field_SDS大小 + val_SDS大小 + dictEntry大小)× field个数 + field_bucket个数× 指针大小 + dict大小 + redisObject大小 +key_SDS大小 + dictEntry大小 ] × key个数 + key_bucket个数×指针大小

测试验证

hash类型容量评估测试脚本如下:

#!/bin/bash
redis_path=~/java/redis-6.0.3/src

#为了凑足value 长度75,这里先写72位,剩下三位是编号
value_prefix="test_value_123456789012345678901234567890123456789012345678901234567890_"

old_memory=`${redis_path}/redis-cli  info|grep used_memory:|awk -F: '{printf "%d", $2}'`

echo "before test, memory used: $old_memory"

for((i=100; i<300; i++))

do

   for((j=100; j<300; j++))

   do

      ${redis_path}/redis-cli hset test_key_$i test_field_$j $value_prefix$j > /dev/null

   done

done

new_memory=`${redis_path}/redis-cli info|grep used_memory:|awk -F: '{printf "%d", $2}'`

echo "after test, memory used: $new_memory"

let difference=new_memory-old_memory

echo "difference is: $difference"


测试用例中,key长度为 12,field长度为14,value长度为75,key个数为200,field个数为200,根据上面总结的容量评估模型,容量预估值为[(16 + 16 + 32 + 96 + 32)×200 + 256×8 + 96 + 16 + 32 + 32 ]× 200 + 256× 8 = 8126848

运行测试脚本,得到结果如下:
在这里插入图片描述

占用了5.30M,与预估结果相差2.452M,好像这差的有点多了。

2.3 zset

同哈希对象类似,有序集合对象的底层实现数据结构也分两种:ziplist或者skiplist,当同时满足下面这两个条件时,有序集合对象使用ziplist这种结构(此处列出的条件都是redis默认配置,可以更改):

有序集合对象保存的元素数量小于128个;

有序集合保存的所有元素成员的长度都小于64字节;

业务侧真实使用时基本都不能同时满足这两个条件,因此这里只讲skiplist结构的情况。skiplist类型的值对象指向一个zset结构,zset结构同时包含一个字典和一个跳跃表,占用的总字节数为16,具体定义如下(redis.h/zset):

typedef struct zset {

dict *dict;

zskiplist *zsl;

} zset;

跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素,dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素,这两种数据结构会通过指针来共享相同元素的成员和分值,没有浪费额外的内存。zset类型的结构关系如图所示:

在这里插入图片描述
zset类型结构关系图

一个zadd命令最终会产生以下几个消耗内存的结构:

1个dictEntry结构,24字节,负责保存当前的有序集合对象;

1个SDS结构,(key长度 + 9)字节,用作key字符串;

1个redisObject结构,16字节,指向当前key下属的zset结构;

1个zset结构,16字节,负责保存下属的dict和zskiplist结构;

1个dict结构,88字节,负责保存集合元素中成员到分值的映射;

n个dictEntry结构,24×n字节,负责保存具体的成员和分值,n等于集合成员个数;

1个zskiplist结构,32字节,负责保存跳跃表的相关信息;

1个32层的zskiplistNode结构,24+16×32=536字节,用作跳跃表头结点;

n个zskiplistNode结构,(24+16×m)×n字节,用作跳跃表节点,m等于节点层数;

n个redisObject结构,16×n字节,用作集合中的成员对象;

n个SDS结构,(value长度 + 9)×n字节,用作成员字符串;

因为每个zskiplistNode节点的层数都是根据幂次定律随机生成的,而容量评估需要确切值,因此这里采用概率中的期望值来代替单个节点的大小,结合jemalloc内存分配规则,经计算,单个zskiplistNode节点大小的期望值为53.336。

zset类型内部同样包含两个dict结构,所以最终会有产生两种rehash,一种rehash基准是成员个数,另一种rehash基准是key个数,zset类型的容量评估模型为:

总内存消耗 = [(val_SDS大小 + redisObject大小 + zskiplistNode大小 + dictEntry大小)×value个数 +value_bucket个数 ×指针大小 + 32层zskiplistNode大小 + zskiplist大小 + dict大小 + zset大小 + redisObject大小 + key_SDS大小 + dictEntry大小 ] ×key个数 +key_bucket个数 × 指针大小

测试验证

zset类型容量评估测试脚本如下:

#!/bin/sh

value_prefix="test_value_123456789012345678901234567890123456789012345678901234567890_"

old_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`

echo "before test, memory used: $old_memory"

for((i=100; i<300; i++))

do

for((j=100; j<300; j++))

do

./redis-cli -h 0 -p 10009 zadd test_key_$i $j $value_prefix$j > /dev/null

done

sleep 0.5

done

new_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`

echo "after test, memory used: $new_memory"

let difference=new_memory-old_memory

echo "difference is: $difference"

测试用例中,key长度为 12,value长度为75,key个数为200,value个数为200,根据上面总结的容量评估模型,容量预估值为[(96 + 16 + 53.336 + 32)×200 + 256×8 + 640 + 32 + 96 + 16 + 16 + 32 + 32 ] ×200 + 256 × 8 = 8477888

运行测试脚本,得到结果如下:

在这里插入图片描述

真实占用6.84MB,结果相差1.24MB,说明模型预测比较准确。

2.4 list

列表对象的底层实现数据结构同样分两种:ziplist或者linkedlist,当同时满足下面这两个条件时,列表对象使用ziplist这种结构(此处列出的条件都是redis默认配置,可以更改):

列表对象保存的所有字符串元素的长度都小于64字节;

列表对象保存的元素数量小于512个;

因为实际使用情况,这里同样只讲linkedlist结构。linkedlist类型的值对象指向一个list结构,具体结构关系如图6所示:

在这里插入图片描述

图6. linkedlist类型结构关系图

一个rpush或者lpush命令最终会产生以下几个消耗内存的结构:

1个dictEntry结构,24字节,负责保存当前的列表对象;

1个SDS结构,(key长度 + 9)字节,用作key字符串;

1个redisObject结构,16字节,指向当前key下属的list结构;

1个list结构,48字节,负责管理链表节点;

n个listNode结构,24×n字节,n等于value个数;

n个redisObject结构,16×n字节,用作链表中的值对象;

n个SDS结构,(value长度 + 9)×n字节,用作值对象指向的字符串;

list类型内部只有一个dict结构,rehash基准为key个数,综上,list类型的容量评估模型为:

总内存消耗 = [(val_SDS大小 + redisObject大小 + listNode大小)× value个数 + list大小 + redisObject大小 + key_SDS大小 + dictEntry大小 ] × key个数 + key_bucket个数 × 指针大小

测试验证

list类型容量评估测试脚本如下:

#!/bin/sh

value_prefix="test_value_123456789012345678901234567890123456789012345678901234567890_"

old_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`

echo "before test, memory used: $old_memory"

for((i=100; i<300; i++))

do

for((j=100; j<300; j++))

do

./redis-cli -h 0 -p 10009 rpush test_key_$i $value_prefix$j > /dev/null

done

sleep 0.5

done

new_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`

echo "after test, memory used: $new_memory"

let difference=new_memory-old_memory

echo "difference is: $difference"


测试用例中,key长度为 12,value长度为75,key个数为200,value个数为200,根据上面总结的容量评估模型,容量预估值为[(96 + 16 + 32) ×200 + 48 + 16 + 32 + 32 ] × 200 + 256 ×8 = 5787648

运行测试脚本,得到结果如下:
在这里插入图片描述
实际占用个约4MB 内存,与理论相差2.46MB emmmm 好像不大对。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值