自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

如履薄冰:Redis懶惰刪除的巨大犧牲

數(shù)據(jù)庫 其他數(shù)據(jù)庫 Redis
大家都知道 Redis 是單線程的,但是 Redis 4.0 增加了懶惰刪除功能,懶惰刪除需要使用異步線程對已刪除的節(jié)點(diǎn)進(jìn)行內(nèi)存回收,這意味著 Redis 底層其實(shí)并不是單線程,它內(nèi)部還有幾個(gè)額外的鮮為人知的輔助線程。

[[252391]]

大家都知道 Redis 是單線程的,但是 Redis 4.0 增加了懶惰刪除功能,懶惰刪除需要使用異步線程對已刪除的節(jié)點(diǎn)進(jìn)行內(nèi)存回收,這意味著 Redis 底層其實(shí)并不是單線程,它內(nèi)部還有幾個(gè)額外的鮮為人知的輔助線程。

這幾個(gè)輔助線程在 Redis 內(nèi)部有一個(gè)特別的名稱,就是“BIO”,全稱是 Background IO,意思是在背后默默干活的 IO 線程。

不過內(nèi)存回收本身并不是什么 IO 操作,只是 CPU 的計(jì)算消耗可能會比較大而已。

01.懶惰刪除的最初實(shí)現(xiàn)不是異步線程

Redis 大佬 Antirez 實(shí)現(xiàn)懶惰刪除時(shí),它并不是一開始就想到了異步線程。它最初的嘗試是在主線程里,使用類似于字典漸進(jìn)式搬遷的方式來實(shí)現(xiàn)漸進(jìn)式刪除回收。

比如對于一個(gè)非常大的字典來說,懶惰刪除是采用類似于 scan 操作的方法,通過遍歷第一維數(shù)組來逐步刪除回收第二維鏈表的內(nèi)容,等到所有鏈表都回收完了,再一次性回收第一維數(shù)組。這樣也可以達(dá)到刪除大對象時(shí)不阻塞主線程的效果。

但是說起來容易做起來卻很難。漸進(jìn)式回收需要仔細(xì)控制回收頻率,它不能回收得太猛,這會導(dǎo)致 CPU 資源占用過多,也不能回收得像蝸牛那么慢,因?yàn)閮?nèi)存回收不及時(shí)可能導(dǎo)致內(nèi)存消耗持續(xù)增長。

Antirez 需要采用合適的自適應(yīng)算法來控制回收頻率。他首先想到的是通過檢測內(nèi)存增長的趨勢是增長“+1”還是下降“-1”,來漸進(jìn)式調(diào)整回收頻率系數(shù),這樣的自適應(yīng)算法實(shí)現(xiàn)也很簡單。

但是測試后發(fā)現(xiàn)在服務(wù)繁忙的時(shí)候,QPS 會下降到正常情況下 65% 的水平,這點(diǎn)非常致命。

所以 Antirez 才使用了如今的方案——異步線程。異步線程這套方案就簡單多了,釋放內(nèi)存不用為每種數(shù)據(jù)結(jié)構(gòu)適配一套漸進(jìn)式釋放策略,也不用搞個(gè)自適應(yīng)算法來仔細(xì)控制回收頻率,只是將對象從全局字典中摘掉,然后往隊(duì)列里一扔,主線程就干別的去了。異步線程從隊(duì)列里取出對象來,直接走正常的同步釋放邏輯就可以了。

不過使用異步線程也是有代價(jià)的,主線程和異步線程之間在內(nèi)存回收器(jemalloc)的使用上存在競爭。

這點(diǎn)競爭消耗是可以忽略不計(jì)的,因?yàn)?Redis 的主線程在內(nèi)存的分配與回收上花的時(shí)間相對整體運(yùn)算時(shí)間而言是極少的。

02.異步線程方案其實(shí)也相當(dāng)復(fù)雜

上文筆者剛說異步線程方案很簡單,為什么在這里又說它很復(fù)雜呢?因?yàn)橛幸稽c(diǎn),筆者之前沒有提到,這點(diǎn)非??膳?,嚴(yán)重阻礙了異步線程方案的改造,那就是 Redis 的內(nèi)部對象有共享機(jī)制。

