PolarDB-X 是如何用15M内存跑1G的TPCH

本文涉及的产品
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
云原生数据库 PolarDB 分布式版,标准版 2核8GB
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介: 本文主要对PolarDB-X计算层的内存管理进行分析,这有助于大家有PolarDB-X有更深入的理解。PolarDB-X内存管理机制的设计,主要为了几类问题:1. 让用户更容易控制每个查询的内存限制;2. 预防内存使用不当,导致内存溢出进而引发OOM;3. 避免查询间由于内存争抢出现相互饿死现象;4. 避免AP Workload使用过多内存,严重拖慢TP Workload

在数据时代,过多耗内存的大查询都有可能压垮整个集群,所以其内存管理模块在整个系统中扮演着非常重要的角色。


而PolarDB-X 作为一款分布式数据库,其面对的数据可能从TB到GB字节不等,同时又要支持TP和AP Workload,要是在计算过程中内存使用不当,不仅会造成TP和AP相互影响,严重拖慢响应时间,甚至会出现内存雪崩、OOM问题,导致数据库服务不可用。


CPU和MEMORY相对于网络带宽比较昂贵,所以PolarDB-X 代价模型中,一般不会将涉及到大量数据又比较耗内存的计算下推到存储DN,DN层一般不会有比较耗内存的计算。这样还有一个好处,当查询性能不给力的时候,无状态的CN节点做弹性扩容代价相对于DN也低。


鉴于此,所以本文主要对PolarDB-X计算层的内存管理进行分析,这有助于大家有PolarDB-X有更深入的理解。


PolarDB-X内存管理机制的设计,主要为了几类问题:

  1. 让用户更容易控制每个查询的内存限制;
  2. 预防内存使用不当,导致内存溢出进而引发OOM;
  3. 避免查询间由于内存争抢出现相互饿死现象;
  4. 避免AP Workload使用过多内存,严重拖慢TP Workload



业界解决方案


在计算层遇到的内存问题,业界其他产品也会遇到。这里我们先看下业界对此类问题是如何解决的。

PostgreSQL

1.jpg


为了面对内存问题,PG的内存管理有两大特点:

  1. 内存按照一定的比例,分为四大区域(Shared Buffers、Temp Buffers、Work Mem、Maintenance Work Mem);
  2. 在查询过程算子主要使用Work Mem区域内存,且每个算子都是预先分配固定内存的,但内存不够时,需要与硬盘进行swap;


所以PG在生产中要求业务方合理配置内存比例,配置不当的话会极大的降低系统的性能。

一般有两种方式去指导业务方配置:估计方法与计算方法

第一种是可以根据业务量的大小和类型,一般语句运行时间,来粗略的估计一下。第二种方式是通过对数据库的监控,数据采集,然后计算其大小。


Flink


2.jpg

接下来我们看看作为大数据库实时领域非常流行的计算引擎Flink,他的内存管理特点是:

  1. 引入了一套基于字节流的数据结构,在JVM虚拟机上实现了类似C申请和释放内存的池化管理;
  2. 内存主要分为三块: 网络内存池、系统预留的内存池、计算内存池。
  3. 为每个算子设置一个[min-memory, max-meory] 调度上可以保证每个算子初始化的时候预先分配min memory,在计算过程中不断从计算内存池申请内存,当申请失败或者内存使用超过算子预先定义max memory的时候,主动触发数据落盘。


Flink相对于PG来说,在计算过程中引入了动态管理机制,但这种机制是有限的。主要应该有两点考虑:

  1. 整个计算是动态的,其实是很难权衡哪个算子抢占权重高,如果任意放开,肯定会影响性能;
  2. Flink是流水线计算模型,类似PartialAgg算子一定不落盘,所以一旦PartialAgg占用过多内存,导致下游没有内存可用,而PartialAgg又一定会往下游发送数据,这样必然会导致Dead Lock。



Spark


说到计算层的内存管理,就不得不提SPARK。SPARK早在2015年就提出了Project Tungsten。

