背景
摸清 Redis 的数据清理策略,给内存使用高的被动缓存场景,在遇到内存不足时
怎么做是最优解提供决策依据。
本文整理 Redis 的数据清理策略所有代码来自 Redis version : 5.0, 不同版本的 Redis 策略可能有调整
清理策略
Redis 的清理策略,总结概括为三点,被动清理、定时清理、驱逐清理
被动清理
访问 Key 时,每次都会检查该 Key 是否已过期,如果过期则删除该 Key ,get 、scan 等指令都会触发 Key 的过期检查。
关键代码如下, expireIfNeeded (redisDb db, robj key) 函数会触发检查并删除
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 定期触发清理,可以通过 hz 参数,配置每秒执行多少次清理任务,流程如下
1、Redis 配置项 hz 定义了 serverCron 任务的执行周期,默认为 10,即 CPU 空闲时每秒执行 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) 会执行上述逻辑
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 还会在主事件循环 eventLoop 中被调用,此时 type = ACTIVE_EXPIRE_CYCLE_FAST, 控制了最多执行 timelimit = 1000us 的快速清理,也会删除部分 Key 。
驱逐清理
Redis 在命令处理函数 processCommand 会进行内存的检查和驱逐,任何命令都会出触发,包括 ping 命令。
如果配置了 maxmemory ,且当前内存超过 maxmemory 时,则会执行 maxmemory_policy 筛选出需要清理的 Key,继而判断 lazyfree-lazy-eviction 是否开启来进行 Key 的同步还是异步删除。无论是同步删除还是异步删除,最后都会继续校验内存是否超限,直到内存低于 maxmemory。驱逐只会在 Master 节点进行。
maxmemory_policy 可选如下:
volatile-lru:从已设置过期时间的数据集中挑选【最近最少使用】的 Key 进行删除
volatile-ttl:从己设置过期时间的数据集中挑选【将要过期】的 Key 进行删除
volatile-lfu:从己设置过期时间的数据集中选择【最不常用】的 Key 进行删除
volatile-random:从己设置过期时间的数据集中【任意选择】Key 进行删除
allkeys-lru:从数据集中挑选【最近最少使用】的 Key 进行删除
allkeys-lfu:从数据集中【优先删除掉最不常用】的 Key
allkeys-random:从数据集中【任意选择】 Key 进行删除
no-enviction:禁止驱逐数据
如上图,6.2 后的版本支持通过逐出因子 maxmemory-eviction-tenacity 来控制逐出阻塞的时间。具体的阻塞耗时间可以通过 latency-monitor 里的 eviction-cycle、eviction-del 来观测。
关键代码如下,freeMemoryIfNeeded () 函数会执行上述逻辑
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: 当触发 aof 文件重写,fork 操作会阻塞主进程,此时积压的指令需要的内存,在 fork 结束后,需要一次性 eviction 出来,这时的 eviction-cycle 耗时会恶化的很严重,达到秒级的阻塞,此时可通过 latency-monitor 观测 eviction-cycle 、fork 总是成对出现。
总结
回到开篇的背景问题,当遇到内存使用高的被动缓存场景,可用内存不足时:
离线分析内存,是否存在大量【已过期】的内存来不及定时清理,此时可调大 hz 参数来加速过期内存的主动清理。hz 参数最大 500 ,不过要观察 CPU 的影响,不要因为 hz 影响读写流量
如果调整 hz 还是没法及时清理已过期的内存,则可以使用 scan 指令来被动访问 key 的方式手动删除,注意执行 scan 时的 count ,同时观测 CPU 使用情况,scan 的 count 越大,CPU 消耗会越高,完成一次 sacn 删除的时间最快。为了减少对线上的影响,可以在业务低峰期,周期性的执行。
通过 latency-monitor 观测 eviction-cycle、eviction-del 指标,是否因内存驱逐阻塞严重,可开启 lazyfree-lazy-eviction 来缓解阻塞。
业务上可以考虑关闭 aof 的影响,关闭 aof 可以减少驱逐清理 eviction-cycle 延迟带来的读写超时影响。
可升级到 7.x 版本的 Redis ,通过 maxmemory-eviction-tenacity 参数主动控制每次驱逐的阻塞时间
如果还是很慢,可考虑升级内存规格