本文主要從實現角度分析了redis lazy free特性的使用方法和注意事項redis
有幫助的話就點個贊,關注專欄數據庫,不跑路吧~~
不按期更新數據庫的小知識和實用經驗,讓你不用再須要擔憂跑路數據庫
衆所周知,redis對外提供的服務是由單線程支撐,經過事件(event)驅動各類內部邏輯,好比網絡IO、命令處理、過時key處理、超時等邏輯。在執行耗時命令(如範圍掃描類的keys, 超大hash下的hgetall等)、瞬時大量key過時/驅逐等狀況下,會形成redis的QPS降低,阻塞其餘請求。近期就遇到過大容量而且大量key的場景,因爲各類緣由引起的redis內存耗盡,致使有6位數的key幾乎同時被驅逐,短時間內redis hang住的狀況segmentfault
耗時命令是客戶端行爲,服務端不可控,優化餘地有限,做者antirez在4.0這個大版本中增長了針對大量key過時/驅逐的lazy free功能,服務端的事情仍是可控的,甚至提供了異步刪除的命令unlink(來龍去脈和做者的思路變遷,見做者博客:Lazy Redis is better Redis - <antirez>)網絡
lazy free的功能在使用中有幾個注意事項(如下爲我的觀點,有誤的地方請評論區交流):架構
具體分析請見下文dom
redis 4.0新加了4個參數,用來控制這種lazy free的行爲異步
以上參數默認都是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多功能以後,代碼主體邏輯依舊沒有大改,真的是作到了對修改關閉,對擴展開放。向大佬學習
有幫助的話就點個贊,關注專欄數據庫,不跑路吧~~
不按期更新數據庫的小知識和實用經驗,讓你不用再須要擔憂跑路