第9章 分布式存储引擎
分布式存储引擎层负责处理分布式系统中的各种问题,例如数据分布、负载均衡、容错、一致性协议等。与其他分布式存储系统类似,分布式存储引擎层支持根据主键更新、插入、删除、随机读取以及范围查找等操作,数据库功能层构建在分布式存储引擎层之上。
分布式存储引擎层包含三个模块:RootServer、UpdateServer以及ChunkServer。其中,RootServer用于整体控制,实现子表分布、副本复制、负载均衡、机器管理以及Schema管理;UpdateServer用于存储增量数据,数据结构为一个内存B树,并通过主备实时同步实现高可用,另外,UpdateServer的网络框架也经过专门的优化;ChunkServer用于存储基线数据,基线数据按照主键有序划分为一个个子表,每个子表在ChunkServer上存储了一个或者多个SSTable,另外,定期合并和数据分发的主要逻辑也由ChunkServer实现。
OceanBase包含一个公共模块,包含其他模块共用的网络框架、内存池、任务队列、锁、基础数据结构等,本章将介绍分布式存储引擎以及公共模块的实现。
9.1 公共模块
OceanBase源代码中有一个公共模块,包含其他模块需要的公共类,例如公共数据结构、内存管理、锁、任务队列、RPC框架、压缩/解压缩等。下面介绍其中部分类的设计思路。
9.1.1 内存管理
内存管理是C++高性能服务器的核心问题。一些通用的内存管理库,比如Google TCMalloc,在内存申请/释放速度、小内存管理、锁开销等方面都已经做得相当卓越了,然而,我们并没有采用。这是因为,通用内存管理库在性能上毕竟不如专用的内存池,更为严重的问题是,它鼓励了开发人员忽视内存管理的陋习,比如在服务器程序中滥用C++标准模板库(STL)。
在分布式存储系统开发初期,内存相关的Bug相当常见,比如内存越界、服务器出现Core Dump,这些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
{
public:
//设置模块号
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),当出现内存越界等问题时,服务器程序会很快在出现问题的位置Core Dump,而不是带着错误运行一段时间后才Core Dump,从而方便问题定位。
总而言之,OceanBase的内存管理没有采用高深的技术,也没有做到通用或者最优,但是很好地满足了服务器程序开发的两个最主要的需求:可控性以及没有内存碎片。