摘要:好消息!我们将推出「产品模块原理系列」内容,其中包含了OceanBase最核心的SQL引擎、存储引擎、内存管理、分布式并行计算引擎等重要的技术原理,很多内容还是第一次对外发布哦~第三期我们为大家带来分布式数据库OceanBase的内存管理原理介绍,更多内容敬请期待!
OceanBase内存管理原理
内存管理是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的内存块,每个线程总是首先尝试从线程局部缓存中申请内存,如果申请不到,再从全局内存池中申请。
全局内存池的意义如下:
全局内存池可以统计每个模块的内存使用情况,如果出现内存泄露,可以很快定位到发生问题的模块。
全局内存池可用于辅助调试。例如,可以将全局内存池中申请到的内存块按字节填充为某个非法的值(比如0xFE),当出现内存越界等问题时,服务器程序会很快在出现问题的位置Core Dump,而不是带着错误运行一段时间后才Core Dump,从而方便问题定位。
总而言之,OceanBase的内存管理没有采用高深的技术,也没有做到通用或者最优,但是很好地满足了系统初期的两个最主要的需求:可控性以及没有内存碎片。
DB层内存的管理和配置
1. OceanBase系统内部内存配置
OceanBase是支持多租户的一个数据库系统,但是OceanBase内存上限中配置的内容并不能全部分配给租户使用。因为每一个OBServer上租户都会共享部分资源或功能,这些资源或功能所使用的内存由于并不属于任何一个普通租户,所以被归结到“系统内部内存”中。
系统内部内存可使用的内存上限是可以通过system_memory_percentage配置参数配置的,它的含义是系统内部可使用OceanBase内存上限的百分之多少。
假设OceanBase内存上限为80GB,system_memory_percentage为20%,那么系统内部可使用的内存就是80GB*20%=16GB。换而言之,可用于租户分配的内存就是剩下的80GB-16GB=64GB了。
2. OceanBase内存上限管理
OceanBase提供两种方式设置自身内存的上限:一种是按照计算机器总内存上限的百分比计算自身可以使用的总内存,由memory_limit_percentage参数配置;另一种是直接设置OceanBase可用内存的上限,由memory_limit参数配置。其中memory_limit参数值为0时,使用百分比的配置方式,否则则使用绝对值的配置方式。
举例来说,假设在一台100GB的机器上启动一个OceanBase实例:
memory_limit_percentage |
memory_limit |
OceanBase内存上限 |
80 |
0 |
80GB |
80 |
90GB |
90GB |
第一种情况,由于memory_limit为0,所以以memory_limit_percentage为准,那么OceanBase内存上限就是100GB*80%=80GB。
第二种情况,由于memory_limit为90GB,所以以memory_limit为准,OceanBase内存上限就是90GB。
故一台OBServer上内存分布大致如下图:
图1: OBServer内存分布
业务租户内部内存管理
OceanBase把租户内部的内存总体上分为两个部分:
1、不可动态伸缩的内存
2、可动态伸缩的内存
其中,不可动态伸缩的内存主要由保存数据库增量更新的memstore使用;可动态伸缩的内存主要由KVCache进行管理。
可动态伸缩的KVCache会尽量使用除去不可动态伸缩后租户的全部内存。
1. 不可动态伸缩的内存管理
目前与不可动态伸缩内存相关的配置只有memstore_limit_percentage,它表示租户的memstore部分最多占租户总内存上限的百分比。租户的写入或者更新会增加memstore的内存使用,当租户的memstore部分内存到达上限以后,后续的写入或者更新操作将会被拒绝。OceanBase会根据memstore的内存使用比例决定何时进行转储或者合并释放memstore的内存。该比例由配置项freeze_trigger_percentage控制,表示当memstore内存占用到达其上限的百分比后就进行冻结(转储和合并的前置动作)。
2. 可动态伸缩的内存管理
可动态伸缩的内存主要部分是KVCache。OceanBase将绝大多数的KV格式的缓存统一在了KVCache中进行管理,KVCache支持动态伸缩,不同KV的优先级控制以及智能的淘汰机制。
KVCache一般不需要配置,特殊场景下可以通过参数控制各种KV的优先级,优先级高的KV类比优先级低的KV类更容易被保留在Cache中。涉及参数如下,具体含义可参考配置项参考:
参数名 |
默认值 |
clog_cache_priority |
1 |
index_clog_cache_priority |
1 |
user_tab_col_stat_cache_priority |
1 |
index_cache_priority |
10 |
index_info_block_cache_priority |
1 |
user_block_cache_priority |
1 |
user_row_cache_priority |
1 |
bf_cache_priority |
1 |
bf_cache_miss_count_threshold |
100 |
故业务内存分配大致如下图所示:
图2: 业务租户内存分配(比例由参数控制)
结合二三部分所述,单台物理机上的内存分配大致如下图所示:
图3: 物理机内存分配图
其中各余部分均由OBServer内部参数控制及管理:
操作系统占用:物理可用内存*(1-memory_limit_percentage);
OBServer内部租户:物理可用内存* memory_limit_percentage*system_memory_percentage;
memstore:物理可用内存* memory_limit_percentage*memstore_limit_percentage;
KVCache(此范围内动态伸缩):物理可用内存* memory_limit_percentage*(1- memstore_limit_percentage)。
生产环境中常见的三种场景内存管理策略
1、OLTP场景
业务特点:读写RT敏感、数据一致性敏感、SQL多以key-value类型为主、范围查询及连表查询较少且也都走最优索引、读写比相差不大、业务存在明显高峰期。
场景解读:一般OLTP场景对读写RT要求严格,GB级OB库一般要求RT保证在10ms以内,TB级OB库甚至要求更低比如5ms。日常交易或者说事务要求严格保证数据一致性,如发生RT抖动,锁冲突骤增、极易发生业务数据错误。日常读写流量比基本上在3:1~1:1之间,且日常流量一般存在峰值,跟社会生活正常作息时间正相关。涉及行业一般有金融、电商、物流、社会各类服务业等。
配置推荐:对于此类场景的特点,一般需要将OB内存切换时间调整到最小,在日常流量峰值期间尽可能避免集群发生合并,而将日常合并时间尽量调整到业务低峰期保证集群合并对集群性能影响最小,具体推荐配置参数如下:
memory_limit=0(默认即可,即OB使用物理机操作系统内存上限由memory_limit_percentage参数决定)
memory_limit_percentage=80/90(根据物理机可用内存大小决定,512G以下的使用80%,512及以上的采用90%)
freeze_trigger_percentage=60~75%(根据读写比例判断,建议写越高则取数越趋近于memory_limit_percentage值,目的是能更多的存放dml数据到内存,减少转储和合并)
minor_freeze_times=3-8(根据每日dml量决定,转储可快速释放内存但也存在轻微抖动,转储速度取决于存储介质,ssd>hdd>sata)
2、OLAP场景
业务特点:读写RT可容忍在百毫秒甚至秒级、数据一致性不敏感(可容忍毫秒级节点间数据不一致,且分析型业务聚合计算数据都是万到百万级别量级)、SQL多以条件范围排序、连表聚合计算为主(此类AP查询SQL在OB中只需走分区键索引或主键即可)、读写比往往是读远小于写、仅读业务存在高峰期、写业务峰值往往有周期性或间歇性(主要原因为大多数AP类业务都有实时、离线周期性同步任务)。涉及行业一般有金融、电商、彩票、服务业、咨询、科学计算领域等,较AP类业务更常见。
配置推荐:对于此类场景的特点,一般需要将OB内存切换时间调整到尽可能小的程度以保证数据同步性能不受合并影响,但同时也要考虑到AP场景物理机存储介质对合并影响的问题。日常合并次数减少带来的一个问题就是合并时间也会增加(相比于每日开多轮转储和不开转储的情况)。所以这类场景需要结合存储介质进行决策,如采用ssd介质可相对增加转储次数,尽量将合并控制在数据同步低峰期;如采用sata慢速存储介质,可优化合并线程数并减少转储次数(类似于历史库场景),尽可能增加合并速度,减少合并对写入的影响。具体推荐配置参数如下:
memory_limit=0(默认即可,即OB使用物理机操作系统内存上限由memory_limit_percentage参数决定)
memory_limit_percentage=80(建议使用80的配置,保证更多内存分配到cache和sql线程等动态伸缩内存中,保证读数据正常返回)
freeze_trigger_percentage=60%(这里建议调小合并阈值,保证剩余内存能支持冻结后内存未释放期间的批量写入,从而不影响到同步写数据任务)
minor_freeze_times=3-5(该场景下建议ssd和hdd存储介质开启转储,转储可快速释放内存但也存在轻微抖动,转储速度取决于存储介质,ssd>hdd>sata,sata盘在此场景中不建议开启转储)
3、历史库场景
业务特点:读RT与TP类型业务无太大差异,对数据一致性敏感,但对写RT不敏感,SQL类型与TP类业务相似,多为key-value查询,偶尔有统计及聚合类SQL但量不大。读业务存在高峰期,写业务无绝对峰值但存在规律性(同步数据任务为主)。涉及行业及使用场景场景的有金融行业的冷备库、电商行业订单类冷备库等,主要作用为存放有价值的冷数据,提供数据回溯、历史记录查询、历史数据统计等,需保证读RT在毫秒级,写流量持续稳定,存储数据量级一般在TB甚至PB级,故一般存储介质采用sata盘。
配置推荐:根据此类场景的特点,需要控制OB在内存切换时不影响读性能,同时由于存储介质带宽比较低,故还需控制合并效率,参数如下:
memory_limit=0(默认即可,即OB使用物理机操作系统内存上限由memory_limit_percentage参数决定)
memory_limit_percentage=80(根据物理机可用内存大小决定,512G以下的使用80%,512及以上的采用90%)
freeze_trigger_percentage=80%(这里建议将合并阈值调到跟memstore参数一致,保证每日合并次数尽可能少)
minor_freeze_times=0(该场景多为sata盘,sata盘在此场景中不建议开启转储)
sys_bkgd_io_percentage = 90(调整系统IO线程占比到90,保证合并时数据盘带宽能开到最大)
merge_thread_count= 48(默认为0,即由租户线程自动分配合并线程,将其调整到48,使更多线程参与到合并中保证合并时间不会太久)
总结来说,使用OceanBase的过程中需要根据使用场景以及物理机型去调整内存管理参数,使集群性能与业务要求达到最佳匹配,在充分发挥出OceanBase内存表查询低RT的优势的同时,保证了在高可用前提下设备的最大利用率,在性能和性价比之间达到最佳均衡。