比如集合的并集操作 sunionstore 用來將多個(gè)集合合并成一個(gè)新集合。 

  1. > sadd src1 value1 value2 value3  
  2. (integer) 3  
  3. > sadd src2 value3 value4 value5  
  4. (integer) 3  
  5. > sunionstore dest src1 src2  
  6. (integer) 5  
  7. > smembers dest  
  8. 1) "value2"  
  9. 2) "value3"  
  10. 3) "value1"  
  11. 4) "value4"  
  12. 5) "value5" 

我們看到新的集合包含了舊集合的所有元素。但是這里有一個(gè)我們沒看到的 trick,那就是底層的字符串對象被共享了,如下圖所示。

為什么對象共享是懶惰刪除的巨大障礙呢?因?yàn)閼卸鑴h除相當(dāng)于徹底砍掉某個(gè)樹枝,將它扔到異步刪除隊(duì)列里去。

注意這里必須是徹底刪除,不能藕斷絲連。如果底層對象是共享的,那就做不到徹底刪除。如圖 2 所示的刪除就不是徹底刪除。

所以 Antirez 為了支持懶惰刪除,將對象共享機(jī)制徹底拋棄,它將這種對象結(jié)構(gòu)稱為“share-nothing”,也就是無共享設(shè)計(jì)。

但是甩掉對象共享談何容易!這種對象共享機(jī)制散落在源代碼的各個(gè)角落,牽一發(fā)而動全身,改起來猶如在布滿地雷的道路上小心翼翼地行走。

不過 Antirez 還是決心改了,它將這種改動描述為“絕望而瘋狂”,可見改動之大、之深、之險(xiǎn),前后花了好幾周時(shí)間才改完。

不過這次修改的效果也是很明顯的,對象的刪除操作再也不會導(dǎo)致主線程卡頓了。

03.異步刪除的實(shí)現(xiàn)

主線程需要將刪除任務(wù)傳遞給異步線程,它是通過一個(gè)普通的雙向鏈表來傳遞的。因?yàn)殒湵硇枰С侄嗑€程并發(fā)操作,所以它需要有鎖來保護(hù)。

執(zhí)行懶惰刪除時(shí),Redis 將刪除操作的相關(guān)參數(shù)封裝成一個(gè) bio_job 結(jié)構(gòu),然后追加到鏈表尾部。異步線程通過遍歷鏈表摘取 job 元素來挨個(gè)執(zhí)行異步任務(wù)。 

  1. struct bio_job {  
  2.     time_t time;  // 時(shí)間字段暫時(shí)沒有使用,應(yīng)該是預(yù)留的  
  3.     void *arg1, *arg2, *arg3;  
  4. }; 

我們注意到這個(gè) job 結(jié)構(gòu)有三個(gè)參數(shù)。為什么刪除對象需要三個(gè)參數(shù)呢?我們看如下代碼。   

  1. /* What we free changes depending on what arguments are set:  
  2.      * arg1 -> free the object at pointer.  
  3.      * arg2 & arg3 -> free two dictionaries (a Redis DB).  
  4.      * only arg3 -> free the skiplist. */  
  5.     if (job->arg1)  
  6.         // 釋放一個(gè)普通對象,string/set/zset/hash 等,用于普通對象的異步刪除  
  7.         lazyfreeFreeObjectFromBioThread(job->arg1);  
  8.     else if (job->arg2 && job->arg3)  
  9.         // 釋放全局 redisDb 對象的 dict 字典和 expires 字典,用于 flushdb  
  10.         lazyfreeFreeDatabaseFromBioThread(job->arg2,job->arg3);  
  11.     else if (job->arg3)  
  12.         // 釋放 Cluster 的 slots_to_keys 對象,請參見第 5.7 節(jié)  
  13.         lazyfreeFreeSlotsMapFromBioThread(job->arg3); 

可以看到,通過組合這三個(gè)參數(shù)可以實(shí)現(xiàn)不同結(jié)構(gòu)的釋放邏輯。

