Redis 內存分配分析

爲何要分析

以前業務反應,數據導入到Redis 中,內存是原來文件佔用的幾倍。因此這裏來介紹一下Redis是如何分配內存的。而且在咱們平常去評估一個新上線的業務redis內存使用也是很是有幫助的。redis

須要瞭解的

這裏以簡單的Redis String數據類型做爲例子,其餘數據類型能夠做爲參考,只要不是採用壓縮數據類型存儲的。全文會介紹到涉及到內存分配的地方。而且會以此來計算Redis使用的內存,最終與Redis info 中統計的內存使用進行比較。服務器

在介紹以前須要簡單介紹一下Redis中是如何存儲Key以及Value的。數據結構

其實在Redis中,並非單純將key 與value保存到內存中就能夠的。它須要依賴一些結構對其進行管理。ide

圖片描述

如上圖所示,在Redis中,一個DB對應上面綠色的一個 dict結構體:工具

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

該結構體包含兩個dictht結構體,dictht結構體以下:ui

typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

dictht結構體中又包含指向多個dictEntry 結構體的指針,dictEntry結構體以下:this

typedef struct dictEntry {
    void *key;             
    union {               
        void *val;         
        uint64_t u64;     
        int64_t s64;       
        double d;         
    } v;                   
    struct dictEntry *next;
} dictEntry;

因此最終key及value是存儲在dictEntry中(準確說是key和val指向對應的key及value對象)。編碼

開始計算

這裏Redis爲何要這麼設計就不重點介紹了,這裏重點討論在Redis存儲一個 鍵值對(key/value)的時候,這些結構體中涉及到須要分配內存的地方。lua

咱們先看在咱們執行一條 set jingbo test 命令的時候,Redis是怎麼分配內存的。spa

在Redis 服務器端接收到 set jingbo test這條命令的時候,會在processMultibulkBuffer 方法中調用createStringObject方法分別爲set/jingbo/test 建立三個字符串對象。
建立字符串對象的時候又區分是不是EMBSTR 編碼,這裏就不討論了。由於不論是否是採用EMBSTR編碼,所佔的內存是沒有變化的,只是影響效率。
因爲這裏 set/jingbo/test 字符都沒有超過39個,因此Redis會採用EMBSTR編碼,那麼建立對象方法以下:

/* Create a string object with encoding REDIS_ENCODING_EMBSTR, that is
 * an object where the sds string is actually an unmodifiable string                                                                                                                                                                       
 * allocated in the same chunk as the object itself. */
robj *createEmbeddedStringObject(char *ptr, size_t len) {
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr)+len+1);
    struct sdshdr *sh = (void*)(o+1);
    o->type = REDIS_STRING;
    o->encoding = REDIS_ENCODING_EMBSTR;
    o->ptr = sh+1;
    o->refcount = 1;
    o->lru = LRU_CLOCK();
 
    sh->len = len;
    sh->free = 0;
    if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {
        memset(sh->buf,0,len+1);
    }   
    return o;
}

那麼咱們能夠看到在第一行中,Redis爲其分配了sizeof(robj)+sizeof(struct sdshdr)+len+1這麼大的內存。
這裏計算 sizeof(robj)= 16 +sizeof(struct sdshdr) = 8 + len(字符串自己長度) + 1
因此jingbo這個字符串在這裏就須要16+8+6+1=31b,可是Redis採用的內存分配器實際爲其分配32b,同理test這個字符串內存分配器爲其分配32b

因爲是set命令,接着就到dbAdd方法下,dbAdd方法以下:

/* Add the key to the DB. It's up to the caller to increment the reference
 * counter of the value if needed.
 *
 * The program is aborted if the key already exists. */
void dbAdd(redisDb *db, robj *key, robj *val) {                                                                                                                                                                                             
    sds copy = sdsdup(key->ptr);
    int retval = dictAdd(db->dict, copy, val);
 
    redisAssertWithInfo(NULL,key,retval == REDIS_OK);
    if (val->type == REDIS_LIST) signalListAsReady(db, key);
    if (server.cluster_enabled) slotToKeyAdd(key);
 }

這裏的robj key, robj val 傳入的對象就是剛剛Redis建立的字符串對象。咱們能夠看到在方法的第一行,其實最終建立了一個sds字符串對象,就是調用如下方法:

sds sdsnewlen(const void *init, size_t initlen) {
    struct sdshdr *sh;
 
    if (init) {
        sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
    } else {
        sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
    }
    if (sh == NULL) return NULL;
    sh->len = initlen;
    sh->free = 0;
    if (initlen && init)
        memcpy(sh->buf, init, initlen);
    sh->buf[initlen] = '\0';
    return (char*)sh->buf;
}

因此這裏須要的內存大小就是 sizeof(struct sdshdr)+initlen+1,即 sizeof(struct sdshdr) = 8 + initlen(字符串自己長度) + 1 ,那麼key在這裏就須要8+6+1=15b,內存分配器實際分配16b
因此其實最終Redis 的 key存儲是用的上面的建立的這個SDS對象。value 就是以前建立的字符串對象。最後dictEntry結構體中的key和 value會分別指向key和value對象,那以前建立的字符串對象會在客戶端釋放或者其餘狀況下進行釋放。
這裏還有一個涉及到內存分配的地方就是爲 dictEntry結構體分配內存,dictEntry結構體須要24b,Redis內存分配器實際爲其分配32b

