redis怎么进行内存管理?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了redis怎么进行内存管理?相关的知识,希望对你有一定的参考价值。

参考技术A 当mem_fragmentation_ratio>1时,说明used_memory_rss-used_memory多出的部分内存并没有用于数据存储,而是被内存碎片所消耗,如果两者相差很大,说明碎片率严重。 

当mem_fragmentation_ratio<1时,这种情况一般出现在操作系统把Redis内存交换(Swap)到硬盘导致,出现这种情况时要格外关注,由于硬盘速度远远慢于内存,Redis性能会变得很差,甚至僵死。Redis进程内消耗主要包括:自身内存+对象内存+缓冲内存+内存碎片,其中Redis空进程自身内存消耗非常少,通常used_memory_rss在3MB左右,used_memory在800KB左右,一个空的Redis进程消耗内存可以忽略不计。

◆ 缓冲内存主要包括: 客户端缓冲、复制积压缓冲区、AOF缓冲区。客户端缓冲指的是所有接入到Redis服务器TCP连接的输入输出缓冲。输入缓冲无法控制,最大空间为1G,如果超过将断开连接。

◆ 复制积压缓冲区: Redis在2.8版本之后提供了一个可重用的固定大小缓冲区用于实现部分复制功能,根据repl-backlog-size参数控制,默认1MB。对于复制积压缓冲区整个主节点只有一个,所有的从节点共享此缓冲区,因此可以设置较大的缓冲区空间,如100MB,这部分内存投入是有价值的,可以有效避免全量复制AOF缓冲区:这部分空间用于在Redis重写期间保存最近的写入命令,AOF缓冲区空间消耗用户无法控制,消耗的内存取决于AOF重写时间和写入命令量,这部分空间占用通常很小。

Redis默认的内存分配器采用jemalloc, 可选的分配器还有:glibc、tcmalloc 。内存分配器为了更好地管理和重复利用内存,分配内存策略一般采用固定范围的内存块进行分配。内存碎片问题虽然是所有内存服务的通病,但是jemalloc针对碎片化问题专门做了优化,一般不会存在过度碎片化的问题,正常的碎片率(mem_fragmentation_ratio)在1.03左右。

一般以下场景容易出现高内存碎片问题:

●  频繁做更新操作,例如频繁对已存在的键执行append、setrange等更新操作。大量过期键删除,键对象过期删除后,释放的空间无法得到充分利用,导致碎片率上升。

出现高内存碎片问题时常见的解决方式如下:

●  数据对齐:在条件允许的情况下尽量做数据对齐,比如数据尽量采用数字类型或者固定长度字符串等,但是这要视具体的业务而定,有些场景无法做到。

●  安全重启:重启节点可以做到内存碎片重新整理,因此可以利用高可用架构,如Sentinel或Cluster,将碎片率过高的主节点转换为从节点,进行安全重启子进程内存消耗主要指执行AOF/RDB重写时Redis创建的子进程内存消耗。Redis执行fork操作产生的子进程内存占用量对外表现为与父进程相同,理论上需要一倍的物理内存来完成重写操作。

内存大页机制(Transport Huge Pages,THP),是linux2.6.38后支持的功能,该功能支持2MB的大爷内存分配,默认开启。在redis.conf中增加了一个新的配置项“disable-thp”来控制THP是否开启。

子进程内存消耗总结如下:

1、Redis产生的子进程并不需要消耗1倍的父进程内存,实际消耗根据期间写入命令量决定,但是依然要预留出一些内存防止溢出。

2、需要设置sysctl vm.overcommit_memory=1允许内核可以分配所有的物理内存,防止Redis进程执行fork时因系统剩余内存不足而失败。

3、排查当前系统是否支持并开启THP,如果开启建议关闭,防止copy-onwrite期间内存过度消耗。

在日志信息中可以查看到关于THP的日志内容, 如下:

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

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

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

需要注意 ,maxmemory限制的是Redis实际使用的内存量,也就是used_memory统计项对应的内存。由于内存碎片率的存在,实际消耗的内存可能会比maxmemory设置的更大,实际使用时要小心这部分内存溢出。

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

1、删除到达过期时间的键对象。

2、内存使用达到maxmemory上限时触发内存溢出控制策略。

