【Redis源碼分析】Redis 懶刪除(lazy free)簡史

張仕華redis

提一個問題:Redis是單進程單線程模式嗎?

  下圖爲Redis5.0啓動以後的效果。LWP爲線程ID,NLWP爲線程數量。能夠看到,5.0的redis server共有四個線程,一個主線程48684,三個bio(background IO,後臺io任務)線程,三個後臺線程分別執行不一樣的io任務,咱們重點考察刪除一個key時的io線程執行。多線程

clipboard.png

  Redis增長了異步刪除命令unlink,防止刪除大key時阻塞主線程。其原理爲執行unlink時會將須要刪除的數據掛到一個鏈表中,由專門的線程負責將其刪除。而原來的del命令仍是阻塞的。咱們經過對一個有1000萬條數據的集合分別執行del和unlink來觀察其效果。app

看一個大集合的刪除

首先經過腳本生成一個有1000萬個元素的集合testset,而後經過del命令刪除,以下:dom

127.0.0.1:8888>info//首先調用info命令查看內存消耗:
 
# Memory
used_memory:857536
used_memory_human:837.44K
 
127.0.0.1:8888> eval "local i = tonumber(ARGV[1]);local res;math.randomseed(tonumber(ARGV[2]));while (i > 0) do res = redis.call('sadd',KEYS[1],math.random());i = i-1;end" 1  testset 10000000 2
(nil)
(18.51s)//建立耗時18.51s 
 
127.0.0.1:8888>info//再次查看內存消耗
# Memory
used_memory:681063080
used_memory_human:649.51M

127.0.0.1:8888> scard testset//查看集合中元素數量
(integer) 9976638 //經過math.random()生成,因爲集合中不能有重複數據,能夠看到,最終只有9976638條數據不重複。
127.0.0.1:8888> sscan testset 0 //查看集合中的元素內容
1) "3670016"
2)  1) "0.94438312106969913"
    2) "0.55726669754705704"
    3) "0.3246220281927949"
    4) "0.51470726752407259"
    5) "0.33469647464095453"
    6) "0.48387842554779648"
    7) "0.3680923377946449"
    8) "0.34466382877187052"
    9) "0.019202849370987551"
   10) "0.71412580307299545"
   11) "0.12846412375963484"
   12) "0.10548432828182557"

127.0.0.1:8888> del testset //調用del命令刪除,耗時2.76s 
(integer) 1
(2.76s) 
 
127.0.0.1:8888>info//再次查看內存消耗
# Memory
used_memory:858568
used_memory_human:838.45K

從新作上邊的實驗,此次試用unlink來刪除。異步

127.0.0.1:8888> unlink testset//unlink瞬間返回
(integer) 1
127.0.0.1:8888>info//再次查看內存消耗。能夠看到,返回以後testset並無清理乾淨。內存仍然佔用了大約一半,再通過1-2s,會清理乾淨
# Memory
used_memory:326898224
used_memory_human:311.75M

嘗試漸進式刪除

參見:http://antirez.com/news/93ide

  爲了解決這個問題,Redis做者Antirez首先考慮的是經過漸進式刪除來解決。Redis也在不少地方用到了漸進式的策略,例如 lru eviction,key 過時以及漸進式rehash.原文以下:函數

So this was the first thing I tried: create a new timer function, and perform the eviction there. Objects were just queued into a linked list, to be reclaimed slowly and incrementally each time the timer function was called. This requires some trick to work well. For example objects implemented with hash tables were also reclaimed incrementally using the same mechanism used inside Redis SCAN command: taking a cursor inside the dictionary and iterating it to free element after element. This way, in each timer call, we don’t have to free a whole hash table. The cursor will tell us where we left when we re-enter the timer function.

大意就是把要刪除的對象放到一個鏈表中,起一個按期任務,每次只刪除其中一部分。性能

這會有什麼問題呢,仍然看原文中說的一種案例:ui

WHILE 1
        SADD myset element1 element2 … many many many elements
        DEL myset
    END

若是刪除沒有增長快,上邊這種案例會致使內存暴漲.(雖然不知道什麼狀況下會有這種案例發生)。因而做者開始設計一種自適應性的刪除,即經過判斷內存是增長仍是減小,來動態調整刪除任務執行的頻率,代碼示例以下:this

