OceanBase分布式存储引擎公共模块——内存管理
内存管理是C++高性能服务器的核心问题。一些通用的内存管理库,比如Google TCMalloc,在内存申请/释放速度、小内存管理、所开销等方面都已经做得相当卓越了,然而,我们并没有采用。这是因为,通用内存管理库在性能上毕竟不如专用的内存池,更为严重的问题是,它鼓励了开发人员忽视内存管理的陋习,比如在服务器程序中滥用C++标准模板库(STL)。
在分布式存储系统开发初期,内存相关的Bug相当常见,比如内存越界、服务器出现Core Comp,这些Bug都非常难以调试。因此,这个时期内存管理的首要问题并不是高效,而是可控性,并防止内存碎片。
OceanBase系统有一个全局的定长内存池,这个内存池维护了由64KB大小的定长内存块组成的空闲链表,其工作原理如下:
- 如果申请的内存不超过64KB,尝试从空闲链表中获取一个64KB的内存块返回给申请者;如果空闲链表为空,需要首先从操作系统中申请一批大小为64KB的内存块加入空闲链表。释放时将64KB的内存块加入到空闲链表中以便下次重用。
-
如果申请的内存超过64KB,直接调用Glibc 的内存分配(malloc)函数,向操作系统申请用户所需大小的内存块。释放时直接调用Glibc的内存释放(free)函数,将内存块归还操作系统。
OceanBase的全局内存池实现简单,但内存使用率较低,即使申请几个字节的内存,也需要占用大小为64KB的内存块。因此,全局内存池不适合管理小块内存,每个需要申请内存的模块,比如UpdateServer中的MemTable,ChunkServer中的缓存等,都只能从全局内存池中申请大块内存,每个模块内部再实现专用的内存池。每个线程会缓存若干个大小分别为64KB和2MB的内存块,每个线程总是首先尝试从线程局部缓存中申请内存,如果申请不到,再从全局内存池中申请。
class ObIAllocator
{
public:
//内存申请接口
virtual void* alloc (const int64_t sz) = 0;
//内存释放接口
virtual void free (void* ptr) = 0;
};
class ObMalloc : public ObIAllocator
{
public:
//设置模块号
void set_mod_id(int32_t mod_id);
//申请大小为sz的内存块
void * alloc (const int64_t sz);
//释放内存
void free (void* ptr);
}
class ObTCMalloc : public ObIAllocator
{
publilc:
//设置模块号
void set_mod_id(int32_t mod_id);
//申请大小为sz的内存块
void * alloc (const int64_t sz);
//释放内存
void free (void* ptr);
}
ObIAllocator 是内存管理器的接口,包含alloc和free两个方法。ObMalloc和ObTCMalloc是两个实现了ObIAllocator接口的全局内存池,不同点在于,ObMalloc不支持线程缓存,ObTCMalloc支持线程缓存。ObTCMalloc首先尝试从线程局部的空闲链表申请内存块,如果申请不到,在通过ObMalloc的alloc方法申请。释放内存时,如果没有超出线程缓存的内存块个数限制,则将内存块还给线程局部的空闲链表;否则,通过ObMalloc的free方法释放。另外,允许通过set_mod_id函数设置申请者所在的模块编号,便于统计每个模块的内存使用情况。
全局内存池的意义如下:
- 全局内存池可以统计每个模块的内存使用情况,如果出现内存泄漏,可以很快定位到发生问题的模块。
- 全局内存池可用于辅助调试。例如,可以将全局内存池中申请到的内存块按字节填充为某个非法的值(比如0xFE),当出现内存越界等问题时,服务器程序会很快在chu'xian'wen'ti'd出现问题的位置Core Dump,而不是带着错误运行一段时间后才Core Dump,从而方便问题定位。
总而言之,OceanBase的内存管理没有采用高深的技术,也没有做到通用或者最优,但是很好的满足了服务器程序开发的两个最主要的需求:可控性以及没有内存碎片。