删除过期键对象:

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

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

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

当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-random: 随机删除过期键,直到腾出足够空间为止。

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

Redis内存管理

Redis数据库的内存管理函数有关的文件为:zmalloc.h和zmalloc.c。

Redis作者在编写内存管理模块时考虑到了查看系统内是否安装了TCMalloc或者Jemalloc模块,这两个是已经存在很久的内存管理模块,代码稳定、性能优异,如果已经安装的话,则使用之,最后检查是否是Mac系统,如果是Mac系统的话加载的文件不同,额,本人没进行过Mac编程,这块儿不考虑。对应的源代码为:

 1 //检查是否定义了TCMalloc,TCMalloc(Thread-Caching Malloc)与标准glibc库的malloc实现一样的功能,但是TCMalloc在效率和速度效率都比标准malloc高很多
 2 #if defined(USE_TCMALLOC)
 3     #define ZMALLOC_LIB ("tcmalloc-" __xstr(TC_VERSION_MAJOR) "." __xstr(TC_VERSION_MINOR))
 4     #include <google/tcmalloc.h>
 5 
 6     #if (TC_VERSION_MAJOR == 1 && TC_VERSION_MINOR >= 6) || (TC_VERSION_MAJOR > 1)
 7         #define HAVE_MALLOC_SIZE 1
 8         #define zmalloc_size(p) tc_malloc_size(p)
 9     #else
10         #error "Newer version of tcmalloc required"
11     #endif
12 //检查是否定义了jemalloc库
13 #elif defined(USE_JEMALLOC)
14     #define ZMALLOC_LIB ("jemalloc-" __xstr(JEMALLOC_VERSION_MAJOR) "." __xstr(JEMALLOC_VERSION_MINOR) "." __xstr(JEMALLOC_VERSION_BUGFIX))
15     #include <jemalloc/jemalloc.h>
16     #if (JEMALLOC_VERSION_MAJOR == 2 && JEMALLOC_VERSION_MINOR >= 1) || (JEMALLOC_VERSION_MAJOR > 2)
17         #define HAVE_MALLOC_SIZE 1
18         #define zmalloc_size(p) je_malloc_usable_size(p)
19     #else
20         #error "Newer version of jemalloc required"
21     #endif
22 //检查是否是苹果系统
23 #elif defined(__APPLE__)
24     #include <malloc/malloc.h>
25     #define HAVE_MALLOC_SIZE 1/*标记已经找到了可用的现成函数库*/
26     #define zmalloc_size(p) malloc_size(p)/*因为每个库的实现方式不一样,所以测试大小的函数也不一样*/
27 #endif

 先整体说一下内存管理的所有接口函数,然后慢慢分析,所有的接口函数为:

void *zmalloc(size_t size);//重写malloc函数,申请内存
void *zcalloc(size_t size);//重写calloc函数,不再支持按块的成倍申请,内部调用的是zmalloc
void *zrealloc(void *ptr, size_t size); //重写内存扩展函数
void zfree(void *ptr); //重写内存释放函数,释放时会更新已使用内存的值,如果在多线程下没有开启线程安全模式,可能会出现并发错误。
char *zstrdup(const char *s); //字符串持久化存储函数函数,为字符串在堆内分配内存。
size_t zmalloc_used_memory(void); //获取已经使用内存大小函数。
void zmalloc_enable_thread_safeness(void); //设置内存管理为多线程安全模式,设置之后在更新已使用内存大小时会用Mutex进行互斥操作。
void zmalloc_set_oom_handler(void (*oom_handler)(size_t)); //设置内存异常时调用的函数。
float zmalloc_get_fragmentation_ratio(size_t rss); //
size_t zmalloc_get_rss(void); //获取进程可使用的所有内存大小。
size_t zmalloc_get_private_dirty(void); //
size_t zmalloc_get_smap_bytes_by_field(char *field);
void zlibc_free(void *ptr); //释放指针指向内存函数,用这个释放内存时,不会更新使用内存变量的值。
#ifndef HAVE_MALLOC_SIZE
  size_t zmalloc_size(void *ptr); //获取内存块总题大小函数。
#endif

