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和内存的隔离性,如下图所示:


    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控制)。定时任务中删除过期键逻辑采用了自适应算法,根据键的过期比例、使用快慢两种速率模式回收键,流程如下图:


        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指定的内存量,从而达到快速回收内存的目的。注意,此操作导致数据丢失和短暂的阻塞问题,一般在缓存场景下使用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容