当然钨丝计划的提出,主要是为了不要让硬件成为Spark性能的瓶颈,无限充分利用硬件资源。而其中内存管理的设计不仅仅可以减少JVM内存释放的开销和垃圾回收的压力,结合落盘能力可以支持海量数据的查询。


3.jpg


Spark 内存管理和Flink很像的,这里就不展开来说。但是它支持完全动态内存抢占,这种动态内存抢占主要体现在:Excution和Storage内存可以相互强制;Task间可以相互抢占;Task内部可以相互抢占。Spark这里假设了每个Task动态抢占内存的能力相当于,理论值都是1/N (N表示进程内部Task数量)。

Spark其实是可以这么假设的,他是典型的MR模型,只有Map端计算完后,才调度Reduce端,所有在一个进程内部多个Task你可以简单认为都是Map Task。当某个Task使用的内存不足1/2N时,它会等到其他Task释放内存。而Task间最多可以抢占的阈值也是1/N,而Task内算子是可以完全相互动态抢占的。

Spark的计算模型决定了他可以做到不预先为每个Task和算子预先分配内存,随着N的变化,动态调整每个Task的抢占能力。个人觉得这种动态抢占内存的方案,相对于其他计算引擎优越不少。但是由于Spark主要是针对大数据的ETL场景,稳定性至关重要,尽量避免任务重试。所以对每个Task抢占的上界都做了约束(Task Memory < 1/N * Executor Memory),确保每个Task都有内存执行任务。


Presto


4.jpg

Presto是由 Facebook 推出的一个基于Java开发的开源分布式SQL查询引擎。它的特点是:

  1. 采用的是逻辑内存池,来管理不同类型的内存需求。
  2. Presto整体将内存分为三块: System Pool、Reserved Pool、General Pool。
  3. Presto内存申请其实是不预分配的,且抢占是完全随机的,理论上不设定Task抢占的上限,但为了出现某个大查询把内存耗尽的现象,所以后台会有定时轮训的机制,定时轮训和汇总Task使用内存,限制Query使用的最大内存;同时当内存不够用的时候,会对接下来提交的查询做限流,确保系统能够平稳运行。


考虑到Presto是基于逻辑计数方式做内存管理,所以是不太精准的。但逻辑计算的好处是,管理内存成本比较低,这又往往适合对延迟要求特别低的在线计算场景。


PolarDB-X内存管理


业界基本上都会将内存分区域管理,针对于计算层主要区别在于: 内存是否预分配和查询过程中是否支持动态抢占。而PolarDB-X 是存储计算分离的架构,CN节点是无状态的,且主要针对于HTAP场景,所以对实时性要求比较高。

能否充分使用内存,对我们的场景比较关键。所以我们采取类似Presto这种无预分配内存,且支持动态抢占内存方案。除此之外,我们会根据不同的WorkLoad来管理内存,充分满足HTAP场景。

统一的内存模型

PolarDB-X内存也是分区域管理。结合自身的计算特点,按照不同的维度会有不同的划分和使用方式。

  • 结合HTAP特性,我们将内存池划分为:
  1. System Memory Pool:系统预留的内存,主要用于分片元数据和数据结构、临时对象;
  2. Cache Memory Pool:用于管理Plan Cache和其他LRU Cache的内存;
  3. AP Memory Pool: 用于管理WorkLoad是AP的内存;
  4. TP Memory Pool: 用于管理WorkLoad是TP的内存;


5.jpg

相对于业界其他产品来说,我们主要划分出了AP和TP的内存区域。这么做是为了做TP/AP Workload在内存使用上的资源隔离。

  • 结合我们的计算层次结构,我们按照树形结构去管理内存:


6.jpg

一个TP Workload的查询,我们会从TP Memory 申请出Query Memory,然后按照我们的计算层次关系,又会划分出Task/Pipeline/Driver/Operator Memory。

