在数据时代 过多耗内存的大查询都有可能压垮整个集群 所以其内存管理模块在整个系统中扮演着非常重要的角色。
而PolarDB-X 作为一款分布式数据库 其面对的数据可能从TB到GB字节不等 同时又要支持TP和AP Workload 要是在计算过程中内存使用不当 不仅会造成TP和AP相互影响 严重拖慢响应时间 甚至会出现内存雪崩、OOM问题 导致数据库服务不可用。
CPU和MEMORY相对于网络带宽比较昂贵 所以PolarDB-X 代价模型中 一般不会将涉及到大量数据又比较耗内存的计算下推到存储DN DN层一般不会有比较耗内存的计算。这样还有一个好处 当查询性能不给力的时候 无状态的CN节点做弹性扩容代价相对于DN也低。
鉴于此 所以本文主要对PolarDB-X计算层的内存管理进行分析 这有助于大家有PolarDB-X有更深入的理解。
PolarDB-X内存管理机制的设计 主要为了几类问题
让用户更容易控制每个查询的内存限制业界解决方案
在计算层遇到的内存问题 业界其他产品也会遇到。这里我们先看下业界对此类问题是如何解决的。
PostgreSQL为了面对内存问题 PG的内存管理有两大特点:
所以PG在生产中要求业务方合理配置内存比例 配置不当的话会极大的降低系统的性能。
一般有两种方式去指导业务方配置 估计方法与计算方法。
第一种是可以根据业务量的大小和类型 一般语句运行时间 来粗略的估计一下。第二种方式是通过对数据库的监控 数据采集 然后计算其大小。
接下来我们看看作为大数据库实时领域非常流行的计算引擎Flink 他的内存管理特点是
Flink相对于PG来说 在计算过程中引入了动态管理机制 但这种机制是有限的。主要应该有两点考虑:?
整个计算是动态的 其实是很难权衡哪个算子抢占权重高 如果任意放开 肯定会影响性能说到计算层的内存管理 就不得不提SPARK。SPARK早在2015年就提出了Project Tungsten。
当然钨丝计划的提出 主要是为了不要让硬件成为Spark性能的瓶颈 无限充分利用硬件资源。而其中内存管理的设计不仅仅可以减少JVM内存释放的开销和垃圾回收的压力 结合落盘能力可以支持海量数据的查询。
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是由 Facebook 推出的一个基于Java开发的开源分布式SQL查询引擎。它的特点是:
采用的是逻辑内存池 来管理不同类型的内存需求。考虑到Presto是基于逻辑计数方式做内存管理 所以是不太精准的。但逻辑计算的好处是 管理内存成本比较低 这又往往适合对延迟要求特别低的在线计算场景。
业界基本上都会将内存分区域管理 针对于计算层主要区别在于: 内存是否预分配和查询过程中是否支持动态抢占。而PolarDB-X 是存储计算分离的架构 CN节点是无状态的 且主要针对于HTAP场景 所以对实时性要求比较高。
能否充分使用内存 对我们的场景比较关键。所以我们采取类似Presto这种无预分配内存 且支持动态抢占内存方案。除此之外 我们会根据不同的WorkLoad来管理内存 充分满足HTAP场景。
统一的内存模型PolarDB-X内存也是分区域管理。结合自身的计算特点 按照不同的维度会有不同的划分和使用方式。
结合HTAP特性 我们将内存池划分为相对于业界其他产品来说 我们主要划分出了AP和TP的内存区域。这么做是为了做TP/AP Workload在内存使用上的资源隔离。
一个TP Workload的查询 我们会从TP Memory 申请出Query Memory 然后按照我们的计算层次关系 又会划分出Task/Pipeline/Driver/Operator Memory。
这样的好处是 我们可以动态监测不同维度下的内存使用情况。这里唯一要注意的是在Query Memory下 我们额外会创建一个Planner Memory 用于管理查询时和DN交互的数据对象的内存。
结合算子对内存申请和释放的行为 我们对一个Operator Memory划分为两个内存块: Reserve和Revoke。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。
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。
Driver被内存阻塞主要条件是
当内存池子(AP/TP Memory Pool)内存不足阈值的0.8之时 申请内存的算子会在下一刻会被暂停执行 等待被触发数据落盘 以到达释放内存的目的。当Driver被内存阻塞后 Driver会退出执行线程 将线程资源拱手相让给其他Driver。同时会回调MemoryRevokeService服务 该服务主要是基于内存大小和Pipeline的依赖关系挑选出耗内存的Driver进行标记。
当内存不够时 CN会基于Used Memory Size对Task做排序 依次对占用大内存的Task打标记 Taks内部则基于依赖关系 对Pipeline做排序 依次对父节点的Pipeline打标 Pipeline内部首先基于Driver Used Memory Siz做排序后 再结合Operator前后依赖关系 来决定Operator的内存释放顺序。
这里唯一需要注意的是MemoryRevokeService只是对需要释放内存的Operator进行打标记 然后唤醒对应的Driver 等待被调度的时候由执行线程池来触发数据落盘。
这里可能会有一个疑问 为什么打标记的同时不立刻触发数据落盘释放内存呢。由于Driver被落盘标记的时候 也有可能正在执行线程池执行 如果这个时候触发Driver执行落盘操作的时候 Driver会有并发安全的问题。所以我们将Driver的落盘动作和执行动作都交给执行线程池统一处理。
从上图还可知 一旦Driver被MemoryRevokeService唤醒 就调度到执行线程池中 首先做自我巡检 判断当前Driver是否包含SPILL标记 如果是的话 就需要触发Driver上相应算子做数据落盘的动作 这里唯一需要注意的是算子真正做数据落盘的逻辑是完全异步的 不占用执行线程。
一旦Driver开始做异步落盘后 也会主动退出执行线程 直到异步落盘结束后 才会唤醒当前Driver。整个过程确保了执行线程的资源不会被卡住。
PolarDB-X在HTAP场景下是希望做到AP Workload不影响TP Workload。但当系统承载的都是AP Workload时 AP可以充分利用所有资源 当系统承载的都是TP Worload时 TP可以充分利用所有的资源。这里头我们巧妙了利用了TP Memory的阈值来到达目的。
Driver 执行过程中 按需申请内存 如果内存足够 会反复被调度执行其中系统触发AP Worload自杀的回调机制 是比较危险的 默认是关闭的。而一旦触发AP Worload限流 则令牌桶流入的速率会减半 这样接下来的AP workload请求可能获取不到Token 而只能排队等待。
默认我们将TP Memory Pool的值设置为内存最大可利用资源的80% 表示在没有AP查询时 TP可以使用100%资源 在没有TP查询时 AP可以使用100%资源 当TP和AP Worload比较高时 AP最大只能占用20%的资源。
测试了TPCC和TPCH混跑情况下 AP 对TP的影响。这部分测试也涵盖了CPU的资源隔离 后面我们会单独开一篇谈谈PolarDB-X基于负载的资源隔离技术。在不开启TPCH的情况下 tpmc保持在3.2w-3.4w之间; 再开启TPCH后 tpmc基本可以维持在3w以上 且有抖动10%左右。
总结
从上表对比来看
PolarDB-X算子不预分配内存 而是在计算过程中按需去申请内存 可以充分提高内存利用率从业界产品来看 每个产品在内存管理上都有各自的特点 这和产品本身的定位是有一定关系。而PolarDB-X作为一款计算存储分离的HTAP数据库来说 其计算层目前采用的完全动态内存抢占方案 可以做到充分使用内存 避免AP对TP的影响。
欢迎大家对我们持续关注
2021年3月24日,主题为《数据的世界,世界的数据》的星环科技2021春季新品发布会...
前提条件 请您在购买前确保已完成注册和充值。详细操作请参见 如何注册公有云管...
本文整理自直播《Hologres 数据导入/导出实践-王华峰(继儒)》 视频链接: https:/...
在Python语言中有如下3种方法: 成员方法 类方法(classmethod) 静态方法(staticm...
从 10.0.0 版开始,异步迭代器就出现在 Node 中了,在本文中,我们将讨论异步迭...
【51CTO.com快译】 数据可视化工具不断发展,提供更强大的功能,同时改善可访问...
建站 什么 虚拟主机 够用?这要看搭建的是什么类型的网站。比如个人博客类型的网...
信息化2.0时代提出开展智慧教育创新发展行动。2019年2月,中共中央、国务院印发...
Docker生成新镜像版本的两种方式 There are two ways Docker can generate new m...
摘要 元旦期间 订单业务线 告知 推送系统 无法正常收发消息,作为推送系统维护者...