背景
内存管理不恰当,会有造成大量内存碎片、内存分配效率低、内存实际使用率低。在程序运行中,这些问题会导致程序无法持久正常运行,于服务器技术而言更是如此。
目标
内存管理可以分为三个层次,自底向上分别是:
- 操作系统内核的内存管理
- 使用系统调用维护的内存管理算法
- 在上一步基础上,根据应用程序本身的程序特性进行优化, 比如使用引用计数、内存池方式等
本文我们主要讨论第二步的使用系统调用维护的内存管理算法
对于需要持久运行的程序而言,内存管理十分重要,一个优秀的内存管理策略应该具有如下特性:
- 分配速度尽可能快
- 内存碎片尽可能少
- 内存实际使用率高
memcached内存管理机制
memcached是一个知名的高效的分布式内存cache,默认使用SlabAllocation机制管理内存,其主要思想是按照预先规定的大小,将分配的内存分割成特定长度的块以存储相应长度的key-value数据记录,以完全解决内存碎片问题。
Slab和Chunk
slab是memcached一次申请内存的最小单位。memcached启动时使用参数-m指定其可用内存,但并不是启动时所有的内存就全部分配出去了,只有在需要时才会去申请,而且每次申请一定是一个slab。Slab的大小固定为1M(1048576 Byte)。
一个slab由若干个大小相等的chunk组成。每个chunk中都保存了一个item结构体、一对key和value。
虽然在同一个slab中chunk的大小相等的,但是在不同的slab中chunk的大小并不一定相等,在memcached中按照chunk的大小不同,可以把slab分为很多种类(class)。
在启动memcached的时候可以通过-vv来查看slab的种类:
1 | $ memcached -vv |
向memcached添加item时,memcached首先会根据item大小,根据chunk size向上取整,来选择接近的slab class。例如,item大小为156,向上取最小的chunk size为176.
memcached采取SlabAllocation策略,一次向系统申请1M空间内存作为slab。slab根据其chunk size分割成等大的内存块chunk,每个chunk都可以存储一个item。memcached内存分配做到了一次申请大量空间,分割多块给小对象多次使用,大大降低了向操作系统申请内存的时间,大部分内存管理机制都会用到这个策略。
其次,memcached将slab按chunk size分组,将每一个slab内部按chunk size分割成固定大小的内存块。固定大小的chunk,回收之后还可以用于下一次item的存储,内存的分布情况不会变的错综复杂,有利于减少内存碎片。chunk size递增策略,每一次item都向上取到最接近的chunk size对应的slab来存储,有利于提高其实际内存利用率
Redis内存管理机制
Redis默认使用jemalloc作为内存分配器。jemalloc 是一个通用的 malloc(3)实现,它强调了分段回避和可伸缩并发支持。jemalloc 在 2005 年首次作为 FreeBSD libc 分配器使用,2010年,jemalloc 的功能延伸到如堆分析和监控/调优等。现代的 jemalloc 版本依然集成在 FreeBSD 中。
jemalloc机制相对memcached的SlabAllocation策略来说比较复杂,这里不做详细介绍,详情可参考本文列出的参考文档。
类似的,对jemalloc策略总结如下:
- 一次向系统申请大内存4M(chunk),供之后多次使用
- 将控制的内存分成不同大小的小内存块(bins),重复使用
- 存放对象时,向上找到最接近size的bin对应的空闲内存块来存储
- 每个线程有自己的独立空间tcache,将内存划分为arena供线程使用,减少线程间的互斥访问
前三条策略的思想与memcached的SlabAllocationc策略所蕴含的思想一致,第四条策略是针对多线程加速的优化。
参考资料
内存优化总结:ptmalloc、tcmalloc和jemalloc
memcached的内存管理机制
jemalloc和内存管里
A Scalable Concurrent malloc(3) Implementation for FreeBSD
jemalloc内存分配器详解