PS:基于 presto-0.258
整体流程
接收语句
DispatchManager createQueryInternal
queryPreparer.prepareQuer // preparedQuery [封装Statement]
dispatchQueryFactory.createDispatchQuery => DispatchQuery
resourceGroupManager.submit(preparedQuery.getStatement(), dq, selectionContext, queryExecutor)
提交成功
InternalResourceGroup run (LocalDispatchQuery)
InternalResourceGroup startInBackground
LocalDispatchQuery waitForMinimumWorkers
LocalDispatchQuery startExecution
SqlQueryExecution start
开始执行
PlanRoot plan = analyzeQuery();
planDistribution(plan);
scheduler.start(); // SqlQueryScheduler
一些细节
hive表的元数据访问
元数据总体由 HiveMetadata维护,里面包含metastore连接,partitionManager以及一些辅助方法。
获取表的元数据
StatementAnalyzer visitTable
TableMetadata tableMetadata = metadata.getTableMetadata(session, tableHandle.get());
ConnectorMetadata metadata = getMetadata(session, connectorId); -> HiveMetadata
解析一些
HiveStorageFormat
properties
partitionedBy
bucketProperty
preferredOrderingColumns
orcBloomFilterColumns
orcBloomFilterFfp
comment
等信息
封装到ConnectorTableMetadata
Source Split的切分
从plan里createStageScheduler
splitSourceProvider // 这里会出现HiveTableLayoutHandle 描述了表的目录 分区 字段 谓词等 甚至有tableParameters
HiveSplitSource allAtOnce //返回的是HiveSplitSource实例 封装了一个AsyncQueue队列去存储split
HiveSplitSource getNextBatch //这是每一批
BackgroundHiveSplitLoader loadSplits //这里触发分区 文件的迭代 和split计算 。。。
StoragePartitionLoader loadPartition //这里有个 DirectoryLister 【重点关注】
这里夹杂几种情况
SymlinkTextInputFormat
shouldUseFileSplitsFromInputFormat(inputFormat))
InputSplit[] splits = inputFormat.getSplits(jobConf, 0); 去拿到split 。。
if (tableBucketInfo.isPresent()) {
不同情况解析split的逻辑不一样
正常情况是非bucket普通表
是用DirectoryLister去list分区目录path 一个文件对应一个InternalHiveSplit(也可能被path filter过滤)
Optional createInternalHiveSplit(HiveFileInfo fileInfo
这里的逻辑:
1)提取 List addresses
2)计算分区这个文件的相对路径 URI relativePath = partitionInfo.getPath().relativize(path.toUri());
上面返回的只是InternalHiveSplit 还需要在 HiveSplitSource的getNextBatch里变成HiveSplit
queues.borrowBatchAsync(bucketNumber xxx 触发future list目录任务 。。
最后对外输出的是 HiveSplit【封装了一大堆东西。。基于maxSplitSize算出来的 即一个文件 可能有多个】
关于split元数据这块比spark调度要好很多 因为是流式的 不是静态的集合 。。 内存需求会少很多。
最主要的是ListenableFuture> future = hiveSplitSource.addToQueue(splits.next());
最后输出的HiveSplit在一个PerBucket + AsyncQueue 组合的复杂的队列缓存结构里
节点选择 [SOFT Affinity scheduler]
- 这里实际上是用path的哈希取模所有节点 得到固定的目标节点映射列表
(好像忽略了文件实际位置。。但是因为这有缓存 包括文件的 所以可能是综合考虑 如果是hard的话 是不是可能不均衡 ?) - 貌似只适合于存算分离的架构。。
- 如果是存算一体的 建议选HARD Affinity ,即类似spark的preference local node
缓存(Raptorx中的特性)
- 1)文件 cache 【coordinater上 放内存】【done】
本质是guava的Cache> cache类实例 分区目录也假设为不动的。。
This can only be applied to sealed directories
见:StoragePartitionLoader.createInternalHiveSplitIterator
boolean cacheable = isUseListDirectoryCache(session);
if (partition.isPresent()) {
// Use cache only for sealed partitions
cacheable &= partition.get().isSealedPartition();
}
文件的list是根据 hdfs 的 remoteIterator 迭代的 。。不像spark 跑了并行任务去获取location信息 全部一起缓存 。。
- 2)tail/footer cache【在节点上 也是放内存】
注:OrcDataSource这个类和tail/footer没关系 只是封装了流读取的一些入口
这个类是必须要打开至少一次ORC文件的
HiveClientModule -> createOrcFileTailSource 里决定了是否启用缓存 。。
Cache cache
具体来说
OrcReader里面的两个主要元数据 都来自 orcFileTailSource提供的OrcFileTail // Slice 里保存了 byte[]
private final Footer footer; // 文件级别的统计 stripe摘要
private final Metadata metadata; //stripe级的统计
还有stripe的StripeMetadataSource -> 这个类提供获取StripeFooter的方法
(StripeFooter 包含一堆Stream 即各列数据信息 以及索引信息 StripeReader会用 selectRowGroups )
这里面会判断是否要缓存isCachedStream
return streamKind == BLOOM_FILTER || streamKind == ROW_INDEX;
注意:这个方法调用时是传入OrcDataSource的 所以能拿到ORC文件流 但是之后就不需要这个流了。seek 等也不需要了。
OrcFileTail orcFileTail = orcFileTailSource.getOrcFileTail(OrcDataSource orcDataSource)
谓词裁剪(plan层)
- 1)分区裁剪
SqlQueryExecution analyzeQuery
logicalPlanner plan
IterativeOptimizer【这个类类似于scala里面的模式匹配 不同的规则去catch其对应的语法树节点去执行逻辑】
而所有的规则都在 PlanOptimizers 去添加 每个匹配逻辑是一个Rule类的实现
如PickTableLayout 有一个规则是pickTableLayoutForPredicate
hivePartitionResult = partitionManager.getPartitions(
这里如果有谓词 where 就会把tablescan替换成FilterNode(里边包含tablescan)
这样就完成了查询计划的替换
分区裁剪过程【这里很抽象 谓词传递 命名很不好理解 。。。】
- 2)谓词表示体系
重要
这里要解释一个较Domain的类。。实际上就是表示某个值的范围(离散值,范围,无穷等)
以及其服务类:TupleDomain 。。是限定了字段 + 值范围的组合
(PS:这命名实在让人别扭。)
参考 TestTupleDomainFilter
还搞了个缓存去防止多次解析 。。
TupleDomainFilterCache -> Converting Domain into TupleDomainFilter is expensive, hence, we use a cache keyed on Domain
传递到下游的时候 是TupleDomain domainPredicate
这里面Subfield是一个可以多层表达的字段表示
TupleDomain 是一个泛型Map 大概就是<字段 值范围>的一个模式。
Constraint
// 这又是另一个表示条件的类 。。里面封装了 TupleDomain summary;
// 和另一个 Optional>> predicate 这个是Java Function接口里面的Predicate
// 有几个主要方法 and/or/test -> 得到返回值是Boolean抽象 。
这里面涉及到的泛型有
ColumnHandle -> 一个空接口 这是presto spi 定义的 各个connector可能有不同实现
Map effectivePredicate -> 这个Column就是Hive元数据里Table下的列,获取分区列表时候用到
HiveColumnHandle -> hive的实现
HivePartition -> Map getKeys() //表示field -> value
读split逻辑
具体的task读的是 hiveSplit
弄清楚split切分逻辑【】
worker上的调用链:
PrioritizedSplitRunner process
DriverSplitRunner processFor
Driver processInternal
xxOperator getOutput -> 触发计算
HivePageSourceProvider createHivePageSource
OrcBatchPageSourceFactory createOrcPageSource
之后就是ORC的解析 OrcReader -> OrcRecordReader 去读取到presto的page相关逻辑了。
是否缓存文件footer元数据 不只是开启了cache配置 还需要选择的split节点在期望节点里 才会去缓存 。即 和nodeSelector策略有关。而且这个缓存 是以每个文件粒度调度的 。(包含在hiveSplit里面)
梳理stage/task/driver/split的并发关系
- Query 根据SQL语句生成查询执行计划,进而生成可以执行的查询(Query),一个查询执行由Stage、Task、Driver、Split、Operator和DataSource组成
- Stage 执行查询阶段 Stage之间是树状的结构 ,RootStage 将结果返回给coordinator ,SourceStage接收coordinator数据 其他stage都有上下游 stage分为四种 single(root)、Fixed、source、coordinator_only(DML or DDL)
- Exchange 两个stage数据的交换通过Exchange 两种Exchange ;Output Buffer (生产数据的stage通过此传给下游stage)Exchange Client (下游消费);如果stage 是source 直接通过connector 读数据
- 一个Task包含一或多个Driver,是作用于一个Split的一系列Operator集合。一个Driver用于处理一个Split产生相应输出,输出由Task收集并传递给下游Stage中的Task
核心问题
1)task个数
正常就是1个stage节点个数个,而presto会尽可能使用资源。每个stage每个节点都有一个task。(当然是非root stage)
2)driver个数
其实就是split个数
3)split个数(根据stage的类型不同而不同)
single(root)-> 1个
coordinator only -> 元数据操作 也是一个
如果是source的stage -> 由connector的splitmanager决定
一个文件最少一个split
remainingInitialSplits 有个参数影响了maxSplitBytes // 如果计算次数少于remainingInitialSplits 会采用 maxInitialSplitSize
否则用配置的maxSplitSize去滚动每个文件生成HiveSplit
(最后2个split会平衡 避免过小的split 导致时间不太均衡...)
hive.max-split-size
hive.max-initial-splits(默认200 不调节也行。。需要调节 maxInitialSplitSize 如果不设置就是默认 maxSplitSize/2 )
hive.max-initial-split-size
如果是中间stage -> hash_partition_count 这个session 参数?还是 task.concurrency ?
举例说明:对与读取hive表来说,1G的数据,设置 hive.max-split-size = 64MB,hive.max-initial-split-size= 64MB,最后才会得到期望的1G/64MB个source split
线程并发模型
- task.max-worker-threads // worker启动的线程池的大小,即工作线程个数
- task.concurrency // set session task_concurrency=1; 这个影响 agg/join 的并发
- task.min-drivers // 默认是 task.max-worker-threads x2 ,worker最少在执行的split数,如果有足够资源和任务
- task.min-drivers-per-task // task最少并行执行的split数
- initial_splits_per_node // 。。(应该是调度时候)
在taskExecutor的enqueueSplits里
for (SplitRunner taskSplit : taskSplits) {
xxx
scheduleTaskIfNecessary(taskHandle); //按task级别调度 会用到 task.min-drivers-per-task 即可并发运行的split
addNewEntrants();
//在资源变动( 如task remove/split finish/等时候 去尝试去调度更多split 【这里比较模糊。。】用到 task.min-drivers 参数 )
//比如 task.min-drivers-per-task 是4 task.min-drivers是10 则相当于进行了2次调度 。。
}
在Presto中有一个配置query.execution-policy,它有两个选项,一个是all-at-once,另一个是 phased // set session execution_policy='phased';
线程和并发模型:
SqlTaskExecutionFactory -> SqlTaskExecution
Coordinator分发Task到对应Worker,通过HttpClient发送给节点上TaskResource提供的RESTful接口
Worker启动一个SqlTaskExecution对象或者更新对应对象需要处理的Split
这里能看到每个split其实对应一个driverSplitRunner(这个类里面有DriverSplitRunnerFactory)
// Enqueue driver runners with split lifecycle for this plan node and driver life cycle combination.
ImmutableList.Builder runners = ImmutableList.builder();
for (ScheduledSplit scheduledSplit : pendingSplits.removeAllSplits()) {
// create a new driver for the split
runners.add(partitionedDriverRunnerFactory.createDriverRunner(scheduledSplit, lifespan));
}
enqueueDriverSplitRunner(false, runners.build());
在DriverSplitRunner的Process方法里
this.driver = driverSplitRunnerFactory.createDriver(driverContext, partitionedSplit);
TaskExecutor 封装了TaskRunner(执行split的地方 PrioritizedSplitRunner(实现类是DriverSplitRunner))
TaskExecutor 里具体执行任务是是一个线程池
config.getMaxWorkerThreads(), // 这个是启动的固定线程池 。。不同SQL不同task都在里面执行 。。线程池大小是固定的:task.max-worker-threads
config.getMinDrivers(),// 这个默认是上面 x 2 不知有什么用?
config.getMinDriversPerTask(), // ?
config.getMaxDriversPerTask(),
PrioritizedSplitRunner实现了时间片机制(固定1秒去执行split 挑选优先级)
这种调度是不是牺牲了部分性能 换取迭代 优先级 多租户 多任务管理 结果快速反馈机制。。。
PrioritizedSplitRunner里实际运行的是Driver,封装的一堆Operatior 如表Scan/filter/limit/taskoutPut 作用在split上