作者 | KL博主
背景
摸清 Redis 的數(shù)據(jù)清理策略,給內存使用高的被動緩存場景,在遇到內存不足時,怎么做是最優(yōu)解提供決策依據(jù)。?
本文整理 Redis 的數(shù)據(jù)清理策略所有代碼來自 Redis version :5.0, 不同版本的 Redis 策略可能有調整
清理策略
Redis 的清理策略,總結概括為三點,被動清理、定時清理、驅逐清理
被動清理
訪問 Key 時,每次都會檢查該 Key 是否已過期,如果過期則刪除該 Key ,get 、scan 等指令都會觸發(fā) Key 的過期檢查。
關鍵代碼如下, expireIfNeeded (redisDb *db, robj *key) 函數(shù)會觸發(fā)檢查并刪除
robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) { robj *val; if (expireIfNeeded(db,key) == 1) { /* Key expired. If we are in the context of a master, expireIfNeeded() * returns 0 only when the key does not exist at all, so it's safe * to return NULL ASAP. */ if (server.masterhost == NULL) { server.stat_keyspace_misses++; return NULL; } if (server.current_client && server.current_client != server.master && server.current_client->cmd && server.current_client->cmd->flags & CMD_READONLY) { server.stat_keyspace_misses++; return NULL; } } val = lookupKey(db,key,flags); if (val == NULL) server.stat_keyspace_misses++; else server.stat_keyspace_hits++; return val; }
定時清理
通過 serverCron 定期觸發(fā)清理,可以通過 hz 參數(shù),配置每秒執(zhí)行多少次清理任務,流程如下
1、Redis 配置項 hz 定義了 serverCron 任務的執(zhí)行周期,默認為 10,即 CPU 空閑時每秒執(zhí)行 10 次
2、每次過期 Key 清理的 timelimit 不超過 CPU 時間的 25% ,即若 hz = 1,則一次清理時間最大為 250ms,若 hz = 10,則一次清理時間最大為 25ms,計算邏輯(timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;)
3、清理時依次遍歷所有的 db;
4、從 db 中隨機取 20 個 key,判斷是否過期,若過期,則逐出;
5、若有 5 個以上 key 過期,則重復步驟 4,否則遍歷下一個 db;
6、在清理過程中,若達到了 timelimit 時間,退出清理過程;
關鍵代碼如下,activeExpireCycle (int type) 會執(zhí)行上述邏輯
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { ... databasesCron(); ... } void databasesCron(void) { /* Expire keys by random sampling. Not required for slaves * as master will synthesize DELs for us. */ if (server.active_expire_enabled) { if (server.masterhost == NULL) { activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW); } else { expireSlaveKeys(); } } ... }ps: activeExpireCycle 還會在主事件循環(huán) eventLoop 中被調用,此時 type = ACTIVE_EXPIRE_CYCLE_FAST, 控制了最多執(zhí)行 timelimit = 1000us 的快速清理,也會刪除部分 Key 。?
驅逐清理
Redis 在命令處理函數(shù) processCommand 會進行內存的檢查和驅逐,任何命令都會出觸發(fā),包括 ping 命令。
如果配置了 maxmemory ,且當前內存超過 maxmemory 時,則會執(zhí)行 maxmemory_policy 篩選出需要清理的 Key,繼而判斷 lazyfree-lazy-eviction 是否開啟來進行 Key 的同步還是異步刪除。無論是同步刪除還是異步刪除,最后都會繼續(xù)校驗內存是否超限,直到內存低于 maxmemory。驅逐只會在 Master 節(jié)點進行。
maxmemory_policy 可選如下:
volatile-lru:從已設置過期時間的數(shù)據(jù)集中挑選【最近最少使用】的 Key 進行刪除
volatile-ttl:從己設置過期時間的數(shù)據(jù)集中挑選【將要過期】的 Key 進行刪除
volatile-lfu:從己設置過期時間的數(shù)據(jù)集中選擇【最不常用】的 Key 進行刪除
volatile-random:從己設置過期時間的數(shù)據(jù)集中【任意選擇】Key 進行刪除
allkeys-lru:從數(shù)據(jù)集中挑選【最近最少使用】的 Key 進行刪除
allkeys-lfu:從數(shù)據(jù)集中【優(yōu)先刪除掉最不常用】的 Key
allkeys-random:從數(shù)據(jù)集中【任意選擇】 Key 進行刪除
no-enviction:禁止驅逐數(shù)據(jù)
如上圖,6.2 后的版本支持通過逐出因子 maxmemory-eviction-tenacity 來控制逐出阻塞的時間。具體的阻塞耗時間可以通過 latency-monitor 里的?eviction-cycle、eviction-del?來觀測。 關鍵代碼如下,freeMemoryIfNeeded () 函數(shù)會執(zhí)行上述邏輯
int processCommand(client *c) { ... if (server.maxmemory && !server.lua_timedout) { int out_of_memory = freeMemoryIfNeededAndSafe() == C_ERR; ... } ... } int freeMemoryIfNeededAndSafe(void) { if (server.lua_timedout || server.loading) return C_OK; return freeMemoryIfNeeded(); }
ps: 當觸發(fā) aof 文件重寫,fork 操作會阻塞主進程,此時積壓的指令需要的內存,在 fork 結束后,需要一次性 eviction 出來,這時的 eviction-cycle 耗時會惡化的很嚴重,達到秒級的阻塞,此時可通過 latency-monitor 觀測 eviction-cycle 、fork 總是成對出現(xiàn)。
總結
回到開篇的背景問題,當遇到內存使用高的被動緩存場景,可用內存不足時:
離線分析內存,是否存在大量【已過期】的內存來不及定時清理,此時可調大 hz 參數(shù)來加速過期內存的主動清理。hz 參數(shù)最大 500 ,不過要觀察 CPU 的影響,不要因為 hz 影響讀寫流量
如果調整 hz 還是沒法及時清理已過期的內存,則可以使用 scan 指令來被動訪問 key 的方式手動刪除,注意執(zhí)行 scan 時的 count ,同時觀測 CPU 使用情況,scan 的 count 越大,CPU 消耗會越高,完成一次 sacn 刪除的時間最快。為了減少對線上的影響,可以在業(yè)務低峰期,周期性的執(zhí)行。
通過 latency-monitor 觀測 eviction-cycle、eviction-del 指標,是否因內存驅逐阻塞嚴重,可開啟 lazyfree-lazy-eviction 來緩解阻塞。
業(yè)務上可以考慮關閉 aof 的影響,關閉 aof 可以減少驅逐清理 eviction-cycle 延遲帶來的讀寫超時影響。
可升級到 7.x 版本的 Redis ,通過 maxmemory-eviction-tenacity 參數(shù)主動控制每次驅逐的阻塞時間
如果還是很慢,可考慮升級內存規(guī)格
編輯:黃飛
?
評論
查看更多