业务侧申请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内存容量评估的更多相关文章

  1. Redis 内存满了怎么办?这样设置才正确!

    上回在<Redis 数据过期了会被立马删除么?>说到如果过期的数据太多,定时删除无法删除完全(每次删除完过期的 key 还是超过 25%),同时这些 key 再也不会被客户端请求,就无法走 ...

  2. 深入学习Redis(1):Redis内存模型

    前言 Redis是目前最火爆的内存数据库之一,通过在内存中读写数据,大大提高了读写速度,可以说Redis是实现网站高并发不可或缺的一部分. 我们使用Redis时,会接触Redis的5种对象类型(字符串 ...

  3. 深入理解Redis内存模型

    前言 Redis是目前最火爆的内存数据库之一,通过在内存中读写数据,大大提高了读写速度,可以说Redis是实现网站高并发不可或缺的一部分. 我们使用Redis时,会接触Redis的5种对象类型(字符串 ...

  4. Redis 内存模型

    了解 Redis 的 5 种对象类型(字符串.哈希.列表.集合.有序集合)的用法和特点的基础,了解 Redis 的内存模型,对 Redis 的使用有很大帮助,例如: 估算 Redis 内存使用量.内存 ...

  5. MySQL准入规范及容量评估

    一.数据库设计 1.表结构设计 -表中的自增列(auto_increment属性)推荐使用bigint类型 -首选使用非空的唯一键, 其次选择自增列或发号器 不使用更新频繁的列,尽量不选择字符串列,不 ...

  6. 【转】深入学习Redis(1):Redis内存模型

    原文:https://www.cnblogs.com/kismetv/p/8654978.html 前言 Redis是目前最火爆的内存数据库之一,通过在内存中读写数据,大大提高了读写速度,可以说Red ...

  7. Redis内存回收:LRU算法

    Redis技术交流群481804090 Redis:https://github.com/zwjlpeng/Redis_Deep_Read Redis中采用两种算法进行内存回收,引用计数算法以及LRU ...

  8. redis内存模型

    前言 Redis是目前最火爆的内存数据库之一,通过在内存中读写数据,大大提高了读写速度,可以说Redis是实现网站高并发不可或缺的一部分. 我们使用Redis时,会接触Redis的5种对象类型(字符串 ...

  9. redis内存模型及应用解读

    Redis是目前最火爆的内存数据库之一,通过在内存中读写数据,大大提高了读写速度,可以说Redis是实现网站高并发不可或缺的一部分. 我们使用Redis时,会接触Redis的5种对象类型:字符串.哈希 ...

  10. 深度历险:Redis 内存模型详解

    https://mp.weixin.qq.com/s/Gp6Ur7omGY6ZqDWygU2meQ Redis 是目前最火爆的内存数据库之一,通过在内存中读写数据,大大提高了读写速度,可以说 Redi ...

随机推荐

  1. c# 属性类(特性)

    前言 c# 属性类也称做特性.这是一篇垫文,为后面的过滤器和其他特性类的东西做铺垫. 正文 看一段代码: static void Main(string[] args) { Attribitefunc ...

  2. C++ 默认参数与引用传递:语法、用法及示例

    C++ 默认参数 默认参数概述 在 C++ 中,函数参数可以拥有默认值.这意味着,在调用函数时,如果省略了某个参数,那么将使用为该参数指定的默认值. 设置默认参数 默认参数值使用等号 = 符号进行设置 ...

  3. 10个常用的JS工具库,80%的项目都在用

    高手区别于普通人的重要一点是,他们善于利用工具,把更多的时间留给了规划和思考.写代码也是同样的道理,工具用好了,你就有更多的时间来规划架构和攻克难点.今天就给大家分享一下当前最流行的 js 工具库,如 ...

  4. 红日安全vulnstack (一)

    网络拓扑图 靶机参考文章 CS/MSF派发shell 环境搭建 IP搭建教程 本机双网卡 65网段和83网段是自己本机电脑(虚拟机)中的网卡, 靶机外网的IP需要借助我们这两个网段之一出网 Kali ...

  5. Windows代理配合Burp抓取客户端+小程序数据包

    "感谢您阅读本篇博客!如果您觉得本文对您有所帮助或启发,请不吝点赞和分享给更多的朋友.您的支持是我持续创作的动力,也欢迎留言交流,让我们一起探讨技术,共同成长!谢谢!"  在渗透测 ...

  6. Oracle 与当前日期有关的内容

    Oracle 与当前日期有关的内容 求当前日期是周几: 大概就是下面这种方法 to_char(date,'D') Select to_char(date,'ss') from dual取当前时间秒部分 ...

  7. 全链路灰度新功能:MSE 上线配置标签推送

    简介: 本文介绍了全链路灰度场景给配置管理带来的问题,介绍了 MSE 针对这一场景的解决方案,并通过实践的方式展示了配置标签推送的使用流程.后续,MSE 还会针对配置治理做更多的探索,帮助用户更好地解 ...

  8. 如何迁移 Flink 任务到实时计算

    简介: 本文由阿里巴巴技术专家景丽宁(砚田)分享,主要介绍如何迁移Flink任务到实时计算 Flink 中来. 通常用户在线下主要使用 Flink run,这会造成一些问题,比如:同一个配置因版本而变 ...

  9. 从 Flink Forward Asia 2021,看Flink未来开启新篇章

    ​简介:本文将对FFA Keynote议题作一些简单的归纳总结,感兴趣的小伙伴们可以在FFA官网[2]找到相关主题视频观看直播回放. ​ 作者 | 梅源(Yuan Mei) 来源 | 阿里技术公众号 ...

  10. [Caddy2] cloudflare, acme: cleaning up failed: no memory of presenting a DNS record

    使用 cloudflare 做为 DNS 之后,使用 Caddy 申请 Lets Encrypt 证书. 有时在日志里会发现一系列的提示信息: acme: use dns-01 solver acme ...