/* Compute the memory trend, biased towards thinking memory is raising
     * for a few calls every time previous and current memory raise. */
    
    //只要內存有一次顯示是增長的趨勢,則接下來即便內存再也不增長,仍是會有連續六次mem_is_raising都是1,即判斷爲增長。
    //注意mem_is_raising的值是根據mem_trend和0.1來比較,當0.9^22 以後小於0.1
    //這也就是上邊註釋描述的會偏向於認爲只要有一次內存是增長的,就會連續幾回加快執行調用刪除任務的頻率
    if (prev_mem < mem) mem_trend = 1; 
    mem_trend *= 0.9; /* Make it slowly forget. */
    int mem_is_raising = mem_trend > .1;

    //刪除一些數據
    /* Free a few items. */
    size_t workdone = lazyfreeStep(LAZYFREE_STEP_SLOW);

    //動態調整執行頻率
    /* Adjust this timer call frequency according to the current state. */
    if (workdone) {
        if (timer_period == 1000) timer_period = 20;
        if (mem_is_raising && timer_period > 3)//若是內存在增長,就加大執行頻率
            timer_period--; /* Raise call frequency. */
        else if (!mem_is_raising && timer_period < 20)
            timer_period++; /* Lower call frequency. *///不然減少頻率
    } else {
        timer_period = 1000;    /* 1 HZ */
    }

這種方法有個缺陷,由於畢竟是在一個線程中,當回收的特別頻繁時,會下降redis的qps,qps只能達到正常狀況下的65%.

when the lazy free cycle was very busy, operations per second were reduced to around 65% of the norm

因而redis做者antirez開始考慮異步線程回收。

異步線程

共享對象

異步線程爲什麼不能有共享數據
共享數據越多,多線程之間發生爭用的可能性越大。因此爲了性能,必須首先將共享數據消滅掉。

那麼redis在什麼地方會用到共享數據呢

如何共享

以下代碼示例爲Redis2.8.24.

先看執行sadd時底層數據是如何保存的

sadd testset val1

底層保存以下(gdb過程以下,比較晦澀,參考下文解釋):

