Redis 6.2和7渐进式逐出优化

ilikevc
发布于 2023-12-4 14:56
浏览
0收藏

一、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缓冲区

Redis 6.2和7渐进式逐出优化-鸿蒙开发者社区

所以如果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

可以看到两个版本内存消耗几乎一致,并且在整个导入过程中未发生异常

Redis 6.2和7渐进式逐出优化-鸿蒙开发者社区

(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. 渐进式逐出

Redis 6.2和7渐进式逐出优化-鸿蒙开发者社区

(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的相关优化和功能(如有漏掉,欢迎指正)

Redis 6.2和7渐进式逐出优化-鸿蒙开发者社区

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)

五、结论

  1. Redis 7可以放心使用,内存、性能、可用性都有一定提升。
  2. 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开发运维实战

分类
标签
已于2023-12-4 14:56:09修改
收藏
回复
举报
回复
    相关推荐