400-035-6699
当前位置: 首页 » 技术支持 » 博文资讯 »

Redis内存淘汰策略详解

Redis 是一款广泛使用的内存数据库,其高效的数据处理能力离不开精细的数据清理策略。本文将探讨 Redis 如何在内存使用高的场景下,通过三种清理策略来优化内存管理。
首先,Redis 的被动清理策略是每次访问 Key 时都会检查其是否已过期。如果 Key 过期,它会立即被删除。这一过程由 `expireIfNeeded` 函数触发,无论是在执行 `get`、`scan` 等指令时都会进行检查。
其次,定时清理策略通过 `serverCron` 函数定期执行。这个函数可以配置每秒执行清理任务的次数,通过 `hz` 参数进行设置。每次清理的时间限制不超过 CPU 时间的 25%,以避免过多的性能开销。定时清理过程中,Redis 会遍历所有数据库,随机选取 20 个 Key 进行过期检查,如果发现有超过 5 个 Key 过期,则会继续清理,直到检查下一个数据库。
除此之外,Redis 还有一种驱逐清理策略。当配置了 `maxmemory` 并且当前内存使用超过这个限制时,Redis 会根据 `maxmemory_policy` 来选择驱逐哪些 Key。这些策略包括从过期键中选择最近最少使用的、即将过期的、最不常用的,或者是任意选择。驱逐过程只会发生在 Master 节点,并且可以通过 `lazyfree-lazy-eviction` 参数来控制 Key 的同步或异步删除。
内存驱逐的效率和影响可以通过 `maxmemory-eviction-tenacity` 参数进行调节,以减少内存驱逐过程中的阻塞时间。此外,Redis 6.2 版本后还支持 `latency-monitor` 来监控 `eviction-cycle` 和 `eviction-del` 指标,帮助识别和优化内存驱逐的性能瓶颈。
在内存使用高的情况下,如果遇到可用内存不足的问题,可以采取以下措施:
1. 离线分析内存使用情况,如果存在大量已过期的内存未及时清理,可以考虑调大 `hz` 参数来加速过期内存的清理。但要注意,过大的 `hz` 值可能会影响 CPU 性能。
2. 如果调整 `hz` 参数仍然无法及时清理内存,可以使用 `scan` 指令手动删除 Key。在使用 `scan` 时,应适当设置 `count` 参数,并密切监控 CPU 使用情况。
3. 通过 `latency-monitor` 观察内存驱逐的性能指标,如果发现内存驱逐阻塞严重,可以考虑开启 `lazyfree-lazy-eviction` 来缓解。
4. 在业务允许的情况下,可以关闭 AOF(Append Only File)功能,以减少因内存驱逐导致的读写超时问题。
5. 升级到 Redis 7.x 版本,利用 `maxmemory-eviction-tenacity` 参数来主动控制每次驱逐的阻塞时间。
6. 如果以上方法都无法解决问题,可能需要考虑升级服务器的内存规格。
总之,通过合理配置和优化 Redis 的数据清理策略,可以在确保数据高效处理的同时,有效管理内存使用。

作者 | KL博主

Redis内存淘汰策略详解

背景

摸清 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:禁止驱逐数据

5a46b70e-5699-11ee-939d-92fbcf53809c.png

如上图,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 总是成对出现。

5a523aca-5699-11ee-939d-92fbcf53809c.png

总结

回到开篇的背景问题,当遇到内存使用高的被动缓存场景,可用内存不足时:

离线分析内存,是否存在大量【已过期】的内存来不及定时清理,此时可调大 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 参数主动控制每次驱逐的阻塞时间

如果还是很慢,可考虑升级内存规格

编辑:黄飞

 

【限时免费】一键获取网络规划系统模板+传输架构设计+连通性评估方案

相关文章

服务电话:
400-035-6699
企服商城