Presto-SqlTaskExecution【pipeline和life cycle】

1,967 阅读10分钟

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集合构成的,逻辑是一样的,这个比较好理解如下图所示。

image.png 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。

image.png

状态管理与推进 LifeSpan

然后再说说task的生命周期和状态管理。首先说单个pipeline的

对于ungroup的场景,最简单的事情就是接收到noMoresplit的事件,这个时候作为调度的任务已经完成,可以将当前节点移到下一个planId上。

对于group场景则稍微复杂一点,之前提到group的pipeline是有多组数据的,这样有多重处理手段,presto的策略是对每个lifespan独立管理状态推进。

image.png 本来这么来看似乎我们只需要一个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哈哈~)。

  1. 更新dataSource
    1. 根据maxAcknowledgedSplit判断一下是否有新的source。
    2. 遍历新的source
      1. 检查source的planNodeId是否存在与ScanTable里面,
        1. 存在对应则说明是一个tableScan的操作。
          1. 尝试将splitemerge到正在pendig的split中
            1. 获取当前的PlanNode,向Factory添加正在调度的split
            2. 给当前调度的lifeSpan添加新的split
            3. 给lifespan设置nomore标准,(我理解是talbeScan只有一个event,所以默认设置了)
            4. 设置当前的Node是全部为noMoreSplit
          2. 遍历schedulingLifespanManager中的active-lifeSpan
            1. 根据lifeSpan的对应的PendingSplit获取需要调度的PlanNode,
            2. 如果调度的是group的,则需要根据lifeSpan开启driverRunnerFactoriesWithDriverGroupLifeCycle对应的Driver的调度
            3. 开启在merge阶段添加的scheduledSplit的Driver的调度
            4. 判断pendigSplits是否有后续split如果没有则进行清除操作,并推进当前的lifeSpan到下一个planNode。
        2. 否则说明是一个remoteTask,更新remoteSource即可。
      2. 尝试关闭driverFactory,
      3. 尝试更新maxAcknowledgedSplit