Presto基础知识

Presto 优缺点

优点:
- 支持 SQL 标准
- 完全基于内存的并行计算,中间结果不需要落盘,速度很快
- 支持多数据源,你还可以手动实现 connector
- 没有 mapreduce里面stage 的概念,并行度更高
- 自身不带数据,扩缩容方便。美团甚至能白天把服务拉起来,晚上把服务关掉。
- 进行了一些工程上的优化,包括动态编译,使用Slice,GC控制等

缺点:
- 吃内存,特别是大表join可能会oom
- 没有fail over机制,没法处理时间较长的 query
- 需要从数据源拉取数据,一方面会带来额外的开销,一方面rt受数据源长尾worker的影响很严重
- 迭代速度很慢,发布五年后才加上join重排优化


执行过程:

SQL Statement提交 
-> Statement分析,识别关键语法 group by  where orderby 生成AST
-> 解析并生成逻辑执行计划 
-> 逻辑执行计划优化  剪枝、谓词下推 
-> 逻辑计划优化分片  生成n个Fragment ,对应n个Stage
-> 创建调度器构建调度,依据逻辑执行计划生成n个Stage,
-> 根据机器负载情况,构建物理执行计划,多个子任务,每个子任务包含多个算子,分布在不同机器上。
-> Coordinator 将 Stage 串联起来,调度 Worker节点生成Task。
-> Task和Split调度 执行
-> 收集结果输出

Presto 会二次聚合
局部聚合-针对count sum 操作在每个stage 中局部聚合, 最终生成Final 聚合 发给coordinator
中间聚合- Final 聚合前,再进行worker 级别的聚合。减轻coordinator 压力

优化器顺序


OutPutNode -> ProjectNode -> LimitNode -> SortNode -> ProjectNode -> AggregationNode -> ProjectNode -> TableScanNode
在生成了无任何优化的逻辑执行计划以后,下一步,就是对逻辑执行计划进行优化,
比如: 
1.  Limit 下推
2. 无效和无用字段的清理
3. 子查询中的无用的`Sort`的清理(子查询中的排序往往没有意义) 
4. 局部聚合节点的生成(`count(*)`的局部聚合, `TopN`的局部聚合) 
5. `Join`重排序 
6. `CrossJoin`的消除

Presto缓存

引入Presto缓存之前 BackgroundHiveSplitLoader 使用底层的文件系统直接进行数据的读写;
引入Presto缓存机制之后,底层的文件系统被被CachingFileSystem 代理一层

CachingFileSystem 有两个子类,根据你选用的底层缓存引擎的不同可能会是下面的两个之一:
AlluxioCachingFileSystem /əˈluːʒ(ə)n/: 在Presto Worker本地利用磁盘进行了数据的缓存
FileMergeCachingFileSystem: 在本地缓存检查这个数据块是否已经读取过了,通过底层的文件系统来从远端来读取数据,读取到数据之后再添加到本地的磁盘缓存


Presto自己实现FileSystem接口,添加cacheable参数,确定是否缓存数据


如何保证缓存的命中率?
即,相同数据请求打到同一节点上,本地读取缓存进行查询提速
节点分发策略:NodeSelectionStrategy:1.指定节点。 2.尽量指定节点 3.随机指定节点  
将worker节点进行hash计算,优先选择Prefered Nodes,判断是否繁忙TotalSplitCount,若繁忙则再选择最空闲的节点。

Presto数据类型

Type接口封装,定义了类型定长、变长属性。

定长:

Boolean: 用 Byte 来表示, 0-1 表示 是否

float:内部对应real ,底层用int表示,因为block类针对的都是int

其余:Decimal、时间类型、字符串、IpAddress、Geo等

变长:

varchar(n)

Presto Slice

Slice是Presto里面用来对内存高效地、自由地进行操作的接口。是对sun.misc.Unsafe的包装,更加安全易用。


Slice的结构
    Slice里面是通过三个参数来确定一个内存地址: base , address , size
    base:是通过JVM分配出来的内存,在JVM层面是int数组、byte数组的对象,而对Slice来说这就是我们要操作的内存块。
    address:Unsafe常量,表示byte数组里面第一个元素的地址离整个byte数组地址头的偏移量(为什么会有这么一个偏移量?因为数组由元数据和数据组成,默认从16个字节以后开始存储数据)
    size:是我们这块内存的大小,一般来说就是 base 底层所对应的内存的大小(in bytes), 或者更小一点

TupleDomain

TupleDomain 是用来表达 table 里面各个字段的约束条件、取值范围的。内部维护了一个字段名到对应的Domain的映射关系,表示一个表里面多个字段的取值约束条件。
 

Group by 分组聚合操作的逻辑计划执行流程


stage-0 output
stage-1 exchange aggregation project 读取上游outputbuffer数据,聚合
stage-2 tablescan filter project aggregation  扫描数据,过滤,局部聚合

生成Stage

生成逻辑执行计划过程中,递归原root树生成

生成Task

多个Stage,谁首先生成Task?

由ExecutionPolicy决定,默认:AllAtOnceExecutionPolicy

生成几个Task?