在解析函数的详细实现前,先分析一下.c文件开始所做的处理。在.c文件中定义了三个变量,一个用来记录已经使用内存的大小,一个用来记录是否开启了多线程安全模式,一个是互斥锁。

static size_t used_memory = 0;/*记录已经使用碓内存的大小*/
static int zmalloc_thread_safe = 0;/*是否开启了线程安全*/
pthread_mutex_t used_memory_mutex = PTHREAD_MUTEX_INITIALIZER;/*互斥锁,如果开启了多线程安全,而编译器又不支持原子操作的函数,则需要用互斥锁来完成代码的互斥操作。*/

接下来要确定内存块前面是否需要加个头部,因为不能确定具体使用的到底是什么内存分配函数,TCMalloc或者Jemalloc库在申请的内存块前增加了一个小块的内存来记录该内存块的使用情况,因为本人没分析过着俩库,具体的不做分析,具体的代码为:

#ifdef HAVE_MALLOC_SIZE//找到了已有的内存管理库时就会定义这个宏,如果已经定义了,则不再增加头部大小,如果没有定义,则根据具体的系统来确定增加头部大小的长度。
  #define PREFIX_SIZE (0)//头部长度为0
#else
  #if defined(__sun) || defined(__sparc) || defined(__sparc__)
    #define PREFIX_SIZE (sizeof(long long))//头部长度为longlong的大小
  #else
    #define PREFIX_SIZE (sizeof(size_t))//头部长度为size_tde的大小
  #endif
#endif

接下来是定义了两个更新内存占用变量的操作函数update_zmalloc_stat_add和update_zmalloc_stat_sub,这俩函数支持多线程。

 1 /*先检查编译器是否支持原子操作函数,如果支持的话,就不用互斥锁了,毕竟锁的效率很低。*/
 2 #if defined(__ATOMIC_RELAXED)//这个好像是C++11标准开始支持的,具体的不是很清楚
 3     #define update_zmalloc_stat_add(__n) __atomic_add_fetch(&used_memory, (__n), __ATOMIC_RELAXED)
 4     #define update_zmalloc_stat_sub(__n) __atomic_sub_fetch(&used_memory, (__n), __ATOMIC_RELAXED)
 5 #elif defined(HAVE_ATOMIC)//这个是GCC从某个版本开始支持的,不是所有版本的GCC都支持sync系列函数。
 6     #define update_zmalloc_stat_add(__n) __sync_add_and_fetch(&used_memory, (__n))
 7     #define update_zmalloc_stat_sub(__n) __sync_sub_and_fetch(&used_memory, (__n))
 8 #else//不支持原子操作的情况下只能使用互斥锁了。
 9     #define update_zmalloc_stat_add(__n) do { 10         pthread_mutex_lock(&used_memory_mutex); 11         used_memory += (__n); 12         pthread_mutex_unlock(&used_memory_mutex); 13     } while(0)
14 
15     #define update_zmalloc_stat_sub(__n) do { 16         pthread_mutex_lock(&used_memory_mutex); 17         used_memory -= (__n); 18         pthread_mutex_unlock(&used_memory_mutex); 19     } while(0)
20 
21 #endif
22 //更新内存使用变量函数,函数内确定是否需要多线程安全
23 #define update_zmalloc_stat_alloc(__n) do { 24     size_t _n = (__n); 25     if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); 26     if (zmalloc_thread_safe) { 27         update_zmalloc_stat_add(_n); 28     } else { 29         used_memory += _n; 30     } 31 } while(0)
32 //更新内存使用变量函数,函数内确定是否需要多线程安全
33 #define update_zmalloc_stat_free(__n) do 
34 { \ 
35   size_t _n = (__n); \ 
36   if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \ 
37   if (zmalloc_thread_safe) 
38   { \ 
39     update_zmalloc_stat_sub(_n); \ 
40   }
41   else 
42   { \ 
43     used_memory -= _n; \ 
44   } \ 
45 } while(0)

 下一篇再写具体的函数实现。

以上是关于redis怎么进行内存管理?的主要内容,如果未能解决你的问题,请参考以下文章

redis源码笔记-内存管理zmalloc.c

redis 怎么计算数据占用内存

Redis的管理

详解 Redis 内存管理机制和实现

详解 Redis 内存管理机制和实现

redis内存管理