Redis容量评估

存储容量预估系统设计最重要的项之一。精准的容量预估,可以帮助我们选择最合适存储软件。

本文主要来介绍下Redis几种常用的数据结构,以及内存占用大小。

字符串(SDS)

1
2
3
4
5
6
//SDS的定义如下(sds.h/sdshdr):
struct sdshdr {
int len; // 记录buf数组中已使用字节的数量
int free; // 记录buf数组中未使用字节的数量
char buf[]; // 字节数组,用于保存实际字符串
}

字符串长度不能超过512MB

下图展示了一个SDS实例:

SDS实例中存储了字符串“Redis”, 该实例中free的值为5(表示未使用的字节为5),len的值为5(表示已使用的字节为5),加上字符数组末尾的”\0”,所以SDS实例占用的总字节数为:
(sizeof(int) * 2) + (5 + 5 + 1) = 19,即(len+free)+buf

举例说明

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

生产实践

用redis做商品缓存,key为商品id,value为商品信息。key大约占用30个字节,value大约占用1500个字节。
当缓存1百万商品时,容量预估值为(32 + 16 + 64 + 1536) * 1000000+ 1000000(预估) * 8 = 1656000000,约等于1.54G
总结:当value比较大时,占用的内存约等于value的大小*个数

双向链表(List)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//每个链表节点使用一个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;

有序集合(ZSET)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;

typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;

typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;

zskiplistNode结构占用的总字节数为:24 + 16 * n,n为level数组的大小。

zskiplist结构则用于保存跳跃表节点的相关信息,header和tail分别指向跳跃表的表头和表尾节点,length记录节点总数量,level记录跳跃表中层高最大的那个节点的层数量。zskiplist结构占用的总字节数为32。

字典

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;

typedef struct dictType {
uint64_t (*hashFunction)(const void *key);
void *(*keyDup)(void *privdata, const void *key);
void *(*valDup)(void *privdata, const void *obj);
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
void (*keyDestructor)(void *privdata, void *key);
void (*valDestructor)(void *privdata, void *obj);
} dictType;

/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;

typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;

一个Hash存储结构最终会产生以下几个消耗内存的结构:

  • 1个SDS结构,用作key字符串,占9个字节(free4个字节+len4个字节+字符串末尾”\0”1个字节);
  • 1个dictEntry结构,24字节,负责保存当前的哈希对象;
  • 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类型的容量评估模型为:

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

【换算】
总内存消耗 = [ key_SDS大小 + 16 + 24 + 88 + (16 * 2 + field_SDS大小 + val_SDS大小 + 24) * field个数 + field_bucket个数 * 8] * key个数 + key_bucket个数 * 8
总内存消耗 =[128+ key_SDS大小 +(56 + field_SDS大小 + val_SDS大小 ) * field个数 + field_bucket个数 * 8] * key个数 + key_bucket个数 * 8

集合(SET)

SET的实现其实就是value为null的hash表

参考文献

  1. Redis容量预估
  2. 深入学习Redis(1):Redis内存模型