8.2、内存管理

内存管理

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

  1. 设置内存上限

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

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

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

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


    8.2、内存管理_第1张图片
    2019-04-30-21-08-10.png
  2. 动态调整内存上限

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

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

    通过动态修改maxmemory,可以实现在当前服务器下动态伸缩Redis内存的目的。

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

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

  3. 内存回收策略

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

    • 删除到达过期时间的键对象。
    • 内存使用达到maxmemory上限时触发内存溢出控制策略。
    1. 删除过期键对象

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

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

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


        8.2、内存管理_第2张图片
        2019-04-30-21-31-08.png

      流程说明:

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

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

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

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

    2. 内存溢出控制策略

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

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

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

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

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

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

      6)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有从节点,回收内存操作对应的删除命令会同步到从节点,导致写放大的问题。

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

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

你可能感兴趣的:(8.2、内存管理)