Redis6源码系列(二)- 自动碎片整理defrag

1、内存碎片

运行在用户空间(user space)的进程无法直接执行内核代码或者访问内核函数来分配内存资源,需要通过系统调用接口brk/sbrk(),请求系统内核来操作。系统调用会使得CPU从用户态(user mode)切换到内核态(kernel mode),这在需要频繁申请、释放内存的使用场景下会带来较大的性能开销。

为了尽量减少系统调用brk/sbrk()的调用次数,内存管理函数malloc/free()在实现上做了一定的优化。

空闲内存列表.png

一般情况下,在使用free()函数释放内存时不降低 programe break 的位置,而是将需要释放的内存添加到 空闲内存列表 ,供malloc()函数后续循环使用。

也就是说,malloc()函数在申请内存时,会优先在空闲内存列表查找大于或等于申请大小的内存块。如果找到满足需求的内存块,直接返回给调用者;如果内存块较大,可能会对其进行分割,在将一块大小满足需求的内存返回给调用者的同时,把多余的内存块保留着空闲内存列表中。

malloc分配机制.png

Redis自身没有实现底层内存的管理机制,而是依赖于jemalloc/tcmalloc等内存分配器(allocator)的malloc/free()函数族;在删除key或者清除过期keys的时候,调用free()函数来释放内存。实际上这部分内存可能并没有及时返还给操作系统,而是由内存分配器继续持有。

在经过一段时间的使用后,Redis可能会持有大量分配了却没有使用的内存空间,这部分空间被称为 内存碎片。Redis的内存碎片情况可以通过 INFO MEMORY 命令查看:

[root@localhost redis-6.2.6]# redis-cli info memory
# Memory
// 进程申请内存
used_memory:934384
// 实际分配内存
used_memory_rss:2830336
// 碎片率
mem_fragmentation_ratio:3.20
// 碎片大小(字节)
mem_fragmentation_bytes:1946360
...
复制代码

Redis作为一款内存数据库(in-memory database),需要频繁的分配、释放内存,持有适量的空闲内存能有效减少系统性能开销、提升内存分配速度。

但是根据malloc()函数的内存分配机制可以知道,维护在空闲内存列表的 内存块 在经过malloc()函数多次地查找、分割之后,会变得越来越小。直至最后,空闲内存列表中包含大量的小块内存,然而这部分内存的任意一块都无法满足malloc()函数的内存分配需求。

例如,此时堆空间中有总数40k的空闲内存块,但是无法满足一个20k大小的数据的的内存分配需求:

空间不足.png

在物理内存资源紧张的情况下,大量的内存碎片会导致Redis出现 swap交换 甚至是 内存溢出(oom)的情况,影响Redis服务的性能和稳定性。

注:更多内存分配相关的内容,可以查看 Redis6源码系列(一)- 内存管理zmalloc

2、内存压缩(Memory compaction)

内存碎片的问题不仅是体现在用户进程上,还体现在操作系统内核上。

在现代操作系统体系中,往往使用大页面(huge pages)来提升处理器的性能;但是huge pages要求系统能够找到连续的物理内存区域,这些区域不仅要求足够大,而且还要求能正确进行对齐。由于大量内存碎片的存在,系统很可能无法找到满足需求的连续内存空间。

为了解决碎片的问题,内核开发人员采用了各种方法来进行尝试,其中就包含 内存压缩(Memory compaction,也称为内存紧缩)技术。

内存压缩1.png

假定一块内存区域如上图所示:白色为空闲内存页,着色的部分为已被分配使用的内存页。

我们可以简单的认为,内存压缩由2个步骤组成:

标识内存页

可移动内存页列表

从内存区域的地步开始,标识已分配使用的内存页,并构造成一个已分配内存页表,称为可移动内存页列表(Movanle pages)

空闲内存页列表

同时,从内存区域的顶部开始,标识未被分配使用的空闲内存页,并构造成空闲内存页列表(Free pages)

内存压缩2.png

页面迁移

两个标识并创建内存页列表的动作 在内存区域靠近中间的部分相遇,此时将 已分配使用的页面 移动到 内存区域顶部的 空闲空间。

内存压缩3.png

已分配内存页移动后,就得到了一块较为规整的内存区域。当然,这里是一个简化的逻辑,实际上内存压缩(Memory compaction)的实现相当复杂,比如可移动内存页的识别、内存页的移动、压缩动作的触发等等一系列“细节”都是不容易实现的。

3、Redis的碎片整理方法

在查看Redis内存使用情况时,除了使用 info 命令之外,还可以考虑 memory 命令

内存统计

使用 memory stats 命令可以查看Redis服务的内存统计信息:

[root@localhost redis-6.2.6]# ./src/redis-cli memory stats
// Redis使用内存的峰值
 1) "peak.allocated"
 2) (integer) 931888
 // Redis 使用其分配器分配的总字节数
 3) "total.allocated"
 4) (integer) 872024
 ...
复制代码

memory stats 命令返回的结果几乎都能在 info memory 命令的结果中找到对应的数据项。

内存分配状态

在使用jemalloc作为分配器时,可以查看内存分配状态的分析报告:

[root@localhost redis-6.2.6]# ./src/redis-cli memory malloc-stats
___ Begin jemalloc statistics ___
Version: "5.1.0-0-g0"
Build-time option settings
  config.cache_oblivious: true
  ...
Arenas: 16
Quantum size: 8
Page size: 4096
Maximum thread-cached size class: 32768
...  
--- End jemalloc statistics ---
复制代码

内存清理:purge

内存清理 memory purge 同样是jemalloc分配器特有的命令,在使用其他分配器时并不支持。

在进程终止的时候,其所占用的所有内存都会返还给操作系统,所以很多程序的实现中都会依赖这种内存的“自动释放”机制。

但是Redis作为一个数据库服务进程,停机会是一个影响比较大的操作,在常规的生产环境下不应该也不允许经常性的停机重启服务。所以就需要有可以在不停机的情况下清理内存碎片的方法,这就是 memory purge 命令:

[root@localhost redis-6.2.6]# ./src/redis-cli memory purge
OK
复制代码

自动整理:defrag

Redis提供了内存碎片自动整理功能(Active Defragmentation),允许服务实例在不停机、无需人工干预的情况下主动整理内存碎片。通过属性参数设置 config set activedefrag yes 即可启用:

[root@localhost redis-6.2.6]# ./src/redis-cli config get activedefrag
1) "activedefrag"
2) "no"
[root@localhost redis-6.2.6]# ./src/redis-cli config set activedefrag yes
OK
复制代码

4、自动整理实现

内存碎片自动整理功能(Active Defragmentation)是一项比较有意思的特性,来看看它是怎么实现的。

未完待续...

猜你喜欢

转载自juejin.im/post/7031847977782083621
今日推荐