摘要:在執(zhí)行耗時(shí)命令如范圍掃描類的超大下的等瞬時(shí)大量過期驅(qū)逐等情況下,會(huì)造成的下降,阻塞其他請(qǐng)求。
本文主要從實(shí)現(xiàn)角度分析了redis lazy free特性的使用方法和注意事項(xiàng)
有幫助的話就點(diǎn)個(gè)贊,關(guān)注專欄數(shù)據(jù)庫,不跑路吧~~
不定期更新數(shù)據(jù)庫的小知識(shí)和實(shí)用經(jīng)驗(yàn),讓你不用再需要擔(dān)心跑路
眾所周知,redis對(duì)外提供的服務(wù)是由單線程支撐,通過事件(event)驅(qū)動(dòng)各種內(nèi)部邏輯,比如網(wǎng)絡(luò)IO、命令處理、過期key處理、超時(shí)等邏輯。在執(zhí)行耗時(shí)命令(如范圍掃描類的keys, 超大hash下的hgetall等)、瞬時(shí)大量key過期/驅(qū)逐等情況下,會(huì)造成redis的QPS下降,阻塞其他請(qǐng)求。近期就遇到過大容量并且大量key的場(chǎng)景,由于各種原因引發(fā)的redis內(nèi)存耗盡,導(dǎo)致有6位數(shù)的key幾乎同時(shí)被驅(qū)逐,短期內(nèi)redis hang住的情況
耗時(shí)命令是客戶端行為,服務(wù)端不可控,優(yōu)化余地有限,作者antirez在4.0這個(gè)大版本中增加了針對(duì)大量key過期/驅(qū)逐的lazy free功能,服務(wù)端的事情還是可控的,甚至提供了異步刪除的命令unlink(前因后果和作者的思路變遷,見作者博客:Lazy Redis is better Redis -
lazy free的功能在使用中有幾個(gè)注意事項(xiàng)(以下為個(gè)人觀點(diǎn),有誤的地方請(qǐng)?jiān)u論區(qū)交流):
lazy free不是在遇到快OOM的時(shí)候直接執(zhí)行命令,放后臺(tái)釋放內(nèi)存,而是也需要block一段時(shí)間去獲得足夠的內(nèi)存來執(zhí)行命令
lazy free不適合kv的平均大小太小或太大的場(chǎng)景,大小均衡的場(chǎng)景下性價(jià)比比較高(當(dāng)然,可以根據(jù)業(yè)務(wù)場(chǎng)景調(diào)整源碼里的宏,重新編譯一個(gè)版本)
redis短期內(nèi)其實(shí)是可以略微超出一點(diǎn)內(nèi)存上限的,因?yàn)榍耙粭l命令沒檢測(cè)到內(nèi)存超標(biāo)(其實(shí)快超了)的情況下,是可以寫入一個(gè)很大的kv的,當(dāng)后續(xù)命令進(jìn)來之后會(huì)發(fā)現(xiàn)內(nèi)存不夠了,交給后續(xù)命令執(zhí)行釋放內(nèi)存操作
如果業(yè)務(wù)能預(yù)估到可能會(huì)有集中的大量key過期,那么最好ttl上加個(gè)隨機(jī)數(shù),勻開來,避免集中expire造成的blocking,這點(diǎn)不管開不開lazy free都一樣
具體分析請(qǐng)見下文
參數(shù)redis 4.0新加了4個(gè)參數(shù),用來控制這種lazy free的行為
lazyfree-lazy-eviction:是否異步驅(qū)逐key,當(dāng)內(nèi)存達(dá)到上限,分配失敗后
lazyfree-lazy-expire:是否異步進(jìn)行key過期事件的處理
lazyfree-lazy-server-del:del命令是否異步執(zhí)行刪除操作,類似unlink
replica-lazy-flush:replica client做全同步的時(shí)候,是否異步flush本地db
以上參數(shù)默認(rèn)都是no,按需開啟,下面以lazyfree-lazy-eviction為例,看看redis怎么處理lazy free邏輯,其他參數(shù)的邏輯類似
源碼分析 命令處理邏輯int processCommand(client *c)是redis處理命令的主方法,在真正執(zhí)行命令前,會(huì)有各種檢查,包括對(duì)OOM情況下的處理
int processCommand(client *c) { // ... if (server.maxmemory && !server.lua_timedout) { // 設(shè)置了maxmemory時(shí),如果有必要,嘗試釋放內(nèi)存(evict) int out_of_memory = freeMemoryIfNeededAndSafe() == C_ERR; // ... // 如果釋放內(nèi)存失敗,并且當(dāng)前將要執(zhí)行的命令不允許OOM(一般是寫入類命令) if (out_of_memory && (c->cmd->flags & CMD_DENYOOM || (c->flags & CLIENT_MULTI && c->cmd->proc != execCommand))) { flagTransaction(c); // 向客戶端返回OOM addReply(c, shared.oomerr); return C_OK; } } // ... /* Exec the command */ if (c->flags & CLIENT_MULTI && c->cmd->proc != execCommand && c->cmd->proc != discardCommand && c->cmd->proc != multiCommand && c->cmd->proc != watchCommand) { queueMultiCommand(c); addReply(c,shared.queued); } else { call(c,CMD_CALL_FULL); c->woff = server.master_repl_offset; if (listLength(server.ready_keys)) handleClientsBlockedOnKeys(); } return C_OK;內(nèi)存釋放(淘汰)邏輯
內(nèi)存的釋放主要在freeMemoryIfNeededAndSafe()內(nèi)進(jìn)行,如果釋放不成功,會(huì)返回C_ERR。freeMemoryIfNeededAndSafe()包裝了底下的實(shí)現(xiàn)函數(shù)freeMemoryIfNeeded()
int freeMemoryIfNeeded(void) { // slave不管OOM的情況 if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK; // ... // 獲取內(nèi)存用量狀態(tài),如果夠用,直接返回ok // 如果不夠用,這個(gè)方法會(huì)返回總共用了多少內(nèi)存mem_reported,至少需要釋放多少內(nèi)存mem_tofree // 這個(gè)方法很有意思,暗示了其實(shí)redis是可以用超內(nèi)存的。即,在當(dāng)前這個(gè)方法調(diào)用后,判斷內(nèi)存足夠,但是寫入了一個(gè)很大的kv,等下一個(gè)倒霉蛋來請(qǐng)求的時(shí)候發(fā)現(xiàn),內(nèi)存不夠了,這時(shí)候才會(huì)在下一次請(qǐng)求時(shí)觸發(fā)清理邏輯 if (getMaxmemoryState(&mem_reported,NULL,&mem_tofree,NULL) == C_OK) return C_OK; // 用來記錄本次調(diào)用釋放了多少內(nèi)存的變量 mem_freed = 0; // 不需要evict的策略下,直接跳到釋放失敗的邏輯 if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION) goto cant_free; /* We need to free memory, but policy forbids. */ // 循環(huán),嘗試釋放足夠大的內(nèi)存 // 同步釋放的情況下,如果要?jiǎng)h除的對(duì)象很多,或者是很大的hash/set/zset等,需要反復(fù)循環(huán)多次 // 所以一般在監(jiān)控里看到有大量key evict的時(shí)候,會(huì)跟著看到QPS下降,RTT上升 while (mem_freed < mem_tofree) { // 根據(jù)配置的maxmemory-policy,拿到一個(gè)可以釋放掉的bestkey // 中間邏輯比較多,可以再開一篇,先略過了 if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) || server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) { // 帶LRU/LFU/TTL的策略 // ... } else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM || server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM) { // 帶random的策略 // ... } // 最終選中了一個(gè)bestkey if (bestkey) { if (server.lazyfree_lazy_eviction) // 如果配置了lazy free,嘗試異步刪除(不一定異步,相見下文) dbAsyncDelete(db,keyobj); else dbSyncDelete(db,keyobj); // ... // 如果是異步刪除,需要在循環(huán)過程中定期評(píng)估后臺(tái)清理線程是否釋放了足夠的內(nèi)存,默認(rèn)每16次循環(huán)檢查一次 // 可以想到的是,如果kv都很小,那么前面的操作并不是異步,lazy free不生效。如果kv都很大,那么幾乎所有kv都走異步清理,主線程接近空轉(zhuǎn),如果清理線程不夠,那么還是會(huì)話相對(duì)長(zhǎng)的時(shí)間的。所以應(yīng)該是大小混合的場(chǎng)景比較合適lazy free,需要實(shí)驗(yàn)數(shù)據(jù)驗(yàn)證 if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) { if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) { // 如果釋放了足夠內(nèi)存,那么可以直接跳出循環(huán)了 mem_freed = mem_tofree; } } } } cant_free: // 無法釋放內(nèi)存時(shí),做個(gè)好人,本次請(qǐng)求卡就卡吧,檢查一下后臺(tái)清理線程是否還有任務(wù)正在清理,等他清理出足夠內(nèi)存之后再退出 while(bioPendingJobsOfType(BIO_LAZY_FREE)) { if (((mem_reported - zmalloc_used_memory()) + mem_freed) >= mem_tofree) // 這里有點(diǎn)疑問,如果已經(jīng)能等到足夠的內(nèi)存被釋放,為什么不直接返回C_OK??? break; usleep(1000); } return C_ERR; }異步刪除邏輯
// 用來評(píng)估是否需要異步刪除的閾值 #define LAZYFREE_THRESHOLD 64 int dbAsyncDelete(redisDb *db, robj *key) { // 先從expire字典中刪了這個(gè)entry(釋放expire字典的entry內(nèi)存,因?yàn)楹竺嬗貌坏剑粫?huì)釋放key/value本身內(nèi)存 if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr); // 從db的key space中摘掉這個(gè)entry,但是不釋放entry/key/value的內(nèi)存 dictEntry *de = dictUnlink(db->dict,key->ptr); if (de) { robj *val = dictGetVal(de); // 評(píng)估要?jiǎng)h除的代價(jià) // 默認(rèn)1 // list對(duì)象,取其長(zhǎng)度 // 以hash格式存儲(chǔ)的set/hash對(duì)象,取其元素個(gè)數(shù) // 跳表存儲(chǔ)的zset,取跳表長(zhǎng)度 size_t free_effort = lazyfreeGetFreeEffort(val); // 如果代價(jià)大于閾值,扔給后臺(tái)線程刪除 if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) { atomicIncr(lazyfree_objects,1); bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL); dictSetVal(db->dict,de,NULL); } // 釋放entry內(nèi)存 } }總結(jié)
感覺redis可以考慮一個(gè)功能,給一個(gè)參數(shù)配置內(nèi)存高水位,超過高水位之后就可以觸發(fā)evict操作。但是有個(gè)問題,可能清理速度趕不上寫入速度,怎么合理平衡這兩者需要仔細(xì)想一下。
另外感嘆一下antirez代碼層面上的架構(gòu)能力,幾年前看過redis 2.8的代碼,從2.8的分支直接切到5.0之后,原來閱讀的位置并沒有偏離主線太遠(yuǎn)。歷經(jīng)幾個(gè)大版本的迭代,加了N多功能之后,代碼主體邏輯依舊沒有大改,真的是做到了對(duì)修改關(guān)閉,對(duì)擴(kuò)展開放。向大佬學(xué)習(xí)
有幫助的話就點(diǎn)個(gè)贊,關(guān)注專欄數(shù)據(jù)庫,不跑路吧~~
不定期更新數(shù)據(jù)庫的小知識(shí)和實(shí)用經(jīng)驗(yàn),讓你不用再需要擔(dān)心跑路
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/62102.html
摘要:在執(zhí)行耗時(shí)命令如范圍掃描類的超大下的等瞬時(shí)大量過期驅(qū)逐等情況下,會(huì)造成的下降,阻塞其他請(qǐng)求。 本文主要從實(shí)現(xiàn)角度分析了redis lazy free特性的使用方法和注意事項(xiàng) 有幫助的話就點(diǎn)個(gè)贊,關(guān)注專欄數(shù)據(jù)庫,不跑路吧~~不定期更新數(shù)據(jù)庫的小知識(shí)和實(shí)用經(jīng)驗(yàn),讓你不用再需要擔(dān)心跑路 眾所周知,redis對(duì)外提供的服務(wù)是由單線程支撐,通過事件(event)驅(qū)動(dòng)各種內(nèi)部邏輯,比如網(wǎng)絡(luò)IO、...
閱讀 3532·2023-04-25 20:09
閱讀 3736·2022-06-28 19:00
閱讀 3056·2022-06-28 19:00
閱讀 3075·2022-06-28 19:00
閱讀 3168·2022-06-28 19:00
閱讀 2874·2022-06-28 19:00
閱讀 3038·2022-06-28 19:00
閱讀 2632·2022-06-28 19:00