内存管理

8.2.内存管理

        redis主要是通过控制内存上限和相应的回收策略实现内存管理,本节将围绕这两个方面来介绍Redis如何管理内存。

目前成都创新互联公司已为近千家的企业提供了网站建设、域名、虚拟空间、网站托管、服务器租用、企业网站设计、南充网站维护等服务,公司将坚持客户导向、应用为本的策略,正道将秉承"和谐、参与、激情"的文化,与客户和合作伙伴齐心协力一起成长,共同发展。

8.2.1 内存上限

        Redis使用maxmemory参数限制最大可用内存。限制内存的目的主要有:

  • 用于缓存场景,当超出内存上限maxmemory时使用LRU等删除策略释放空间。

  • 防止所用内存超过服务器物理内存。

        需要注意maxmemory限制的是Redis内存实际使用的内存量,也就是used_memory统计项对应的内存。由于内存碎片率的存在,实际消耗的内存可能会比maxmemory设置的更大,实际使用时要小心这部分内存溢出。通过内存上限可以非常方便的实现一台服务器部署多个Redis进程的内存控制。比如一台24GB内存的服务器,为系统预留4GB内存,预留4GB空闲内存给其他进程或Redis fork进程,留给Redis 16GB内存,这样可以部署4个maxmemory=4GB的Redis进程。得益于Redis单线程架构和内存限制机制,即使没有采用虚拟化不同的Redis进程之间也可以很好的实现CPU和内存的隔离性,如图8-2所示。

内存管理

图8-2:服务器分配4个4GB的Redis进程

8.2.2 动态调整内存上限

        Redis的内存上限可以通过config set maxmemory {bytes} 动态修改最大可用内存。例如之前的示例,当发现Redis-1没有做好内存预估实际只用了不到2GB内存,而Redis-2进程需要扩容到6GB内存才够用,这时可以分别执行如下命令调整:

Redis-1>config set maxmemory 2GB
Redis-2>config set maxmemory 6GB

        通过动态修改maxmemory,可以实现在当前服务器下动态伸缩Redis内存的目的,如图8-3所示。

内存管理

图8-3:redis进程之间调整maxmemory伸缩内存

        这个例子过于理想化,如果此时Redis-3和Redis-4进程也需要分别扩容到6Gb,这时超出系统物理内存限制就不能简单的通过调整maxmemory来达到扩容的目的,需要采用在线迁移数据或者基于复制切换服务器来达到扩容的目的,具体细节见集群章节和哨兵章节。

运维提示:
         1:Redis默认无限使用服务器内存,为防止极端情况系统内存耗尽,建议所有的Redis进程都要配置maxmemory。
         2:在保证物理内存足够的情况下,服务器上所有的redis进程可以调整maxmemory参数来达到自由伸缩最大可用内存的目的。

8.2.3 内存回收策略

Redis的内存回收机制主要体现在以下两个方面:

  • 删除到达过期时间的键对象

  • 内存使用达到maxmemory上线时触发内存溢出控制策略

1:删除过期键对象

   Redis所有的键都可以设置过期属性,内部保存在过期字典中。由于进程内保存大量的键,维护每个键精准的过期删除机制会导致消耗大量的CPU,对于单线程的Redis来说成本过高,因此Redis采用惰性删除和定时任务删除机制实现过期健的内存回收。

1)惰性删除:

  惰性删除用于当客户端读取带有超时属性的键时,如果已经超过键设置的过期时间,会执行删除操作并返回空,这种策略是出于节省CPU成本考虑,不需要单独维护TTL链表来处理过期键的删除。但是这种方式存在内存泄露的问题,当过期键一直没有访问将无法得到及时删除,从而导致内存不能及时释放。正因为如此,Redis还提供另一种定时任务删除机制作为惰性删除的补充。

2)定时任务删除:

Redis内部维护一个的定时任务,默认每秒运行10次(通过参数hz控制)。定时任务中删除过期键逻辑采用了自适应算法,根据键的过期比例使用快慢两种速率模式回收键,流程如图8-4:

内存管理

图8-4:定时任务删除过期键逻辑

流程说明:

  • 定时任务在每个数据库空间随机检查20个过期键,当发现过期时删除对应的键。

  • 如果超过检查数25%的键过期,循环执行回收逻辑直到不足25%或运行超时为止,慢模式下超时时间为25毫秒。

  • 如果之前回收键逻辑运行超过,则在Redis触发内部事件之前再次以快模式运行回收过期键任务,快模式下超时时间为1毫秒且2秒内只能运行1次。

  • 快慢两种模式内部删除逻辑相同,只是执行的超时时间不同。

Icon

开发提示:

  1. Redis定时任务通过hz参数控制周期,定时任务内部涉及很多逻辑如:关闭超时连接,删除过期键,AOF写文件频率,集群定时通信等,修改这个参数影响范围非常广,不建议调大hz参数来加速回收内存频率。

  2. 避免大量键设置相同的过期时间,否则容易产生同一时刻超过25%的键过期场景,触发循环删除过期键逻辑从而拖慢Redis响应速度。

