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