那麼目前爲止Redis分配的內存爲 16b(jingbo)+ 32b(test) + 32b(dictEntry 結構體) = 78b

驗證結果

那實際狀況是不是這樣呢?

咱們這裏採用Redis 官方自帶壓測工具benchmark壓測。

/usr/local/rc-redis-3.0.7/src/redis-benchmark -p 6379 -t set -n 1000000 -r 1000000

壓測結果:

[jingbo8@poseidon54 ~]$ redis-cli -p 6379 info|grep keys
expired_keys:0
evicted_keys:0
keyspace_hits:0
keyspace_misses:0
db0:keys=632147,expires=0,avg_ttl=0
[jingbo8@poseidon54 ~]$ redis-cli -p 6379 info|grep mem
used_memory:69423144
used_memory_human:66.21M
used_memory_rss:72884224
used_memory_peak:72148656
used_memory_peak_human:68.81M
used_memory_lua:36864
mem_fragmentation_ratio:1.05
mem_allocator:jemalloc-3.6.0

咱們能夠看到共有632147 個key,佔用66.21M內存。benchmark全部的key以下所示:

set key:000000166802 xxx

key 爲 16個字符,value 爲三個字符,那麼key須要 8+16+1=25b,實際分配 32b,value須要 16+8+3+1=28b,實際分配 32b,dictEntry結構體須要24b,實際分配32b

因此這裏單個key須要 32b(key)+32b(value)+32b(dictEntry結構體 )=96b

那麼總共內存是:

96b*632147=60686112
60686112/1024/1024 = 57.87MB

咱們能夠看到這裏離66.21M還差一些內存。這裏咱們並無去考慮Redis在初始爲一些元數據結構分配的內存(好比建立的共享對象等),咱們離實際使用的內存還差66.21-57.87=8.34MB

那其實咱們少算了dictht結構體所佔用的內存。上圖中的ht[0]和ht[1]爲兩個dictht結構體。ht[1]主要是爲了在ht[0]須要擴容的時候使用。日常不佔用內存,這裏主要看ht[0]中佔用的內存。

/* Expand or create the hash table */
int dictExpand(dict *d, unsigned long size)
{
    dictht n; /* the new hash table */
    unsigned long realsize = _dictNextPower(size);
 
    /* the size is invalid if it is smaller than the number of
     * elements already inside the hash table */
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;
 
    /* Rehashing to the same table size is not useful. */
    if (realsize == d->ht[0].size) return DICT_ERR;
 
    /* Allocate the new hash table and initialize all pointers to NULL */
    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;
 
    /* Is this the first initialization? If so it's not really a rehashing
     * we just set the first hash table so that it can accept keys. */
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }   
 
    /* Prepare a second hash table for incremental rehashing */
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}

在系統最開始初始化的時候會初始化ht[0],而且爲realsize 分配爲4,那麼這裏分配的內存是 realsizesizeof(dictEntry)=4*8 = 32b,這只是最開始的時候,而且只能存儲4個dictEntry指針,也就是對應4個key,在key增加的時候,會對ht[0]進行擴容,這時候會先將ht[1]擴容至ht[0]的兩倍,而後將ht[0]中對應的dictEntry所有遷移到ht[1],而後他們再相互交換一下。那麼ht[1]又變成ht[0]了,以前的ht[0]變爲ht[1]而且釋放內存。因此在每次ht[0]滿了以後都會擴容至之前的2倍。

那目前咱們key的數量是632147,那麼realsize是多少呢

realsize=4
realsize=8
realsize=16
realsize=32
realsize=64
realsize=128
realsize=256
realsize=512
realsize=1024
realsize=2048
realsize=4096
realsize=8192
realsize=16384
realsize=32768
realsize=65536
realsize=131072
realsize=262144
realsize=524288
realsize=1048576

524288 <632147 <1048576,因此目前realsize是1048576,那麼總共須要分配的內存就是1048576*8= 8388608,8388608/1024/1024=8MB

那麼以前8.34-8=0.34 MB,因此目前咱們只相差0.34MB。那麼咱們把全部key清空看一下Redis自己使用了多少內存呢。

[jingbo8@poseidon54 ~]$ redis-cli -p 6379 info|grep mem                                                             
used_memory:349256
used_memory_human:341.07K
used_memory_rss:2015232
used_memory_peak:349256
used_memory_peak_human:341.07K
used_memory_lua:36864
mem_fragmentation_ratio:5.77
mem_allocator:jemalloc-3.6.0

0.34*1024=348.16k,這樣與目前的內存很是接近了。

那麼從上面這個分析結果來看,Redis 自己的結構體佔據了大部分的內存。因此最終形成導入到Redis中的內存與以前文件中所佔用的空間差距較大,內存是寶貴的資源,因此你們在往Redis存儲數據的時候必定要設計好。

至此整個內存如何分配分析完成,有問題能夠隨時聯繫。

相關文章
相關標籤/搜索