这样的好处是,我们可以动态监测不同维度下的内存使用情况。这里唯一要注意的是在Query Memory下,我们额外会创建一个Planner Memory,用于管理查询时和DN交互的数据对象的内存。

  • 结合算子对内存申请和释放的行为,我们对一个Operator Memory划分为两个内存块: Reserve和Revoke。


7.jpg

Reserve Memory顾名思义就是算子一旦申请了内存就一定不会被动释放,直到该算子运行结束,主动释放内存,比如Scan算子,主要和DN交互数据的,一般都采样流式实现,每次运行都从DN获取一批数据,然后发送给下游,该算子一般不支持数据落盘,所以Scan算子都是申请Reseve Memory。

而Revoke Memory表示算子申请内存后,可以通过触发数据落盘的方式被动释放,将申请到的Revoke Memory归还到内存池,以供其他算子使用。比如HashAgg算子,都是申请Revoke Memory,但它申请的内存过多,导致这个查询其他算子无内存可用,就会被框架触发HashAgg数据落盘,将其申请的内存统统归还给内存池。

这里需要注意PolarDB-X对于Reserve和Revoke两块内存并没有按比例严格划分,这两者是可以完全相互抢占的。



内存对象

对于内存管理上,比较重要的实现类就是Memory Pool和MemoryAllocatorCtx。其中Memory Pool提供了申请和释放内存的API。

考虑到内存是按树形结构管理起来的,申请一次内存会涉及到多次函数调用,所以这里封装了MemoryAllocatorCtx对象,用于确保每次申请/释放内存的最小单位是512Kb,避免对MemoryPool相关函数多次调用的开销。且MemoryAllocatorCtx对象会记录上一次内存申请失败的大小,是框架用于判断内存是否不够用的重要标志。


interface MemoryPool {//申请reserve 内存ListenableFuture<?> allocateReserveMemory(long size);//尝试着申请reserve 内存boolean tryAllocateReserveMemory(long size, ListenableFuture<?> allocFuture);//申请revoke 内存ListenableFuture<?> allocateRevocableMemory(long size);//释放reserve 内存void freeReserveMemory(long size);//释放revoke 内存void freeRevocableMemory(long size);}


在内存的使用上,我们采样的是非预分配模型,在计算调度上不需要额外考虑每个算子的使用内存。每个算子都是在执行期间按需去申请释放内存的,但这并不是意味着算子就可以任意去申请内存。

一旦所有的算子使用内存之和超过查询规定的最大内存,或者内存不够用的时候,我们都会阻塞当前查询,为此我们在原有的MemoryPool基础之上派生出了BlockingMemory;同样的为了方便AP和TP Workload基于内存做自适应的资源隔离,我们进一步派生出了AdaptiveMemory。


8.jpg

BlockingMemory:内存申请过中,会根据内存是否超过一定的阈值,创建阻塞对象,该对象会被执行线程引用。一般来说全局内存或者Query内存超过一定的阈值(0.8)的时候,就会触发当前算子申请内存失败,并且主动退出执行,这个和Spark算子申请不到内存后,阻塞当前线程的行为是不一致的。

AdaptiveMemory:用于做TP/AP的内存管理,在申请过程中会根据AP和TP的内存占比做一些自适应调整,比如触发限流、query自杀、大查询落盘等操作。


动态抢占内存机制

在内存申请和释放的行为上,我们采样的都是非阻塞模型,这种设计可以很好的和我们的时间片执行框架结合。结合这种设计,PolarDB-X算子都是不预分配内存的,各个算子都是在运行过程中完全动态抢占内存。

这钟动态抢占内存主要体现在: AP Memory 和TP Memory之间、Query Memory之间以及Operator Memory的Revoke和Reserve之间。

而实现这种抢占机制的基本单元依然是Driver,由下图可知,Driver会在运行过程中会根据当前Worload的内存空间、Query Used Memory 和 Operator Used Memory,来判断执行线程是否需要让出执行线程,被内存阻塞。一旦发现内存不够用的话,会主动退出执行队列,加入到阻塞队列,直到内存空间满足一定的条件,才会唤醒该Driver。


9.jpg


Driver被内存阻塞主要条件是:

