Redis 6.2和7渐进式逐出优化
一、Redis 逐出功能简介
网上关于Redis逐出功能和算法的文章比较多,这里就不浪费篇幅介绍,本文旨在介绍Redis 6.2以后一个Redis逐出上的重大优化(渐进式逐出),顺带整理下Redis在各个版本上逐出功能上的优化。
Redis有一个maxmemory配置,每次执行命令时如果发现当前Redis使用内存超过maxmemory时,会使用相应的策略(包含不逐出也是一种策略)把key进行逐出,直到Redis使用内存小于maxmemory,这句话包含了几个重要信息,我们分别来看下。
1.每次执行命令时...直到Redis使用内存小于maxmemory
说明这个逐出检测是一个同步过程,如果逐出花费大量时间,Redis会长期不可用,甚至会触发切换。
2. 相应的策略
策略包含如下几种,可根据相应场景进行测试,默认是NO_EVICTION
MAXMEMORY_VOLATILE_LRU
MAXMEMORY_VOLATILE_LFU
MAXMEMORY_VOLATILE_TTL
MAXMEMORY_VOLATILE_RANDOM
MAXMEMORY_ALLKEYS_LRU
MAXMEMORY_ALLKEYS_LFU
MAXMEMORY_ALLKEYS_RANDOM
MAXMEMORY_NO_EVICTION
3. Redis使用内存
这个使用内存可能会产生歧义,造成不符合预期的事件发生,使用内存定义
使用内存 = used_memory - AOF缓冲区 - slave缓冲区
所以如果normal客户端缓冲区突增(例如持续执行很大的命令,例如lrange 大list),可能也会造成突发逐出。
4. 重要点总结
- 同步逐出: 如果逐出量很大(例如百万级别),可能严重影响可用性
- used_memory参考上图。
- “模拟的”LRU、LFU:可以看下代码,LFU和LRU不是真正的(真正的成本太高了),是用一个队列模拟的。
二、Redis 6.2&7+ 逐出实验
1. 版本选择
- Redis 6.0.15
- Redis 7.0.11
2. 实验条件
- maxmemory = 7.5 GB
- 第一次灌入:70,000,000个key(为了越过 67108864 界限),观察内存
- 第二次灌入:1,000,000个key,观察可用性和逐出 + 800MB string (40个20MB)
./redis-cli debug populate 40 user 20971520
3. 开始实验
(1) 第一次灌入:7000w个key
可以看到两个版本内存消耗几乎一致,并且在整个导入过程中未发生异常
(2) 第二次灌入:1,000,000个key + 800MB string (40个20MB),观察可用性和逐出
客户端异常 | redis-cli latency | 耗时 | |
6.0.15 | 持续发生 | min: 29, max: 347 | 52500ms |
7.0.11 | 未发生 | min: 0, max: 25330 | 51233ms |
可以看到Redis 7.0.11在同等状况下,可用性表现良好,下面来看下相关原理
三、同步逐出与渐进式逐出(since 6.2+)
1. 同步逐出
文章开头已经提到:每次执行命令时如果发现当前使用内存超过maxmemory时,会使用相应的策略(包含不逐出也是一种策略)把key进行逐出,直到Redis使用内存小于maxmemory
2. 渐进式逐出
(1) evictionTimer运行时间超过eviction_time_limit_us后,会开启evict时间事件并退出逐出,这样将逐出循环异步化并保证可用性。
//开始计时
elapsedStart(&evictionTimer);
/* Finally remove the selected key. */
if (bestkey) {
db = server.db+bestdbid;
robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
delta = (long long) zmalloc_used_memory();
latencyStartMonitor(eviction_latency);
if (server.lazyfree_lazy_eviction)
dbAsyncDelete(db,keyobj);
else
dbSyncDelete(db,keyobj);
delta -= (long long) zmalloc_used_memory();
mem_freed += delta;
keys_freed++;
if (keys_freed % 16 == 0) {
if (slaves) flushSlavesOutputBuffers();
if (server.lazyfree_lazy_eviction) {
if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) {
break;
}
}
/* After some time, exit the loop early - even if memory limit
* hasn't been reached. If we suddenly need to free a lot of
* memory, don't want to spend too much time here. */
if (elapsedUs(evictionTimer) > eviction_time_limit_us) {
// We still need to free memory - start eviction timer proc
startEvictionTimeProc();
break;
}
}
}
(2) eviction_time_limit_us计算方法:
可以看到新增maxmemory_eviction_tenacity参数用来控制超时:
- 当maxmemory_eviction_tenacity<=10,超时时间等50微秒 * maxmemory_eviction_tenacity
- 当maxmemory_eviction_tenacity>10 and maxmemory_eviction_tenacity<100,超时时间等于500 * 1.15^(maxmemory_eviction_tenacity - 10.0),该值最大为2分钟
- 当maxmemory_eviction_tenacity = 100,代表没有超时时间,和同步逐出一致。
static unsigned long evictionTimeLimitUs() {
serverAssert(server.maxmemory_eviction_tenacity >= 0);
serverAssert(server.maxmemory_eviction_tenacity <= 100);
if (server.maxmemory_eviction_tenacity <= 10) {
/* A linear progression from 0..500us */
return 50uL * server.maxmemory_eviction_tenacity;
}
if (server.maxmemory_eviction_tenacity < 100) {
/* A 15% geometric progression, resulting in a limit of ~2 min at tenacity==99 */
return (unsigned long)(500.0 * pow(1.15, server.maxmemory_eviction_tenacity - 10.0));
}
return ULONG_MAX; /* No limit to eviction time */
}
(3) 用isEvictionProcRunning控制逐出时间事件
static int isEvictionProcRunning = 0;
static int evictionTimeProc(struct aeEventLoop *eventLoop, long long id, void *clientData) {
if (performEvictions() == EVICT_RUNNING) return 0; /* keep evicting */
/* For EVICT_OK - things are good, no need to keep evicting.
* For EVICT_FAIL - there is nothing left to evict. */
isEvictionProcRunning = 0;
return AE_NOMORE;
}
void startEvictionTimeProc(void) {
if (!isEvictionProcRunning) {
isEvictionProcRunning = 1;
aeCreateTimeEvent(server.el, 0, evictionTimeProc, NULL, NULL);
}
}
3. 风险讨论?
* A new incremental eviction mechanism that reduces latency on eviction spikes (#7653)
In pathological cases this can cause memory to grow uncontrolled and may require
specific tuning.
可以看到不同的maxmemory_eviction_tenacity值,逐出的粒度不尽相同:
- 过低:可用性可以保证,但内存可能会持续增加(生产大于逐出),需要通过监控观测。
- 过高:可用性可能有问题,但内存可以得到控制。
四、历届版本的重要优化
下图展示了Redis在各个版本evict的相关优化和功能(如有漏掉,欢迎指正)
1. Redis 3->Redis 4
- Redis 4引入了LRU算法:详见http://antirez.com/news/109
- Volatile-ttl和LFU、LRU一样使用了evict pool(一个采样的LFU、LFU、TTL池子进行排列)来实现。
- 逐出支持异步删除:lazyfree-lazy-eviction
2. Redis 4->Redis 5
- aof加载期间不执行evict逻辑
- keys命令不触发evict逻辑
3. Redis 5->Redis 6
- 记录lazyfree evcit的latency
- evict支持tracking
4. Redis 6->Redis 6.2、7.0
- 同步逐出改为渐进式逐出 (6.2)
- 添加新的info:total_eviction_exceeded_time and current_eviction_exceeded_time (7.0)
五、结论
- Redis 7可以放心使用,内存、性能、可用性都有一定提升。
- maxmemory_eviction_tenacity默认是10,可以动态设置,maxmemory_eviction_tenacity=100的话可能存在bug(感谢仲肥大佬(Zhao Zhao)提示)
- Redis 6.2需要大于6.2.8
- Redis 7.0需要大于7.0.5
* Fix a hang when eviction is combined with lazy-free and maxmemory-eviction-tenacity
is set to 100 (#11237)
文章转载自公众号:Redis开发运维实战