Redis中对内存的管理功能由 zmalloc 完成,对应 zmalloc.h/zmalloc.c 文件;头文件 zmalloc.h 中包含了相关的宏定义和函数声明,具体的实现在 zmalloc.c 文件中。
zmalloc本质上是对 jemalloc、tcmalloc、libc(ptmalloc2)等内存分配器(算法库)的简单抽象封装,提供了统一的内存管理函数,屏蔽底层不同分配器的差异。
1、函数定义
在头文件 zmalloc.h 中,定义了Redis内存分配的主要功能函数,这些函数就包括内存申请、释放和统计等功能:
1 | arduino复制代码// 申请内存 |
2、宏和全局变量
zmalloc定义了几个宏变量、函数和全局变量,用于记录一些状态信息
PREFIX_SIZE
C语言标准库函数malloc在申请内存时,会记录申请的内存块大小,并把大小数值存储到分配的内存块中,用于外部获取已分配空间的大小。存储大小数值的内存空间是申请大小的额外空间,PREFIX_SIZE
就是表示这块额外内存空间的大小。
zmalloc根据实际使用的内存分配器判断是否需要申请额外的内存空间,然后通过 HAVE_MALLOC_SIZE
变量进行标识。
tcmalloc和Mac平台下的malloc函数族提供了计算已分配空间大小的函数,Redis不需要多申请一个PREFIX_SIZE
大小的内存空间来记录分配的内存块大小,此时PREFIX_SIZE
值为0。否则根据Redis服务器所在的系统平台,使用sizeof(long long)
或sizeof(size_t)
大小的额外空间记录申请的空间大小。
1 | arduino复制代码#ifdef HAVE_MALLOC_SIZE |
ASSERT_NO_SIZE_OVERFLOW
在上面的宏定义中还包含用于检查申请内存大小是否有效的函数ASSERT_NO_SIZE_OVERFLOW
,断言 申请的内存大小size加上PREFIX_SIZE
的和 大于 size本身
used_memory
zmalloc使用used_memory
变量来统计当前已分配的总内存大小,同时定义了两个原子操作,用于更新该变量的值:
1 | scss复制代码// 原子操作,used_memory增加分配的内存空间size大小 |
3、zmalloc函数
zmalloc函数用于申请指定大小内存,函数实现由2个部分组成:尝试申请内存、失败处理
1 | arduino复制代码void *zmalloc(size_t size) { |
尝试申请内存
ztrymalloc_usable
函数尝试申请内存,会先检查申请的内存块大小,然后调用malloc函数
进行内存分配;申请成功后统计内存大小,如果申请失败则返回NULL
1 | arduino复制代码void *ztrymalloc_usable(size_t size, size_t *usable) { |
实际的内存分配是由 malloc函数 完成的,malloc函数是对实际内存分配器的内存分配函数的抽象封装,用于屏蔽不同内存分配器函数的差异。例如使用tcmalloc时,调用malloc函数实际就是在调用tc_malloc函数。
除了malloc函数,calloc、realloc和free等函数也采用了相同的做法
1 | scss复制代码#if defined(USE_TCMALLOC) |
在调用malloc函数之前,还有个最小申请大小的处理;如果申请的内存大小size小于0,则返回储存long类型数值所需要的空间大小
1 | scss复制代码/* When using the libc allocator, use a minimum allocation size to match the |
OOM处理
在申请内存失败时,会调用 zmalloc_oom_handler函数 进行处理。zmalloc_oom_handler 函数的默认实现是打印“Out of memory”异常信息,并终止服务进程
1 | arduino复制代码static void zmalloc_default_oom(size_t size) { |
头文件 zmalloc.h 定义了可以指定oom异常处理逻辑的函数 zmalloc_set_oom_handler,允许通过传入函数对异常进行处理
1 | javascript复制代码void zmalloc_set_oom_handler(void (*oom_handler)(size_t)) { |
在 server.c 中有对zmalloc_set_oom_handler函数的使用,服务启动时传入redisOutOfMemoryHandler函数,将异常日志打印到日志文件中:
1 | scss复制代码int main(int argc, char **argv) { |
整理下代码逻辑,zmalloc函数内部逻辑大致流程如下:
4、zcalloc函数
zcalloc函数也用于申请分配内存,zcalloc函数跟zmalloc函数 唯一区别是在实际分配内存空间时,调用的是calloc函数
1 | arduino复制代码/* Allocate memory and zero it or panic */ |
5、zfree函数
zfree函数用于内存回收,释放由zmalloc、zcalloc函数申请分配的内存空间
1 | arduino复制代码void zfree(void *ptr) { |
zfree函数内部的实现逻辑,也区分不同的底层库。 例如使用tcmalloc,HAVE_MALLOC_SIZE
变量的值为true,此时直接调用zmalloc_size函数获取ptr指针指向的内存空间大小,然后直接释放ptr即可。否则需要将ptr指针向前偏移PREFIX_SIZE
字节的长度,获取到内存块实际的起始地址进行释放;计算释放的内存空间大小,也需要加上PREFIX_SIZE
字节。
6、选择内存分配器
内存的分配、释放都依赖于底层使用的内存分配器(算法库),那么Redis是怎么指定底层使用的具体内存分配器的呢?
在 README.md 文件中有对内存分配器(Allocator)的描述:
1 | vbnet复制代码Allocator |
大抵意思是Redis在Linux系统上默认为jemalloc,但是可以通过设置“MALLOC”环境变量进行指定。
启动一个Redis服务验证一下,使用 info memory 命令查看运行中的Redis服务内存信息:
1 | csharp复制代码[root@localhost redis-6.2.6]# ./src/redis-cli info memory |grep mem_allocator |
可以确定,默认情况下Redis使用的内存分配器是jemalloc。接着尝试指定内存分配器,Redis的内存分配器在程序编译时进行指定,所以需要编译redis源码;在使用make命令编译时,直接指定使用的内存分配器:
1 | csharp复制代码[root@localhost redis-6.2.6]# make MALLOC=libc |
运行编译好的Redis服务后,通过 redis-cli 直接查看,可以发现使用的内存分配器为 libc:
1 | csharp复制代码[root@localhost redis-6.2.6]# ./src/redis-cli info memory |grep mem_allocator |
源码中初始化选择内存分配器的逻辑,在 zmalloc.h 中通过判断变量引入不同的头文件来实现。
1 | arduino复制代码// 使用tcmalloc时,引入google/tcmalloc.h文件 |
通过条件编译逻辑可以知道,zmalloc根据Makefile定义的不同变量进行判断,查看 Makefile 文件:
1 | perl复制代码# Default allocator defaults to Jemalloc if it's not an ARM |
Makefile 文件中判断到 $(USE_JEMALLOC) 参数的值为“no”时,将会使用 libc 默认的内存分配器,那似乎可以来点这样的操作:
1 | bash复制代码[root@localhost redis-6.2.6]# make USE_JEMALLOC=no |
运行编译好的Redis服务后,通过 redis-cli 直接查看,可以发现使用的内存分配器为 libc:
1 | csharp复制代码[root@localhost redis-6.2.6]# ./src/redis-cli info memory |grep mem_allocator |
7、内存大小与碎片率
在查看Redis服务器内存信息的时候,会发现里面包含有内存碎片率mem_fragmentation_ratio
信息,但是在zmalloc中只有一个全局变量used_memory
用于统计已分配内存大小。那么碎片率是怎么来的?
1 | makefile复制代码[root@localhost redis-6.2.6]# redis-cli info memory |
获取内存大小
先来看看获取内存大小的函数 zmalloc_used_memory:
1 | arduino复制代码size_t zmalloc_used_memory(void) { |
zmalloc_used_memory函数直接获取used_memory
的值进行返回,这里使用了个原子的赋值操作,除此之外也就没有其他什么逻辑了。
我们知道used_memory
是Redis自身统计维护的内存大小总数,实际上操作系统分配给Redis进程的内存大小未必就是这个数。例如在调用free函数释放内存空间时,zmalloc将used_memory的值减去了被释放的空间大小,但是free函数内部的实现为了减少调用系统调用接口,可能并没有实际释放这部分内存空间,而是由进程继续持有着这块空间,用于下次malloc函数申请内存时复用。
zmalloc定义了从操作系统角度获取当前Redis进程分配内存大小的函数 zmalloc_get_rss。既然从操作系统的角度获取进程的内存占用大小,那么就需要区分不同的操作系统实现。
我们正常需要获取进程的内存分配的情况,可以通过虚拟文件 /proc/$pid/stat 得到。 /proc 以文件系统的形式为内核和进程提供通信的接口,实际上并不存储在磁盘上,而是系统内存的映射。
先查询Redis服务的进程号,然后直接看进程号对应的 /stat 文件:
1 | csharp复制代码// 1、获取进程号 |
/state文件的第24个数据项就是进程当前驻留物理地址空间的大小,单位是page(物理内存页)。这里可以看到,进程当前占用了692个内存页。
系统内存页的大小,可以通过getconf
命令获取:
1 | csharp复制代码[root@localhost ~]# getconf PAGESIZE |
单个页面大小为 4096 字节,692个内存页也就是 2834432 字节。看下Redis的内存信息 used_memory_rss
,跟计算出来的数据是一致的。
1 | makefile复制代码[root@localhost redis-6.2.6]# ./src/redis-cli info memory |grep mem |grep rss |
再来看下zmalloc_get_rss函数的实现,能够发现逻辑几乎是一致的:
1 | ini复制代码#if defined(HAVE_PROC_STAT) |
如果有 /proc/pid/stat∗文件,则从该文件中获取。在不同的操作系统下,可能并不一定有∗/proc/pid/stat\ 文件,则从该文件中获取。在不同的操作系统下,可能并不一定有 */proc/pid/stat∗文件,则从该文件中获取。在不同的操作系统下,可能并不一定有∗/proc/pid/stat* 文件,所以zmalloc还提供了其他的实现。例如读取 /proc/$pid/psinfo 文件、调用task_info函数 ,在上面这些信息都没有的情况下,就返回 used_memory
。完整的函数定义逻辑如下:
1 | c复制代码/* Get the RSS information in an OS-specific way. |
内存碎片率
知道了Redis向系统申请的内存大小,也知道了系统实际分配给Redis进程的内存大小,似乎就不难得出内存碎片率了:
1 | bash复制代码mem_fragmentation_ratio(内存碎片率)= used_memory_rss/used_memory |
来看下Redis的实现,内存碎片率 mem_fragmentation_ratio
信息对应 redisMemOverhead 对象的total_frag属性,计算逻辑在 object.c 文件的 getMemoryOverheadData函数 中:
1 | ini复制代码struct redisMemOverhead *getMemoryOverheadData(void) { |
管理碎片率
一般认为,合理的内存碎片率应该控制在 1~1.5 之间。
(1)碎片率过低
如果内存碎片率低于1,那么说明系统分配给Redis的内存不能满足实际需求,此时的Redis实例可能会把部分数据交换到磁盘上。因为磁盘I/O的读写速度远远慢与内存读写,频繁的磁盘换出换入操作会给Redis带来性能问题。
内存碎片率过低的问题,根据情况可以通过 扩展物理内存 、调整Redis实例的maxmemory
配置 或者是 禁用SWAP 来解决。
Redis的配置文件 redis.conf 提供了实例可用最大内存配置项maxmemory
,搭配maxmemory-policy
配置项控制Redis可使用内存空间的大小,以避免将数据交换到磁盘交换区:
1 | python复制代码# Redis使用内存到达指定值时,根据policy删除keys |
Redis服务的生产环境通常是建议禁用SWAP的,进程的内存使用情况可以通过查看 /proc/$pid/smap 文件辅助判断:
1 | yaml复制代码// 系统swap使用情况 |
数据中的Swap部分表示的就是进程被swap到交换空间的大小(不包含mmap内存)。
(2)碎片率过高
相反地,如果内存碎片率大于1.5,那么说明此时的Redis有较大的内存浪费。
问题产生的原因,是Redis释放了内存,但是内存分配器(Allocator)并没有将这部分内存返还给操作系统。从我们对内存分配的了解可知,这是并不是Redis的特性,而是malloc函数导致的。
Redis提供了自动内存碎片整理功能(defragmentation),通过 redis.conf 文件的配置项 activedefrag
启用:
1 | python复制代码# 是否启用碎片整理功能 |
内存碎片整理功能defragmentation允许Redis实例在运行中对碎片空间进行回收,而不需要重启服务。不过需要注意的是,Redis只有在使用jemalloc内存分配器时才支持该功能。
注:更多内存碎片整理相关的内容,可以查看 Redis6源码系列(二)- 自动碎片整理defrag
本文转载自: 掘金