  1. 当内存池子(AP/TP Memory Pool)内存不足阈值的0.8之时,申请内存的算子会在下一刻会被暂停执行,等待被触发数据落盘,以到达释放内存的目的。
  2. 当前查询申请的总内存超过当前Max Query Memory阈值的0.8之后,申请的算子会被暂停执行等待被被触发数据落盘,以到达释放内存的目的。


当Driver被内存阻塞后,Driver会退出执行线程,将线程资源拱手相让给其他Driver。同时会回调MemoryRevokeService服务,该服务主要是基于内存大小和Pipeline的依赖关系挑选出耗内存的Driver进行标记。


10.jpg


当内存不够时,CN会基于Used Memory Size对Task做排序,依次对占用大内存的Task打标记;Taks内部则基于依赖关系,对Pipeline做排序,依次对父节点的Pipeline打标;Pipeline内部首先基于Driver Used Memory Siz做排序后,再结合Operator前后依赖关系,来决定Operator的内存释放顺序。

这里唯一需要注意的是MemoryRevokeService只是对需要释放内存的Operator进行打标记,然后唤醒对应的Driver,等待被调度的时候由执行线程池来触发数据落盘。

这里可能会有一个疑问?为什么打标记的同时不立刻触发数据落盘释放内存呢。由于Driver被落盘标记的时候,也有可能正在执行线程池执行,如果这个时候触发Driver执行落盘操作的时候,Driver会有并发安全的问题。所以我们将Driver的落盘动作和执行动作都交给执行线程池统一处理。


11.jpg


从上图还可知,一旦Driver被MemoryRevokeService唤醒,就调度到执行线程池中,首先做自我巡检,判断当前Driver是否包含SPILL标记,如果是的话,就需要触发Driver上相应算子做数据落盘的动作,这里唯一需要注意的是算子真正做数据落盘的逻辑是完全异步的,不占用执行线程。

一旦Driver开始做异步落盘后,也会主动退出执行线程,直到异步落盘结束后,才会唤醒当前Driver。整个过程确保了执行线程的资源不会被卡住。


基于负载的内存隔离


1.jpg

PolarDB-X在HTAP场景下是希望做到AP Workload不影响TP Workload。但当系统承载的都是AP Workload时,AP可以充分利用所有资源;当系统承载的都是TP Worload时,TP可以充分利用所有的资源。这里头我们巧妙了利用了TP Memory的阈值来到达目的。

  1. Driver 执行过程中,按需申请内存,如果内存足够,会反复被调度执行;
  2. 当Driver被内存阻塞时,判断其Workload;
  3. 如果是AP,则通过MemoryRevokeService服务触发数据落盘;如果是TP Workload,则在触发数据落盘的同时,会判断当前TP Memory是否超过TP Memory Pool,若不是,则说明AP Workload使用内存过多,则回调AP Workload做调整;
  4. 回调的方式有两种:触发部分AP Worload自杀,且触发Ap Worload 基于令牌桶的方式做滑动限流。


其中系统触发AP Worload自杀的回调机制,是比较危险的,默认是关闭的。而一旦触发AP Worload限流,则令牌桶流入的速率会减半,这样接下来的AP workload请求可能获取不到Token,而只能排队等待。

默认我们将TP Memory Pool的值设置为内存最大可利用资源的80%,表示在没有AP查询时,TP可以使用100%资源,在没有TP查询时,AP可以使用100%资源,当TP和AP Worload比较高时,AP最大只能占用20%的资源。


数据测试

基于15MB的内存跑通1g TPCH,这里我们截取了几个比较耗内存的Query统计了落盘文件的数量。


2.jpg

测试了TPCC和TPCH混跑情况下,AP 对TP的影响。这部分测试也涵盖了CPU的资源隔离,后面我们会单独开一篇谈谈PolarDB-X基于负载的资源隔离技术。在不开启TPCH的情况下,tpmc保持在3.2w-3.4w之间; 再开启TPCH后,tpmc基本可以维持在3w以上,且有抖动10%左右。


3.jpg


总结


4.jpg

从上表对比来看:

