SqlTaskExecution的定位
上次说到了MultilevelSplitQueue部分的实现,最近看了看这些splitRunner是怎么入堆的,主要是 SqlTaskExecution的部分,近期的spark的feature部分先不讨论。
仅仅作为一个爱好者把自己分析代码时的理解和思路分享出来,发表发表不负责任的观点,欢迎讨论,O(∩_∩)O哈哈~。代码在这里SqlTaskExecution
首先我们先界定这个类是用来做什么的,以我的理解在本地执行器的层面,我们需要有一个类似Runnable的东西去接受每个split的Source更新,drive的创建和管理,localplant的运行状态和生命周期,各类回调的注册等等,我理解SqlTaskExecution就是在处理部分工作 。
从调用链路来看,作为调度的入口每次的TaskSource的事件【split的新增】都最终会触发到SqlTaskExecution的创建或者更新
TaskResource-》SqlTaskManager-》SqlTask-》SqlTaskExecution.create/update
概念与数据结构
如何拆分和管理数据:Driver / Pipeline / LifeSpan
一个基本的问题是presto如何组织和构造概念的。 比如下面的图片是presto代码里面的举例,绿色的部分是driver的概念。 每个driver都可以归属到一个pipeline和一个lifeSpan,这2个概念是正交的。
同一个pipeline里面的driver都是由相同的operator集合构成的,逻辑是一样的,这个比较好理解如下图所示。
lifespan则说明数据是同一组的。如何理解这个group的概念?
这里可以讨论下presto是如何切分数据。
首先我们知道对于pipeline来说有一个Execution Strategy的属性,这个属性是一个什么样的概念?我看了下GROUPED_EXECUTION的使用场景,个人的理解是类似于在scan或者join的过程对数据按某个字段hash后分组后进行并行处理需要用到的的概念。
所以我们大概可以这样理解,当pipeline的的执行策略是group的时候说明数据是分组的,那这个pipeline的数据在接受数据的时候就是按照group各自接受的,而每个group都对应于一个lifeSpan的。在一个gourp或者lifeSpan内部则将数据分了split。
split则是代表了一个整个数据集合(比如talbe)的一部分,split的数量也是有限的,但是每个split里面具体有多少数据则是不一定的吗,一般需要根据connector实际返回数据分配的流式处理。
对于pipeline的执行策略是ungroup的时候,这里只有一个lifeSpan了,也就是taskwide的。
group和split都是对数据的分组为何要做2层?我个人的理解是这样的:
- group的概念更多的是类似于一个逻辑的概念,比如按某个字段hash之后分成10个组,split更多是数据层的,比如hive的场景,可能一个split对应一个文件,或者一个文件的一部分。
- 另一个区别是group我理解是贯穿全程的,也就是跨pipeline的概念,而split则是针对某个planId的。
所以作为一个SqlTaskExecution,最核心的职责就是接受到所有的split,并且为每个split创建任务并且获取数据,确保所有split处理完了之后推进到下一个planNode。
状态管理与推进 LifeSpan
然后再说说task的生命周期和状态管理。首先说单个pipeline的
对于ungroup的场景,最简单的事情就是接收到noMoresplit的事件,这个时候作为调度的任务已经完成,可以将当前节点移到下一个planId上。
对于group场景则稍微复杂一点,之前提到group的pipeline是有多组数据的,这样有多重处理手段,presto的策略是对每个lifespan独立管理状态推进。
本来这么来看似乎我们只需要一个lifespan的维度的调度管理器即可了。但是如果考虑到ungroup和group之间同时存在的场景则要更复杂一些,无法单纯的从lifespan的维度管理状态推进。虽然一个pipeline里面的都是相同的Strategy,但是同一个task内部却可能是有不同的Strategy。则pipeline的内部状态会出现如上图所示的场景:
比如我们现在有个4个tableScan的pipeline,需要按顺序进行,他们组成了一个task:
对于 lifeSpan 0的当前状态是pipelieA,这是因为还有很多split还没有到达这个是很正常的。
但是对于lifeSpan 2的其pipelineA的split全部到齐了,但是很可惜并不能进入到pipeline C,因为对于pipelineb还有一些其他的split未到达,只有等待其他的split全部到齐了之后才能推进到下一个状态,所以这个时候lifeSpan 2的当前pipeline是很奇怪的,可以说是pipeline B也可以说其空,但是我们都知道pipline b是ungroup的,所以如果是当前pipeline是B会很奇怪。所以presto选择了使用空,不过空也非常奇怪,O(∩_∩)O哈哈~。
Driver的资源管理与Factory
最后我们再讨论下driver、split、runner的概念。 首先driver是presto里面最底层的并发机制,只有一个输入和一个输出,而split的概念上面解释过。在我的理解里Driver差不多的是对应一个plannode+lifeSpan,但是如果有新的split添加则需要更新Driver。
当presto的worker接受到一个updateSource的请求之后并不会直接创建这个split对应的driver或者connector,因为这样资源并不可控,而是创一个DriverSplitRunner的对象关联一些cotext和factory等,将DriverSplitRunner的丢给上次文章中提到的MultilevelSplitQueue来执行调度。
这一点和一般业务程序中连接池的概念是不太一样的,一方面是因为presto的connector的数量、种类和生命周期和一般的应用程序不一样。另一个是tasksource的将创建链接的代价交给taskprocess的过程中这样就不存在公共资源的占用。或许这一点在MMP或者大数据的其他框架里面也是common sense,但是我还是觉得这些框架对于资源管理的理念很有意思,有空的话可以单独写一篇。
SqlTaskExecution的代码实现
成员变量
现在我们大概明白是希望做一件什么样的事情,之后需要看presto是怎么做的,首先来看看成员变量 总的来说成员变量分为几类
- 一类是task相关信息,比如TaskId,TaskStateMachine,TaskConetx,TaskHandler,
- 一类是资源相关的,OutputBuffer,taskExecutor,notificationExecutor,SplitMonitor,remoteSources,drivers
- 一类是和状态维护和管理相关的,比Status、schedulingLifespanManager、pendingSplitsByPlanNode、maxAcknowledgedSplit、driverRunnerFactoriesWithDriverGroupLifeCycle、 driverRunnerFactoriesWithSplitLifeCycle、driverRunnerFactoriesWithTaskLifeCycle
task相关的其实么有什么特别要注意的。资源相关的其实除了dervier确实是SqlTaskExecution创建和持有的之外,其他的线程资源和monitor都是仅仅是为了注册回调和实现。而driver的创建其实上个小节也说过了。下面我们讨论下是如何维护和推进状态的,然后在回顾下addSources的主要逻辑就ok。
TaskSource
addSource接受的对象是TaskSource,结构基本如下
public class TaskSource{
private final PlanNodeId planNodeId; // 当前source的节点
private final Set<ScheduledSplit> splits; // split有哪些
private final Set<Lifespan> noMoreSplitsForLifespan;// 哪些lifespan不再有后续的split
private final boolean noMoreSplits;// 当前节点不再接受split。
}
public class ScheduledSplit
{
private final long sequenceId;
private final PlanNodeId planNodeId;
private final Split split;
}
所以我们能拿到的信息首先是当前tasksource如上所示: 首先一个是sequenceId。这个序号很目前没关注哪里生成的,会关联到maxAcknowledgedSplit,从逻辑来看如果sequenceId,比当前sqlTaskExecution的大说明是一个新的split,想来是个类似全局自增id的概念,后续再讨论。 其实我们需要关注nomoreSplit是在lifeSpan概念下的,或者node级别的,如果 要判断 一个pipeline是否有nomoreSplit是需要推断出来的。nomoreSplit对推进状态和状态监测都是很重要的指标 。
Status
其次我们看下Status的成员构成
private static class Status{
private final Map<Integer, Map<Lifespan, PerPipelineAndLifespanStatus>> perPipelineAndLifespan;
private final Map<Integer, PerPipelineStatus> perPipeline;
private final Map<Lifespan, PerLifespanStatus> perLifespan = new HashMap<>();
}
private static class PerPipelineStatus
{
final PipelineExecutionStrategy executionStrategy;
int pendingCreation;
int lifespansWithNoMoreDriverRunners;
final List<Lifespan> unacknowledgedLifespansWithNoMoreDrivers = new ArrayList<>();
}
private static class PerLifespanStatus
{
int remainingDriver;
int pipelinesWithNoMoreDriverRunners;
}
private static class PerPipelineAndLifespanStatus
{
int pendingCreation;
boolean noMoreDriverRunner;
}
可以很清晰的看到Stataus的信息做的非常全,维护了3个维度的列表。分别对应pepiline、lifeSpan、PipeLine+lifeSpan。但是更重要的是维护的是什么信息?
- pendingCreation:这个其实是说createDriverRunner已经创建成了但是还没有创建driver,之所有要知道这个字段是因为主要是因为需要管理DriverFactory的生命周期。
- noMoreDriverRunner:这个字段实际上相当于是状态的终止条件,结合之前讨论到的接受事件,我们可以很清晰的知道noMoreSplit是关联到lifeSpan的,所以对于PerPipelineAndLifespanStatus,可以用一个bool值来标记这个字段,而对于lifeSpan和pipeline维度我们则只能统计noMoreDriver的数量。
- unacknowledgedLifespansWithNoMoreDrivers:这个字段其实将noMoreDriver的信息传递给DriverFactory。每个noMoreDriver的事件只会返回一次。每次get都会清空这个队列。
SchedulingLifespanManager
另一个比较重要的是SchedulingLifespanManager,但是在讨论之前我们先讨论下DriverFactory的种类, 其实说明成员变量的时候说明了,SqlTaskExecution有3组DriverFactory:
private final Map<PlanNodeId, DriverSplitRunnerFactory> driverRunnerFactoriesWithSplitLifeCycle;
private final List<DriverSplitRunnerFactory> driverRunnerFactoriesWithDriverGroupLifeCycle;
private final List<DriverSplitRunnerFactory> driverRunnerFactoriesWithTaskLifeCycle;
- driverRunnerFactoriesWithSplitLifeCycle:这一组Driver对应了TableScan的操作,key对应PlanNodeId其实是对应了table的表名列表,其本身也是根据Table来构造的。这组Factory和下面2个的最大的不同是他是分partitio的。
- driverRunnerFactoriesWithDriverGroupLifeCycle这个其实是每个根据lifeSpan来构造的,所以其构造是触发式的构造,只有当状态调度到这个的时候才会构造。
- driverRunnerFactoriesWithTaskLifeCycle从名字上也可看出来和上一个很像,区别在于他是ungroup的,所以这组DriverFactory不是触发式的构造的,而是在创建的完成的初始化的时候自动构造的。
而这个SchedulingLifespanManager则是仅仅管理partitioned drivers的。也就是上面中的driverRunnerFactoriesWithSplitLifeCycle的drive的推进。
private final List<PlanNodeId> sourceStartOrder;
private final StageExecutionDescriptor stageExecutionDescriptor;
private final Status status;
private final Map<Lifespan, SchedulingLifespan> lifespans = new HashMap<>();
// driver groups whose scheduling is done (all splits for all plan nodes)
private final Set<Lifespan> completedLifespans = new HashSet<>();
private final Set<PlanNodeId> noMoreSplits = new HashSet<>();
private int maxScheduledPlanNodeOrdinal;
对照成员变量可以相对清晰的梳理SchedulingLifespanManager的职责概念。
- lifespans维护了SchedulingLifespan,SchedulingLifespan维护了当前lifeSpan的schedulingPlanNodeOrdinal,而SchedulingLifespanManager则维护所有lifespans中的最大值。
- completedLifespans代表了所有plannode都完成了的的lifeSpan
- moreSplit是lifeSpan维度的。
对于SchedulingLifespan来说比较重要的状态是获取当前的SchedulingPlanNode。这部分的逻辑其实就是对应上一章中task的状态管理所述的场景,
- 如果当前的plant的life和lifesplan的类型是一样的,说明是正常逻辑,group对应找那个在进行中的pipeline,从manmager中的获取当前table中的对应ordinal就行。如果是ungroup的则说明就是taskwide。
- 如果是不匹配的,则出现之前讨论的ungroup和group混合的场景,这种情况下如何当前任务是阻塞住了还是可以推进了。这就用到了此前说的SchedulingLifespanManager管理的maxScheduledPlanNodeOrdinal,如果这个order和当前的order是一样的说明我们可以advance到下一个pipelie,否则则返回空,说明处于阻塞状态。
public Optional<PlanNodeId> getSchedulingPlanNode()
{
checkState(!isDone());
while (!isDone()) {
// Return current plan node if this lifespan is compatible with the plan node.
// i.e. One of the following bullet points is true:
// * The execution strategy of the plan node is grouped. And lifespan represents a driver group.
// * The execution strategy of the plan node is ungrouped. And lifespan is task wide.
if (manager.stageExecutionDescriptor.isScanGroupedExecution(manager.sourceStartOrder.get(schedulingPlanNodeOrdinal)) != lifespan.isTaskWide()) {
return Optional.of(manager.sourceStartOrder.get(schedulingPlanNodeOrdinal));
}
// This lifespan is incompatible with the plan node. As a result, this method should either
// return empty to indicate that scheduling for this lifespan is blocked, or skip the current
// plan node and went on to the next one. Which one of the two happens is dependent on whether
// the current plan node has finished scheduling in any other lifespan.
// If so, the lifespan can advance to the next plan node.
// If not, it should not advance because doing so would violate scheduling order.
if (manager.getMaxScheduledPlanNodeOrdinal() == schedulingPlanNodeOrdinal) {
return Optional.empty();
}
verify(manager.getMaxScheduledPlanNodeOrdinal() > schedulingPlanNodeOrdinal);
nextPlanNode();
}
return Optional.empty();
}
对于SchedulingLifespanManager来说比较核心的逻辑是getActiveLifeSpans:他是维护completedlifeSpas的方式,通过判断SchedulingLifespan中的schedulingPlanNodeOrdinal来判断lifeSpan是否终止,而这而这个方法实际上是在处理AddSource的过程中调度的,其实也是将回调逻辑嵌套在时间处理的过程中。
public Iterator<SchedulingLifespan> getActiveLifespans()
{
// This function returns an iterator that iterates through active driver groups.
// Before it advances to the next item, it checks whether the previous returned driver group is done scheduling.
// If so, the completed SchedulingLifespan is removed so that it will not be returned again.
Iterator<SchedulingLifespan> lifespansIterator = lifespans.values().iterator();
return new AbstractIterator<SchedulingLifespan>()
{
SchedulingLifespan lastSchedulingLifespan;
@Override
protected SchedulingLifespan computeNext()
{
if (lastSchedulingLifespan != null) {
if (lastSchedulingLifespan.isDone()) {
completedLifespans.add(lastSchedulingLifespan.getLifespan());
lifespansIterator.remove();
}
}
if (!lifespansIterator.hasNext()) {
return endOfData();
}
lastSchedulingLifespan = lifespansIterator.next();
return lastSchedulingLifespan;
}
};
}
事件处理核心逻辑:AddSources
好了总算到了最后一步,来看下SqlTaskExecution的主要逻辑,其实整个类只有这一段主要逻辑,如果是喜欢跟着逻辑看代码的同学可以从这里开始,只是我个人看代码喜欢先看数据结构而已。简单梳理下逻辑,但是不展开说了不然太多写不完,如果要完全理解可能还要好好看看代码(主要很多细节我也没看懂。O(∩_∩)O哈哈~)。
- 更新dataSource
- 根据maxAcknowledgedSplit判断一下是否有新的source。
- 遍历新的source
- 检查source的planNodeId是否存在与ScanTable里面,
- 存在对应则说明是一个tableScan的操作。
- 尝试将splitemerge到正在pendig的split中
- 获取当前的PlanNode,向Factory添加正在调度的split
- 给当前调度的lifeSpan添加新的split
- 给lifespan设置nomore标准,(我理解是talbeScan只有一个event,所以默认设置了)
- 设置当前的Node是全部为noMoreSplit
- 遍历schedulingLifespanManager中的active-lifeSpan
- 根据lifeSpan的对应的PendingSplit获取需要调度的PlanNode,
- 如果调度的是group的,则需要根据lifeSpan开启driverRunnerFactoriesWithDriverGroupLifeCycle对应的Driver的调度
- 开启在merge阶段添加的scheduledSplit的Driver的调度
- 判断pendigSplits是否有后续split如果没有则进行清除操作,并推进当前的lifeSpan到下一个planNode。
- 尝试将splitemerge到正在pendig的split中
- 否则说明是一个remoteTask,更新remoteSource即可。
- 存在对应则说明是一个tableScan的操作。
- 尝试关闭driverFactory,
- 尝试更新maxAcknowledgedSplit
- 检查source的planNodeId是否存在与ScanTable里面,