2:内存溢出控制策略

     当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。具体策略受maxmemory-policy参数控制,Redis支持6种策略如下:

  • noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息"(error) OOM command not allowed when used memory",此时Redis只响应读操作。

  • volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。

  • allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。

  • allkeys-random:随机删除所有键,直到腾出足够空间为止。

  • volatile-random:随机删除过期键,直到腾出足够空间为止。

  • volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。

        内存溢出控制策略可以采用config set maxmemory-policy {policy} 动态配置。Redis支持丰富的内存溢出应对策略,可以根据实际需求灵活定制,比如当设置volatile-lru策略时,保证具有过期属性的键可以根据LRU剔除,而未设置超时的键可以永久保留。还可以采用allkeys-lru策略把Redis变为纯缓存服务器使用。当Redis因为内存溢出删除键时,可以通过执行info stats命令查看evicted_keys指标找出当前Redis服务器已剔除的键数量。

   每次Redis执行命令时如果设置了maxmemory参数,都会尝试执行回收内存操作。当Redis一直工作在内存溢出(used_memory>maxmemory)的状态下且设置非noeviction策略时,会频繁的触发回收内存的操作,影响Redis服务器的性能。回收内存逻辑伪代码如下:

def freeMemoryIfNeeded() :
    int mem_used, mem_tofree, mem_freed;
    // 计算当前内存总量,排除从节点输出缓冲区和AOF缓冲区的内存占用
    int slaves = server.slaves;
    mem_used = used_memory()-slave_output_buffer_size(slaves)-aof_rewrite_buffer_size();
    // 如果当前使用小于等于maxmemory退出
    if (mem_used <= server.maxmemory) :
        return REDIS_OK;
    // 如果设置内存溢出策略为noeviction(不淘汰),返回错误。
    if (server.maxmemory_policy == 'noeviction') :
        return REDIS_ERR;
    // 计算需要释放多少内存
    mem_tofree = mem_used - server.maxmemory;
    // 初始化已释放内存量
    mem_freed = 0;
    // 根据maxmemory-policy策略循环删除键释放内存
    while (mem_freed < mem_tofree) :
        // 迭代Redis所有数据库空间
        for (int j = 0; j < server.dbnum; j++) :
            String bestkey = null;
            dict dict;
            if (server.maxmemory_policy == 'allkeys-lru' ||
                server.maxmemory_policy == 'allkeys-random'):
                //如果策略是 allkeys-lru/allkeys-random 回收内存目标为所有的数据库键
                dict = server.db[j].dict;
            else :
                // 如果策略是volatile-lru/volatile-random/volatile-ttl回收内存目标为带过期时间的数据库键
                dict = server.db[j].expires;
            
            // 如果使用的是随机策略,那么从目标字典中随机选出键
            if (server.maxmemory_policy == 'allkeys-random' ||
                server.maxmemory_policy == 'volatile-random') :
                //随机返回被删除键
                bestkey = get_random_key(dict);
            else if (server.maxmemory_policy == 'allkeys-lru' ||
                server.maxmemory_policy == 'volatile-lru') :
                //循环随机采样maxmemory_samples次(默认5次),返回相对空闲时间最长的键
                bestkey = get_lru_key(dict);
            else if (server.maxmemory_policy == 'volatile-ttl') :
                //循环随机采样maxmemory_samples次,返回最近将要过期的键
                bestkey = get_ttl_key(dict);
       
            // 删除被选中的键
            if (bestkey != null) :
                long delta = used_memory();
                deleteKey(bestkey);
                                   // 计算删除键所释放的内存量
                delta -= used_memory();
                mem_freed += delta;
                //删除操作同步给从节点
                if (slaves):
                    flushSlavesOutputBuffers();
            
    return REDIS_OK;

   从伪代码可以看到,频繁执行回收内存成本很高,主要包括查找可回收键和删除键的开销,如果当前Redis有从节点,回收内存操作对应的删除命令会同步到从节点,导致写放大的问题,如图8-5所示。

内存管理

图8-5:回收内存触发删除逻辑

Icon

开发提示:建议线上Redis内存工作在maxmemory>used_memory 状态下,避免频繁内存回收开销。

       对于需要收缩Redis内存的场景,可以通过调小maxmemory来实现快速回收。比如对一个实际占用6GB内存的进程设置maxmemory=4GB,之后第一次执行命令时,如果使用非noeviction策略,它会一次性回收到maxmemory指定的内存量,从而达到快速回收内存的目的,注意此操作会导致数据丢失,一般在缓存场景下使用。

 


当前名称:内存管理
本文地址:http://pwwzsj.com/article/jpgoss.html