Stage两种类型,数据源读数据和非数据源读数据

数据源读数据,根据具体的数据源类型来确定
如果从connector中拿到的spilt是必须本地访问的,task数取决于数据分布的节点数。
如果从connector中拿到的spilt是远程访问的,取决于limit和presto node个数的较小值。

非数据源读数据
单节点或hash_partition_count决定

Task状态

内部接口每100毫秒调用一次TaskInfoFetcher

split 生成 (构造HiveSplitSource)

1.HiveSplitManager获取meta信息 获取partition信息
2.构造HiveSplitLoader和HiveSplitSource
3.Loader多线程异步加载hive的splits将原始split放在Source异步队列中
4.返回Source对象

split 消费 (schedule)


1.getNextBatch调用异步队列。获取split
2.根据maxSplitSize(默认64M)对文件进行拆解,并将拆解信息加入到HiveSplit类对象的构造函数中
    例如:130M文件 拆解成3个split ,start0 length64 \ start1 length 64 \ start2 length2
3.worker节点createOrUpdateTask.taskUpdateRequest.getSources()流式的处理splits。通过split中的start和length认领属于自己的row group进行处理


presto 调度的主类是 SqlQueryScheduler, 其内部会构建 
SqlStageExecution
StageScheduler
StageLinkage

SqlStageExecution 负责调度task, split.调用时计算出 NodePartitionMap 对象,包括:
    splitToBucket, bucketToPartition, partitionToNode  三组映射关系 与 task 和 split 的调度紧密相关
    
    NodePartitionMap 对象的计算与当前 plan fragment 的分区策略有关, 具体分为两类:
    对于非源头 fragment, 分区策略是 SystemPartitioningHandle, 
        根据其分区策略, 当前集群可用节点以及 hash partition count 配置来共同确定该 fragment 需要的计算节点个数后, 再调用 NodeSelector 选出.
    对于源头 fragment, 需要通过 partitioningHandle 找到其对应 Connector 提供的 NodePartitioningProvider 来计算 bucketToNode, 这里以 hive connector 的实现举例.
        如果 connector 是多副本存储系统, 那么在计算 NodePartitionMap 时可以在其 connector 的 NodePartitioningProvider 接口中实现数据分区的副本删选, 分区裁剪, 负载均衡等控制逻辑.


tasks 调度

要调度的 stage 内部可能有序, 每次给出一批要先调度的 stage 集合.

presto 提供 all-at-once 和 phased 两种策略:

all-at-once: 一次调度所有 stage。各个 stage 之间是有序的, 从整个 sub plan 树的叶子节点开始, 自底向上的去调度其对应 stage, 最先调度源头 stage, 最后调度到 root stage.


phased: 遍历整个 sub plan 树, 根据规则切分为多个 plan fragment 的集合, 分为多个阶段去调度, 
在构建 SqlStageExecution 时就已经根据当前 stage 的分区策略确定其 stage 调度器了. 
对于源头 stage 使用 FixedSourcePartitionedScheduler 或 SourcePartitionedScheduler, 
而非源头 stage 使用 FixedCountScheduler.


调度stage 时 ,下发多少 task 到 worker?
在计算 NodePartitionMap 对象就基本已经确定了, 
按每个 partition 下发一个 task, 一个 stage 可能分配多个 task 在同个 worker 上的, 只是目前各个 connector 以及 SystemPartitioningHandle 都实现为 partition 与 node 相对应.


splits 下发

split 下发到哪个 worker?
用到 NodePartitionMap 的三组映射关系, 以 split 为 key 获取该 split 需要被下发到哪个 worker 上.
调度 split 时,还涉及到摆放策略 SplitPlacementPolicy, 即 node 的 split 负载统计以及 split 延迟调度等控制机制.

stage 调度器每完成一次调度, 会产生对应的调度结果 ScheduleResult, 
其中包含哪些新创建的 task 以及哪些 split 被 block 住了, 由于自身 stage 的变化, 导致 presto 需要用这个调度结果对上对下都做一些调整适配,
包括 add exchange location 到 parent stage, 再 add output buffer 到 child stage.


对于源头 stage, 消费的是connector 提供的 split, 这些 splits 其实在 planning 阶段就准备好, 并包装在 stage plan 的 splitSource 里了.
但对于非源头 stage, 消费的是上游 stage 提供的 remote split 
因此需要在上游 stage 调度时获取其 task 摆放信息, 以此创建 remote split 喂给下游 stage, 这样每个 task 就都知道去哪些上游 task pull 数据了.


但是由于前文中提到的每个 ExecutionSchedule 的调度策略不同, 上游 stage 被调度时, 下游 task 可能都没有下发到 worker 上, 
这是会暂时把上游 task 的摆放信息先保存在 exchangeLocations 中, 
等调度到下游 stage 后, 先检查其 exchangeLocations 成员, 再将其初始化为 remote split 塞给其各个 task, 这样保证了不管调度顺序如何, 都不会发生 remote split 泄露.
 

Count(distinct) 优化

select A, count(distinct B) from T group by A.

转换成

select A, count(B) from (select A, B from T group by A, B) group by A.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值