254        set = lookupKeyWrite(c->db,c->argv[1]);
(gdb) n
255        if (set == NULL) {
(gdb) p c->argv[1]
$1 = (robj *) 0x7f58e3ccfcc0
(gdb) p *c->argv[1]
$2 = {type = 0, encoding = 0, lru = 1367521, refcount = 1, ptr = 0x7f58e3ccfcd8}

(gdb) p (char *)c->argv[1].ptr //client中的argv是一個個robj,argv[1]的ptr中存儲着key值'testset'
$4 = 0x7f58e3ccfcd8 "testset"
(gdb) n
254        set = lookupKeyWrite(c->db,c->argv[1]);
(gdb) n
255        if (set == NULL) {
...
(gdb) p (char *)((robj *)((dict *)set.ptr).ht[0].table[3].key).ptr
$37 = 0x7f58e3ccfcb8 "val1" //值val1保存在一個dict中,dict保存着一個個dictEntry,dictEntry的key是一個指針,指向一個robj,robj中是具體的值

經過下文結構體講解,能夠看下sadd testset val1,testset和val1保存在什麼地方

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;
 
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;
 
typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    int refcount;
    void *ptr;
} robj;
  • 1.首先全部的key保存在一個dict.ht[0]的dictht結構體中。經過上邊的結構體看到,dictht中的table是一個dictEntry二級指針。
  • 2.執行sadd testset val1時,testset是其中一個dictEntry中的key,key是一個void指針,實際存儲狀況爲testset保存爲一個char 類型
  • 3.假設testset通過哈希以後index爲3,則dict.ht[0].table[3].key爲testset,dict.ht[0].table[3].v.val爲一個void指針,實際存儲一個robj 類型
  • 4.第三步中的robj中有個ptr指針,指向一個dict類型。dict中的其中一個entry的key指向另外一個robj指針,該指針的ptr指向val

即獲取一個值的流程爲:

key -> value_obj -> hash table -> robj -> sds_string

而後看兩個共享對象的典型場景:

  • 1.sunionstore命令

看下代碼實現:

int setTypeAdd(robj *subject, robj *value) {
    ...
    if (subject->encoding == REDIS_ENCODING_HT) {
        if (dictAdd(subject->ptr,value,NULL) == DICT_OK) {
            incrRefCount(value);//此處的value值因爲是從已存在的集合中直接取出,refcount已是1,此處並無新建robj,而是直接將引用計數加1
            return 1;
        }
    } 
    ...
}

執行如下命令:

sadd testset1 value2

sunionstore set testset1 testset2 //即將testset1和testset2的元素取並集並保存到set中

而後咱們能夠經過查看testset的元素,看看其引用計數是否變爲了2

smembers testset

(gdb) p *(robj *)(((dict *)setobj.ptr).ht[0].table[3].key)
$88 = {type = 0, encoding = 0, lru = 1457112, refcount = 2, ptr = 0x7f58e3ccfb68} //refcount爲2
 
(gdb) p (char *)(((robj *)(((dict *)setobj.ptr).ht[0].table[3].key)).ptr)
$89 = 0x7f58e3ccfb68 "val"                                  //值爲val
  • 2.smembers命令

返回元素的時候,重點看返回時的代碼

/* Add a Redis Object as a bulk reply */
void addReplyBulk(redisClient *c, robj *obj) {
    addReplyBulkLen(c,obj);
    addReply(c,obj);
    addReply(c,shared.crlf);
}

會直接將robj對象做爲返回參數

而且客戶端傳入參數也是一個個robj對象,會直接做爲值保存到對象中

共享時如何刪除

那麼,共享對象在單線程狀況下是如何刪除的呢?

看看del命令的實現

del調用dictDelete,最終調用每一個數據類型本身的析構函數

dictFreeKey(d, he);
dictFreeVal(d, he);

集合類型調用以下函數

void dictRedisObjectDestructor(void *privdata, void *val)
{
    DICT_NOTUSED(privdata);

    if (val == NULL) return; /* Values of swapped out keys as set to NULL */
    decrRefCount(val);
}

能夠看到,只是將值的refcount減1

如何解決共享數據

新版本如何解決了共享數據

仍是經過sunionstore和smembers命令看下這兩處如何解決共享:

如下代碼使用redis 5.0.3版本介紹:

void saddCommand(client *c) {
    ...
    for (j = 2; j < c->argc; j++) {
        if (setTypeAdd(set,c->argv[j]->ptr)) added++; //sadd的時候元素也變爲了c->argv[j]->ptr,一個字符串
    }
    ...
}
 
int setTypeAdd(robj *subject, sds value) {//value是一個sds
    long long llval;
    if (subject->encoding == OBJ_ENCODING_HT) {
        dict *ht = subject->ptr;
        dictEntry *de = dictAddRaw(ht,value,NULL);
        if (de) {
            dictSetKey(ht,de,sdsdup(value));
            dictSetVal(ht,de,NULL);
            return 1;
        }
    }
    return 0;
}

增長值的時候已經變爲了一個sds.

如今的保存結構爲:

key -> value_obj -> hash table -> sds_string

而返回到客戶端的時候也變爲了一個sds,以下:

addReplyBulkCBuffer(c,elesds,sdslen(elesds));

void addReplyBulkCBuffer(client *c, const void *p, size_t len) {
    addReplyLongLongWithPrefix(c,len,'$');
    addReplyString(c,p,len);
    addReply(c,shared.crlf);
}

效果如何

效果如何呢?

首先取值的時候從robj的間接引用變爲了一個sds的直接引用。

其次減小了共享會增長內存的消耗,而使用了sds以後,每一個sds的內存佔用會比一個robj要小。咱們看下antirez如何評價這個修改:

The result is that Redis is now more memory efficient since there are no robj structures around in the implementation of the data structures (but they are used in the code paths where there is a lot of sharing going on, for example during the command dispatch and replication). 
...
But, the most interesting thing is, Redis is now faster in all the operations I tested so far. Less indirection was a real winner here. It is faster even in unrelated benchmarks just because the client output buffers are now simpler and faster.

說了兩層意思,一是內存使用更加高效了

二是更少的間接引用致使redis比之前更加快,並且客戶端輸出更加簡潔和快速。

異步線程
異步線程的實現之後在詳細描述

問題

1.多線程之間在堆上分配內存時會有爭用。可是antirez說由於redis在內存分配上使用的時間極少,能夠忽略這種狀況。

如何考慮這個問題?

參考:https://software.intel.com/zh...

相關文章
相關標籤/搜索