  1. PolarDB-X算子不预分配内存,而是在计算过程中按需去申请内存,可以充分提高内存利用率;
  2. 支持Workload/Query/Operator维度的内存动态抢占,比较适合HTAP场景,可以通过设置不同维度的阈值,尽可能确保在内存上TP Workload不受AP的干扰;
  3. 相对于PostgreSQL/Flink/Spark来,PolarDB-X释放内存的时机是Lazy的。就是说当算子内存不够的时候,算子并不是立即落盘释放内存的,而是退出执行线程,由另外一个服务计算出更加耗内存的算子,这种方案可以挑选更加耗内存的查询或者算子做数据落盘,避免对小查询造成影响。但Lazy的方式存在一定的风险,可能在一瞬间内存未及时释放,而运行的查询申请内存过多,导致OOM,但好在PolarDB-X是计算存储分离的架构,计算层是无状态的;
  4. PolarDB-X 相对于Presto 动态内存抢占的粒度更加细,将Workload也纳入考虑。但Presto在实现的细节上会考虑大查询间内存的相互影响。而在HTAP场景下,我们更加注重AP对TP的影响,AP内查询间并没有额外的机制去保证内存不受影响。


从业界产品来看,每个产品在内存管理上都有各自的特点,这和产品本身的定位是有一定关系。而PolarDB-X作为一款计算存储分离的HTAP数据库来说,其计算层目前采用的完全动态内存抢占方案,可以做到充分使用内存,避免AP对TP的影响。


欢迎大家对我们持续关注!


相关实践学习
Polardb-x 弹性伸缩实验
本实验主要介绍如何对PolarDB-X进行手动收缩扩容,了解PolarDB-X 中各个节点的含义,以及如何对不同配置的PolarDB-x 进行压测。
相关文章
|
关系型数据库 数据库 PostgreSQL
PostgreSQL 内存表可选项 - unlogged table
标签 PostgreSQL , 内存表 , unlogged table 背景 内存表,通常被用于不需要持久化,变更频繁,访问RT低的场景。 目前社区版本PostgreSQL没有内存表的功能,postgrespro提供了两个插件可以实现类似内存表的功能。
3100 0
|
3月前
|
人工智能 算法 BI
【经典问题】给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
【1月更文挑战第26天】【经典问题】给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
|
9月前
|
SQL 存储 缓存
PostgreSQL内存上下文[翻译]
PostgreSQL内存上下文[翻译]
161 0
|
11月前
|
数据采集 存储 缓存
腾讯二面:有 40 亿个 QQ 号,限制 1G 内存,问如何去重?被问懵了!
腾讯二面:有 40 亿个 QQ 号,限制 1G 内存,问如何去重?被问懵了!
|
关系型数据库 PostgreSQL 索引
PostgreSQL cheat functions - (内存上下文\planner内容\memory context等常用函数)
标签 PostgreSQL , memory context , plan , pid signal , ... 背景 https://github.com/MasaoFujii/pg_cheat_funcs Fujii Masao MasaoFujii PostgreSQL comm...
1271 0
|
新零售 关系型数据库 测试技术
PostgreSQL技术周刊第10期:PostgreSQL 调用 Rust 函数内存耗用研究
PostgreSQL(简称PG)的开发者们:云栖社区已有5000位PG开发者,发布了3000+PG文章(文章列表),沉淀了700+的PG精品问答(问答列表)。 PostgreSQL技术周刊会为大家介绍最新的PG技术与动态、预告活动、最热问答、直播教程等,欢迎大家订阅PostgreSQL技术周刊。
3172 0
|
关系型数据库 PostgreSQL Rust
|
关系型数据库 PostgreSQL

相关产品

  • 云原生分布式数据库 PolarDB-X
  • 云原生数据库 PolarDB