Redis内存容量评估
业务侧申请redis服务器资源时,需要事先对redis容量做一个大致评估,之前的容量评估公式基本只是简单的 (key长度 value长度)* key个数
,误差较大,后期经常需要进行缩扩容调整,因此提出一个较精确的redis容量评估模型就显得很有必要。
先来查看一个命令:
info memory
used_memory:847624
used_memory_human:827.76K
used_memory_rss:2592768
used_memory_rss_human:2.47M
used_memory_peak:882896
used_memory_peak_human:862.20K
used_memory_peak_perc:96.00%
used_memory_overhead:836086
used_memory_startup:786456
used_memory_dataset:11538
used_memory_dataset_perc:18.86%
total_system_memory:16570413056
total_system_memory_human:15.43G
used_memory_lua:37888
used_memory_lua_human:37.00K
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
mem_fragmentation_ratio:3.06
mem_allocator:jemalloc-4.0.3
active_defrag_running:0
lazyfree_pending_objects:0
看一下这个命令下的一些参数详解
参数 | 详解 |
---|---|
used_memory | 由 Redis 分配器分配的内存总量,包含了redis进程内部的开销和数据占用的内存,以字节(byte)为单位 |
used_memory_human | 以更直观的可读格式显示返回使用的内存量。 |
used_memory_rss | rss是Resident Set Size的缩写,表示该进程所占物理内存的大小,是操作系统分配给Redis实例的内存大小。即Redis进程占据操作系统的内存(单位是字节),与top及ps命令看到的值是一致的;除了分配器分配的内存之外,used_memory_rss还包括进程运行本身需要的内存、内存碎片等,但是不包括虚拟内存。 因此,used_memory和used_memory_rss,前者是从Redis角度得到的量,后者是从操作系统角度得到的量。二者之所以有所不同,一方面是因为内存碎片和Redis进程运行需要占用内存,使得前者可能比后者小,另一方面虚拟内存的存在,使得前者可能比后者大。 |
used_memory_rss_human | 以更直观的可读格式显示该进程所占物理内存的大小。 |
used_memory_peak | redis的内存消耗峰值(以字节为单位) |
used_memory_peak_human | 以更直观的可读格式显示返回redis的内存消耗峰值 |
used_memory_peak_perc | 使用内存达到峰值内存的百分比,即(used_memory/ used_memory_peak) *100% |
used_memory_overhead | Redis为了维护数据集的内部机制所需的内存开销,包括所有客户端输出缓冲区、查询缓冲区、AOF重写缓冲区和主从复制的backlog。 |
used_memory_startup | Redis服务器启动时消耗的内存 |
used_memory_dataset | 数据占用的内存大小,即used_memory-used_memory_overhead |
used_memory_dataset_perc | 数据占用的内存大小的百分比,100%*(used_memory_dataset/(used_memory- used_memory_startup)) |
total_system_memory | 整个系统内存 |
total_system_memory_human | 以更直观的可读格式显示整个系统内存 |
used_memory_lua | Lua脚本存储占用的内存 |
used_memory_lua_human | 以更直观的可读格式显示Lua脚本存储占用的内存 |
maxmemory | Redis实例的最大内存配置 |
maxmemory_human | 以更直观的可读格式显示Redis实例的最大内存配置 |
maxmemory_policy | 当达到maxmemory时的淘汰策略 |
mem_fragmentation_ratio | 内存的碎片率,used_memory_rss/used_memory 比值 --4.0版本之后可以使用memory purge手动回收内存 由于在实际应用中,Redis的数据量会比较大,此时进程运行占用的内存与Redis数据量和内存碎片相比,都会小得多;因此used_memory_rss和used_memory的比例便成了衡量Redis内存碎片率的参数;这个参数就是mem_fragmentation_ratio。 mem_fragmentation_ratio一般大于1,且该值越大,内存碎片比例越大。如果mem_fragmentation_ratio小于1,说明Redis使用了虚拟内存,由于虚拟内存的媒介是磁盘,比内存速度要慢很多,当这种情况出现时,应该及时排查,如果内存不足应该及时处理,如增加Redis节点、增加Redis服务器的内存、优化应用等。 |
mem_allocator | Redis使用的内存分配器,在编译时指定,可以是 libc 、jemalloc或者tcmalloc,默认是jemalloc。截图中使用的便是默认的jemalloc。 |
active_defrag_running | 表示没有活动的defrag任务正在运行,1表示有活动的defrag任务正在运行(defrag:表示内存碎片整理) |
lazyfree_pending_objectsr | 表示redis执行lazy free操作,在等待被实际回收内容的键个数 |
redis常用数据结构
1.SDS
redis没有直接使用c语言传统的字符串(以空字符为结尾的字符数组),而是自己创建了一种名为SDS(简单动态字符串)的抽象类型,用作redis默认的字符串。
SDS的定义如下(sds.h/sdshdr):
struct sdshdr {
int len; // 记录buf数组中已使用字节的数量
int free; // 记录buf数组中未使用字节的数量
char buf[]; // 字节数组,用于保存实际字符串
}
上图的SDS实例中存储了字符串“Redis”, sdshdr中对应的free长度为5,len长度为5, SDS占用的总字节数为sizeof(int) * 2 + 5 + 5 + 1 = 19。
2.链表
链表在redis中的应用非常广泛,列表键的底层实现之一就是链表。每个链表节点使用一个listNode结构来表示,具体定义如下(adlist.h/listNode):
typedef struct listNode {
struct listNode *prev; // 前置节点
struct listNode *next; // 后置节点
void *value; // 节点的值
} listNode;
redis另外还使用了list结构来管理链表,以方便操作,具体定义如下(adlist.h/list):
typedef struct list {
listNode *head; // 表头节点
listNode *tail; // 表尾结点
void *(*dup)(void *ptr); // 节点值复制函数
void (*free)(void *ptr); // 节点值释放函数
int (*match)(void *ptr, void *key); // 节点值对比函数
unsigned int len; // 链表所包含的节点数量
} list;
listNode结构占用的总字节数为24,list结构占用的总字节数为48。
3.跳跃表
redis采用跳跃表(skiplist)作为有序集合键的底层实现之一,跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表可以理解为多层的有序双向链表。
zskiplistNode 结构占用的总字节数为(24 16*n),n为level数组的大小。
zskiplist结构则用于保存跳跃表节点的相关信息,header和tail分别指向跳跃表的表头和表尾节点,length记录节点总数量,level记录跳跃表中层高最大的那个节点的层数量。zskiplist结构占用的总字节数为32。
4.字典
字典在redis中的应用很广泛,redis的数据库就是使用字典作为底层实现的,具体数据结构定义如下(dict.h/dict):
typedef struct dict {
dictType *type; // 字典类型
void *privdata; // 私有数据
dictht ht[2]; // 哈希表数组
int rehashidx; // rehash索引,当不进行rehash时,值为-1
int iterators; // 当前该字典迭代器个数
} dict;
type属性和privdata属性是为了针对不同类型的键值对而设置的,此处了解即可。dict中还保存了一个长度为2的dictht哈希表数组,哈希表负责保存具体的键值对,一般情况下字典只使用ht[0]哈希表,只有在rehash时才使用ht[1]。dict结构占用的总节数为88。
5.对象
内存分配规则
jemalloc是一种facebook推出的通用的内存管理方法,着重于减少内存碎片和支持可伸缩的并发性,我们部门的redis版本中就引入了jemalloc,做redis容量评估前必须对jemalloc的内存分配规则有一定了解。除了jemalloc,还有ptmalloc和tcmalloc等等
在最新的Redis2.4.4版本中,jemalloc已经作为源码包的一部分包含在源码包中,所以可以直接被使用。而如果你要使用tcmalloc的话,是需要自己安装的。
jemalloc基于申请内存的大小把内存分配分为三个等级:small,large,huge:
- Small Object的size以8字节,16字节,32字节等分隔开,小于页大小;
- Large Object的size以分页为单位,等差间隔排列,小于chunk的大小;
- Huge Object的大小是chunk大小的整数倍。
对于64位系统,一般chunk大小为4M,页大小为4K,内存分配的具体规则如下:
下面是jemalloc size class categories,左边是用户申请内存范围,右边是实际申请的内存大小
1 – 4 size class:4
5 – 8 size class:8
9 – 16 size class:16
17 – 32 size class:32
33 – 48 size class:48
49 – 64 size class:64
65 – 80 size class:80
81 – 96 size class:96
97 – 112 size class:112
113 – 128 size class:128
129 – 192 size class:192
193 – 256 size class:256
257 – 320 size class:320
321 – 384 size class:384
385 – 448 size class:448
449 – 512 size class:512
513 – 768 size class:768
769 – 1024 size class:1024
1025 – 1280 size class:1280
1281 – 1536 size class:1536
1537 – 1792 size class:1792
1793 – 2048 size class:2048
2049 – 2304 size class:2304
2305 – 2560 size class:2560
容量评估
1.string
一个简单的set命令最终会产生4个消耗内存的结构,中间free掉的不考虑
- 1个dictEntry结构,24字节,负责保存具体的键值对;jemalloc会分配32字节的内存块。
- 1个redisObject结构,16字节,用作val对象;jemalloc会分配16字节的内存块。
- 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个数 × 指针大小
2.hash
哈希对象的底层实现数据结构可能是zipmap或者hashtable,当同时满足下面这两个条件时,哈希对象使用zipmap这种结构(此处列出的条件都是redis默认配置,可以更改):
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
- 哈希对象保存的键值对的数量都小于512个;
可以看出,业务侧真实使用场景基本都不能满足这两个条件,所以哈希类型大部分都是hashtable结构,与string类型不同的是,hash类型的值对象并不是指向一个SDS结构,而是指向又一个dict结构,dict结构保存了哈希对象具体的键值对,
一个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个数 × 指针大小
3.zset
同哈希对象类似,有序集合对象的底层实现数据结构也分两种:ziplist或者skiplist,当同时满足下面这两个条件时,有序集合对象使用ziplist这种结构(此处列出的条件都是redis默认配置,可以更改):
- 有序集合对象保存的元素数量小于128个;
- 有序集合保存的所有元素成员的长度都小于64字节;
业务侧真实使用时基本都不能同时满足这两个条件,因此这里只讲skiplist结构的情况。skiplist类型的值对象指向一个zset结构,zset结构同时包含一个字典和一个跳跃表,占用的总字节数为16,具体定义如下(redis.h/zset):
- 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 16m)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个数 × 指针大小
4.list
列表对象的底层实现数据结构同样分两种:ziplist或者linkedlist,当同时满足下面这两个条件时,列表对象使用ziplist这种结构(此处列出的条件都是redis默认配置,可以更改):
- 列表对象保存的所有字符串元素的长度都小于64字节;
- 列表对象保存的元素数量小于512个;
因为实际使用情况,这里同样只讲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个数 × 指针大小
实际操作
以会员登录态存储作为例子:
其存储结构如下:
127.0.0.1:6379> keys *
"gateway:session-group:1:1:2181"
"gateway:1:1:cx_no2XUnt-epl+HShc3qhZV"
"gateway:1:1:EetQpVwcYDvoZ5hF8ETUJqqfjPFz4XqsPFcsO122WKp6MEAsCo3VIRtoiMKD9wD8lxU_33JofzT_Rn38MAlWmA"
127.0.0.1:6379> get gateway:1:1:cx_no2XUnt-epl+HShc3qhZV
"2181|EetQpVwcYDvoZ5hF8ETUJqqfjPFz4XqsPFcsO122WKp6MEAsCo3VIRtoiMKD9wD8lxU_33JofzT_Rn38MAlWmA"
127.0.0.1:6379> get gateway:1:1:EetQpVwcYDvoZ5hF8ETUJqqfjPFz4XqsPFcsO122WKp6MEAsCo3VIRtoiMKD9wD8lxU_33JofzT_Rn38MAlWmA
"2181"
127.0.0.1:6379> ZRANGE gateway:session-group:1:1:2181 0 -1
"cx_no2XUnt-epl+HShc3qhZV|app.android"
此时假定为:每个人只在一个设备登录一次的情况(没有踢人的状况)。
refresh-token的key长度为36,value91个
access-token的key长度为98,value4个
session-group的key长度为19,元素有2个,长度为36
按照计算来就是
string:
总内存消耗 = (dictEntry大小 + redisObject大小 + key_SDS大小 + val_SDS大小)× key个数 + bucket个数 × 指针大小
- 一个dictEntry,24字节,jemalloc会分配32字节的内存块。
- 一个RedisObject,16字节,jemalloc会分配16字节的内存块。
- 一个key,36字节,所以SDS(key)需要36+9=45个字节,jemalloc会分配48字节的内存块。
- 一个value,91字节,所以SDS(value)需要91+9=100个字节,jemalloc会分配112字节的内存块。
bucket空间:bucket数组的大小为大于10000的最小的2^14,是16384,每个bucket元素为8字节(因为64位系统中指针大小为8字节)。
(24+16+48+112)×10000+8×16384 = 131072+2000000=2.131072M(线上计算2.197266)
(24+16+112+4)×10000+8×16384 = 131072+1560000=1.691072M(线上计算1.831055)
zset:
总内存消耗 = [(val_SDS大小 + redisObject大小 + zskiplistNode大小 + dictEntry大小)× value个数 value_bucket个数 × 指针大小 + 32层zskiplistNode大小 + zskiplist大小 + dict大小 + zset大小 + redisObject大小 key_SDS大小 + dictEntry大小 ] × key个数 + key_bucket个数 × 指针大小
- 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 16m)n字节,用作跳跃表节点,m等于节点层数;
- n个redisObject结构,16*n字节,用作集合中的成员对象;
- n个SDS结构,(value长度 9)*n字节,用作成员字符串;
[(48 + 16 + 53.336 + 32)× 10000 + 16384 × 8 + 640 + 32 + 96 + 16 + 16 + 32 + 32] × 10000 + 16384 × 8 = 16.316610784 (18.001556)
参考链接
- 腾讯游戏学院Redis容量评估模型:https://gameinstitute.qq.com/community/detail/114987
- redis在线测算:http://www.redis.cn/redis_memory/
- 可能是目前最详细的Redis内存模型及应用解读:https://dbaplus.cn/news-158-2127-1.html