接下來我們繼續(xù)追蹤普通對象的異步刪除 lazyfreeFreeObjectFromBioThread 是如何進(jìn)行的,請仔細(xì)閱讀代碼注釋。 

  1. void lazyfreeFreeObjectFromBioThread(robj *o) {  
  2.     decrRefCount(o); // 降低對象的引用計(jì)數(shù),如果為零,就釋放  
  3.     atomicDecr(lazyfree_objects,1); // lazyfree_objects 為待釋放對象的數(shù)量,用于統(tǒng)計(jì)  
  4.  
  5. // 減少引用計(jì)數(shù)  
  6. void decrRefCount(robj *o) {  
  7.     if (o->refcount == 1) {  
  8.         // 該釋放對象了  
  9.         switch(o->type) {  
  10.         case OBJ_STRING: freeStringObject(o); break;  
  11.         case OBJ_LIST: freeListObject(o); break;  
  12.         case OBJ_SET: freeSetObject(o); break;  
  13.         case OBJ_ZSET: freeZsetObject(o); break;  
  14.         case OBJ_HASH: freeHashObject(o); break;  // 釋放 hash 對象,繼續(xù)追蹤  
  15.         case OBJ_MODULE: freeModuleObject(o); break;  
  16.         case OBJ_STREAM: freeStreamObject(o); break;  
  17.         default: serverPanic("Unknown object type"); break;  
  18.         } 
  19.         zfree(o);  
  20.     } else {  
  21.         if (o->refcount <= 0) serverPanic("decrRefCount against refcount <= 0");  
  22.         if (o->refcount != OBJ_SHARED_REFCOUNT) o->refcount--; // 引用計(jì)數(shù)減 1  
  23.     }  
  24.  
  25. // 釋放 hash 對象  
  26. void freeHashObject(robj *o) {  
  27.     switch (o->encoding) {  
  28.     case OBJ_ENCODING_HT:  
  29.         // 釋放字典,我們繼續(xù)追蹤  
  30.         dictRelease((dict*) o->ptr);  
  31.         break;  
  32.     case OBJ_ENCODING_ZIPLIST:  
  33.         // 如果是壓縮列表可以直接釋放  
  34.         // 因?yàn)閴嚎s列表是一整塊字節(jié)數(shù)組  
  35.         zfree(o->ptr);  
  36.         break;  
  37.     default:  
  38.         serverPanic("Unknown hash encoding type");  
  39.         break;  
  40.     }  
  41.  
  42. // 釋放字典,如果字典正在遷移中,ht[0] 和 ht[1] 分別存儲舊字典和新字典  
  43. void dictRelease(dict *d)  
  44.  
  45.     _dictClear(d,&d->ht[0],NULL); // 繼續(xù)追蹤  
  46.     _dictClear(d,&d->ht[1],NULL);  
  47.     zfree(d);  
  48.  
  49. // 這里要釋放 hashtable 了  
  50. // 需要遍歷第一維數(shù)組,然后繼續(xù)遍歷第二維鏈表,雙重循環(huán)  
  51. int _dictClear(dict *d, dictht *ht, void(callback)(void *)) {  
  52.     unsigned long i;  
  53.     /* Free all the elements */  
  54.     for (i = 0; i < ht->size && ht->used > 0; i++) {  
  55.         dictEntry *he, *nextHe;  
  56.         if (callback && (i & 65535) == 0) callback(d->privdata);  
  57.         if ((he = ht->table[i]) == NULL) continue;  
  58.         while(he) {  
  59.             nextHe = he->next;  
  60.             dictFreeKey(d, he); // 先釋放 key  
  61.             dictFreeVal(d, he); // 再釋放 value  
  62.             zfree(he); // 最后釋放 entry  
  63.             ht->used--;  
  64.             he = nextHe 
  65.         }  
  66.     }  
  67.     /* Free the table and the allocated cache structure */  
  68.     zfree(ht->table); // 可以回收第一維數(shù)組了  
  69.     /* Re-initialize the table */  
  70.     _dictReset(ht);  
  71.     return DICT_OK; /* never fails */  

這些代碼散落在多個(gè)不同的文件,我將它們湊到了一塊便于讀者閱讀。從代碼中我們可以看到釋放一個(gè)對象要深度調(diào)用一系列函數(shù),每種對象都有它獨(dú)特的內(nèi)存回收邏輯。

04.5.9.4 隊(duì)列安全

前面提到任務(wù)隊(duì)列是一個(gè)不安全的雙向鏈表,需要使用鎖來保護(hù)它。當(dāng)主線程將任務(wù)追加到隊(duì)列之前需要給它加鎖,追加完畢后,再釋放鎖,還需要喚醒異步線程——如果其在休眠的話。 

  1. void bioCreateBackgroundJob(int type, void *arg1, void *arg2, void *arg3) {  
  2.     struct bio_job *job = zmalloc(sizeof(*job));  
  3.     job->timetime = time(NULL);  
  4.     job->arg1arg1 = arg1;  
  5.     job->arg2arg2 = arg2;  
  6.     job->arg3arg3 = arg3;  
  7.     pthread_mutex_lock(&bio_mutex[type]); // 加鎖  
  8.     listAddNodeTail(bio_jobs[type],job); // 追加任務(wù)  
  9.     bio_pending[type]++; // 計(jì)數(shù)  
  10.     pthread_cond_signal(&bio_newjob_cond[type]); // 喚醒異步線程  
  11.     pthread_mutex_unlock(&bio_mutex[type]); // 釋放鎖  

異步線程需要對任務(wù)隊(duì)列進(jìn)行輪詢處理,依次從鏈表表頭摘取元素逐個(gè)處理。摘取元素的時(shí)候也需要加鎖,摘出來之后再解鎖。如果一個(gè)元素都沒有,它需要等待,直到主線程來喚醒它繼續(xù)工作。 

  1. // 異步線程執(zhí)行邏輯  
  2. void *bioProcessBackgroundJobs(void *arg) {  
  3. ...  
  4.     pthread_mutex_lock(&bio_mutex[type]); // 先加鎖  
  5.     ...  
  6.     // 循環(huán)處理  
  7.     while(1) {  
  8.         listNode *ln;  
  9.         /* The loop always starts with the lock hold. */  
  10.         if (listLength(bio_jobs[type]) == 0) {  
  11.             // 對列空,那就睡覺吧  
  12.             pthread_cond_wait(&bio_newjob_cond[type],&bio_mutex[type]);  
  13.             continue;  
  14.         }  
  15.         /* Pop the job from the queue. */  
  16.         ln = listFirst(bio_jobs[type]); // 獲取隊(duì)列頭元素  
  17.         job = ln->value;  
  18.         /* It is now possible to unlock the background system as we know have  
  19.          * a stand alone job structure to process.*/  
  20.         pthread_mutex_unlock(&bio_mutex[type]); // 釋放鎖  
  21.         // 這里是處理過程,為了省紙,就略去了  
  22.         ...  
  23.         // 釋放任務(wù)對象  
  24.         zfree(job);  
  25.         ...  
  26.         // 再次加鎖繼續(xù)處理下一個(gè)元素  
  27.         pthread_mutex_lock(&bio_mutex[type]);  
  28.         // 因?yàn)槿蝿?wù)已經(jīng)處理完了,可以放心從鏈表中刪除節(jié)點(diǎn)了  
  29.         listDelNode(bio_jobs[type],ln);  
  30.         bio_pending[type]--; // 計(jì)數(shù)減 1  
  31.     } 

研究完這些加鎖解鎖的代碼后,筆者開始有點(diǎn)擔(dān)心主線程的性能。我們都知道加鎖解鎖是一個(gè)相對比較耗時(shí)的操作,尤其是悲觀鎖最為耗時(shí)。如果刪除很頻繁,主線程豈不是要頻繁加鎖解鎖。

所以這里肯定還有優(yōu)化空間,Java 的 ConcurrentLinkQueue 就沒有使用這樣粗粒度的悲觀鎖,它優(yōu)先使用 cas 來控制并發(fā)。那就讓我們就期待 Redis 在未來的版本里對它進(jìn)一步改造優(yōu)化吧!

 

責(zé)任編輯:龐桂玉 來源: 程序人生
相關(guān)推薦

2015-09-24 16:48:17

數(shù)據(jù)中心云遷移

2013-01-06 10:40:30

網(wǎng)絡(luò)管理數(shù)據(jù)安全

2011-12-29 09:54:07

數(shù)據(jù)安全

2019-03-20 10:10:17

互聯(lián)網(wǎng)數(shù)據(jù)技術(shù)

2010-08-04 14:21:21

面試

2012-05-01 08:18:25

華為

2020-06-12 11:51:07

工控安全網(wǎng)絡(luò)安全網(wǎng)絡(luò)攻擊

2011-11-02 09:29:42

存儲虛擬化虛擬化

2017-01-10 15:22:34

京東容器集群

2021-04-13 17:17:08

線上故障交付

2021-08-18 09:38:51

人工智能AI機(jī)器學(xué)習(xí)

2018-02-26 13:12:20

人工智能

2014-06-05 09:23:47

程序員高效

2024-03-29 08:03:48

單元測試流量

2023-06-15 13:59:00

人工智能智能家居

2024-04-18 00:20:56

Redis策略數(shù)據(jù)

2016-10-09 19:49:30

ERP工具編程

2017-04-21 21:00:13

2012-04-04 22:17:40

移動游戲
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號