文章目录
Presto将查询拆分成多个且有树状层级关系的Stage,实际就是将整个SQL处理过程拆分成多个具有各自功能的执行阶段。Presto将查询请求解析成各个执行阶段后,便会将各个阶段分配到各个计算节点中执行,这个分配的过程实际是基于Stage进行的,不同的Stage有不同的调度策略。每个执行阶段都会被进一步分解为若干个Task,查询调度就是分配Task到Worker节点并发协同完成整个查询的过程。
提交查询
首先来简单看下从用户提交查询到真正执行的整个过程。presto通过Rest接口QueuedStatementResource.postStatement
提交查询,其只是简单地将查询加入到一个映射队列中Map<QueryId, Query> queries = new ConcurrentHashMap<>()
。触发查询实际执行是调用查询任务状态的接口,其代码调用栈如下,可以看到最后实际是通过SqlQueryExecution.start
方法进行查询的执行。
QueuedStatementResource.getStatus
Query.toResponse
Query.waitForDispatched
DispatchManager.createQuery
DispatchManager.createQueryInternal
LocalDispatchQueryFactory.createDispatchQuery
SqlQueryManager.createQuery
SqlQueryExecution.start
生成查询计划并启动调度器
通过SqlQueryExecution.start
方法执行实际的查询,其主要的执行流程如下:
-
将当前线程名更改为
Query-QueryId
,从而可以跟踪具体某个查询的执行; -
将状态机转换到planning状态,通过状态机stateMachine控制各个阶段的流转;
-
由
SqlQueryExecution.analyzeQuery
方法生成逻辑执行计划; -
由
SqlQueryExecution.planDistribution
方法获得Stage的分布式执行计划,并生成查询调度器(默认为SqlQueryScheduler实现); -
将状态机转换到starting状态;
-
启动查询调度器queryScheduler进行实际查询任务的调度执行;
// SqlQueryExecution.java public void start() { // 设置当前线程名 try (SetThreadName ignored = new SetThreadName("Query-%s", stateMachine.getQueryId())) { try { // 将状态机转换到planning状态 if (!stateMachine.transitionToPlanning()) { // 如果查询已经启动或结束了,则直接返回 return; } // 分析查询,生成逻辑执行计划 PlanRoot plan = analyzeQuery(); // 生成查询的分布式执行计划(创建并设置queryScheduler,默认为SqlQueryScheduler实现) planDistribution(plan); // 将状态机转换到starting状态 if (!stateMachine.transitionToStarting()) { // 如果查询已经启动或结束了,则直接返回 return; } // 如果查询还未完成,则启动scheduler调度器,否则取消它 SqlQuerySchedulerInterface scheduler = queryScheduler.get(); if (!stateMachine.isDone()) { scheduler.start(); } } } }
对于每一查询,Presto都会生成一个与其对应的SqlQueryScheduler实例,可称之为整个查询的调度执行器。
查询调度过程
查询调度过程的调用栈如下:
SqlQueryScheduler.start
SqlQueryScheduler.startScheduling
SqlQueryScheduler.schedule
其主要逻辑实现在SqlQueryScheduler.schedule
方法中,可以看到其实际是按stage来进行调度的。其主要执行流程如下:
-
获取所有处于执行就绪状态的stage加入到待调度队列中;
-
遍历待调度队列中的stage并应用运行时CBO优化其执行计划;
-
为每个stage添加相应的执行策略;
-
遍历待调度队列中的stage并进行调度执行;
-
不断循环直至查询执行完成,并在某个stage执行完成时更新其状态以及下游stage的输入数据位置信息,从而可以让下游stage也转变成就绪状态可以被调度执行;
// SqlQueryScheduler.java private void schedule() { // 如果仍然在调度前一批次的stage,则直接返回 if (!scheduling.compareAndSet(false, true)) { return; } // StageExecutionAndScheduler包含stage执行器和调度器实例,以及stage间的血缘关系 List<StageExecutionAndScheduler> scheduledStageExecutions = new ArrayList<>(); // 设置当前线程名 try (SetThreadName ignored = new SetThreadName("Query-%s", queryStateMachine.getQueryId())) { Set<StageId> completedStages = new HashSet<>(); List<ExecutionSchedule> executionSchedules = new LinkedList<>(); // 如果当前线程未被中断,则不断循环 while (!Thread.currentThread().isInterrupted()) { // 遍历executionSchedules,并移除所有已完成部分 executionSchedules.removeIf(ExecutionSchedule::isFinished); // 获取已就绪可以运行的更多的部分(深度先序遍历执行计划树,并限制最大并发数) List<StreamingPlanSection> sectionsReadyForExecution = getSectionsReadyForExecution(); // 如果所有部分都已运行完成,则退出循环 if (sectionsReadyForExecution.isEmpty() && executionSchedules.isEmpty()) { break; } // 在创建SectionExecutions前对就绪部分应用运行时CBO优化 List<SectionExecution> sectionExecutions = createStageExecutions(sectionsReadyForExecution.stream() .map(this::tryCostBasedOptimize) .collect(toImmutableList())); // 如果查询状态机的状态是已完成,则将就绪的sectionExecutions全部终止,并退出循环 if (queryStateMachine.isDone()) { sectionExecutions.forEach(SectionExecution::abort); break; } // 将所有部分的stage添加到scheduledStageExecutions中,然后为每个部分创建相应的执行策略executionPolicy并添加到executionSchedules中 sectionExecutions.forEach(sectionExecution -> scheduledStageExecutions.addAll(sectionExecution.getSectionStages())); sectionExecutions.stream() .map(SectionExecution::getSectionStages) .map(executionPolicy::createExecutionSchedule) .forEach(executionSchedules::add); // 如果待执行调度的部分非空并且不是已执行完成则执行循环 while (!executionSchedules.isEmpty() && executionSchedules.stream().noneMatch(ExecutionSchedule::isFinished)) { List<ListenableFu