Presto相关算子

 

1 所有查询基于 select nationkey,sum(totalprice) from orders a left outer join customer b on a.custkey=b.custkey group by nationkey;
2 逻辑节点

1 ScanFilterAndProjectOperator(SourceOpeator)

1.1 重要方法

不需要addInput

1.1.1 getOutput

if (split == null) { 
  return null; }
if (!finishing && pageSource == null && cursor == null) { //如果没有建立对数据源的连接,则建立            
  ConnectorPageSource source = pageSourceProvider.createPageSource(operatorContext.getSession(), split, columns);
  if (source instanceof RecordPageSource) {   //record类型                
    cursor = ((RecordPageSource) source).getCursor(); }            
  else {pageSource = source;}
}        
if (pageSource != null) {  //非record类型,没有游标,直接使用pageSource处理            
  return processPageSource(); }        
else {  //record类型,使用游标可以计算出各种内存使用情况。            
  return processColumnSource();  // pageBuilder构建出page         
}

重要类:

ConnectorPageSourceProvider

其实现类一般连接了数据源,提供了数据源的数据的抽象映射,提供了presto对数据的查询。

draw.io

FAQ

1 问:经常出现的currentOPerator.getNext 是null

答: 是因为 // only return a page if buffer is full or we are finishing

所以猜想是不停的读取数据,只有buffer满了或者finish才能真正返回page,不然就返回一个null.

2 page和block的meta信息在哪里

1.1.2 processColumnSource(游标)

该方法的代码主要分为两块:

第一部分:

if (!finishing && !yieldSignal.isSet()) {           
  CursorProcessorOutput output = cursorProcessor.process(operatorContext.getSession().toConnectorSession(), yieldSignal, cursor, pageBuilder); //重要
  pageSourceMemoryContext.setBytes(cursor.getSystemMemoryUsage());            
  long bytesProcessed = cursor.getCompletedBytes() - completedBytes;            
  long elapsedNanos = cursor.getReadTimeNanos() - readTimeNanos;            
  operatorContext.recordGeneratedInput(bytesProcessed, output.getProcessedRows(), elapsedNanos);            
  completedBytes = cursor.getCompletedBytes();            
  readTimeNanos = cursor.getReadTimeNanos();            
  if (output.isNoMoreRows()) {                
    finishing = true;                
    mergingOutput.finish();            
  }        
}

第二行核心代码,第二行的处理过程如下图所示:已经从数据库读出了内容,然后在这里通过writeLong之类的方法,写入了blockBuilder中。

而且值得注意的是,下图中的第三四两个方法,的cursorProcessor是函数接口,其代码实现是在LocalExecutionPlanner。

Supplier<CursorProcessor> cursorProcessor = expressionCompiler.compileCursorProcessor(translatedFilter, translatedProjections, sourceNode.getId());
Supplier<PageProcessor> pageProcessor = expressionCompiler.compilePageProcessor(translatedFilter, translatedProjections, Optional.of(context.getStageId() + "_" + planNodeId));

往下追溯,

Class<? extends CursorProcessor> cursorProcessor = cursorProcessors.getUnchecked(new CacheKey(filter, projections, uniqueKey));  
this.cursorProcessors = CacheBuilder.newBuilder() .recordStats().maximumSize(1000).build(CacheLoader.from(key -> compile(key.getFilter(), key.getProjections(), new CursorProcessorCompiler(metadata), CursorProcessor.class)));  

// 将class对象放入缓存,返回由 class生成对应对象的lambda表达式。

private <T> Class<? extends T> compileProcessor(RowExpression filter,List<RowExpression> projections,BodyCompiler bodyCompiler,Class<? extends T> superType){        
 ClassDefinition classDefinition = new ClassDefinition(a(PUBLIC, FINAL),makeClassName(superType.getSimpleName()),type(Object.class),type(superType));        
CallSiteBinder callSiteBinder = new CallSiteBinder();        
bodyCompiler.generateMethods(classDefinition, callSiteBinder, filter, projections);          
generateToString(classDefinition,callSiteBinder,toStringHelper(classDefinition.getType().getJavaClassName()).add("filter", filter).add("projections", projections).toString());        
return defineClass(classDefinition, superType, callSiteBinder.getBindings(), getClass().getClassLoader());

类和方法的定义都是在这个地方。 如上文所说,在cursorProcessor.process方法中处理了数据的write,这个process方法的定义就是在上述方法中生成的。如下图所示,生成的函数如下:

generateMethods方法生成了process方法,generateFilterMethod方法以及generateProjectMethod方法,

process函数如下:

public CursorProcessorOutput process(ConnectorSession session, DriverYieldSignal yieldSignal, RecordCursor cursor, PageBuilder pageBuilder){
	int completedPositions = 0;
  boolean finished = false;
  while(true){ //??
  	if (pageBuilder.isFull() || yieldSignal.isSet()) 
      return new CursorProcessorOutput(completedPositions, false);
    if (!cursor.advanceNextPosition()) 
      return new CursorProcessorOutput(completedPositions, true);
    if((filter(cursor))){
      pageBuilder.declarePosition();
      this.project_xx(session, cursor, pageBuilder.getBlockBuilder(42)));  //project方法
    }
  completedPositions++;
  }
}

CursorProcessorOutput中只记录了已经处理的行的简单信息,其构造函数第二个参数就是标志是否完成process,从上述代码可以看出,在do the projection中进行数据的读取,每读取一行 completedPositions++;

从下图调用栈可以看出 process--> project_1--->writeXX方法,因此真正的数据读取是在如下代码中

ifStatement.ifTrue()
                    .invokeVirtual(classDefinition.getType(),
                            "project_" + projectionIndex,
                            type(void.class),
                            type(ConnectorSession.class),
                            type(RecordCursor.class),
                            type(BlockBuilder.class));
        }

project方法如下:

publid void project(ConnectorSession session,RecordCursor cursor,BlockBuilder output){
	boolean wasNull = false;
  output.xxx;
  if(wasNull){
	  output.appendNull();
  }else{
			writeLong(long xx);  	
  }

}


1 debug过程中发现,price列时double却变成了long是因为jdk中的Double类的public static native long doubleToRawLongBits(double value);方法

2 processPageSource方法中的pageProcessor.process用来处理非record类型的数据源的数据。pageProcessor的处理方法也是代码生成的。

第二部分:

Page page = null;        
if (!pageBuilder.isEmpty() && (finishing || pageBuilder.isFull())) {
  page = pageBuilder.build();            
  pageBuilder.reset();        
}        
pageBuilderMemoryContext.setBytes(pageBuilder.getRetainedSizeInBytes());        
return page;

summary:

第一部分pagebuilder中包含了扫描出来的数据,每个block以blockbuilder形式暂存,在第二部分使用使用blockbuilder构建block,生成page数据

这部分在pageBuilder中new出了page,和block,对于每个block,通过blockBuilder.build()得出block数据。

1.2 查询demo

下图扫描 用户表cust

关于扫描订单表有以下几点说明:

  1. scan类中的cursor含有要扫描的表和要扫描的字段的信息

  2. columns比如需要扫描两个字段,但是一般type及block一般都是有三个, 猜想除了扫描字段之外还会多扫描一个。从打印的执行计划可以看出,这个出来的一个字段是Hash值。

2 HashAggregationOperator

2.1 重要变量

List<Type> groupByTypes //分组字段的类型,因为可能有多个分组字段,所以用集合表示

List<Integer> groupByChannels //分组通道,这里说的通道即分组的字段在传入的字段的位置,用整型表示

Step step //聚合的步骤,主要有partial,即部分聚合;final,即最终聚合

List<AccumulatorFactory> accumulatorFactories //聚合器工厂,根据不同的聚合类型产生不同的聚合器

Optional<Integer> hashChannel //进行hash的通道,和分组通道类似,一般根据什么分组,就要根据什么字段组合进行hash

Iterator<Page> outputIterator //输出迭代器

2.2 执行代码

public Page getOutput()    {        
  if (finished) {            
    return null;        
  }        
  // process unfinished work if one exists        
  if (unfinishedWork != null) {            
    boolean workDone = unfinishedWork.process();            
    aggregationBuilder.updateMemory();            
    if (!workDone) {                
      return null;            
    }            
    unfinishedWork = null;        
  }        
  if (outputPages == null) {            
    if (finishing) {                
      if (!inputProcessed && produceDefaultOutput) {                    
        // global aggregations always generate an output row with the default aggregation output (e.g. 0 for COUNT, NULL for SUM)                    
        finished = true;                    
        return getGlobalAggregationOutput();                
      }                
      if (aggregationBuilder == null) {                    
        finished = true;                    
        return null;                
      }            
    }            // only flush if we are finishing or the aggregation builder is full            
    if (!finishing && (aggregationBuilder == null || !aggregationBuilder.isFull())) {                
      return null;            
    }            
    outputPages = aggregationBuilder.buildResult();        
  }        
  if (!outputPages.process()) {            
    return null;        
  }        
  if (outputPages.isFinished()) {            
    closeAggregationBuilder();            
    return null;        
  }        
  return outputPages.getResult();    
}

其中两个方法比较重要, 一个是return getGlobalAggregationOutput(); 该方法视step是局部聚合还是final聚合执行对应的聚合方法。

// AccumulatorCompiler类i
f (step.isOutputPartial()) {                    
  accumulators.get(j).evaluateIntermediate(output.getBlockBuilder(channel));                
} else {                    
  accumulators.get(j).evaluateFinal(output.getBlockBuilder(channel));                
}            
}

计算方法是accumulators中提供的,那么accumulators是从何而来?可以看下hashAggr的调用栈。

private PhysicalOperation planGlobalAggregation(AggregationNode node, PhysicalOperation source, LocalExecutionPlanContext context)
	accumulatorFactories.add(buildAccumulatorFactory(source, aggregation));
		return metadata.getFunctionRegistry().getAggregateFunctionImplementation(aggregation.getSignature())。。。
    	return specializedAggregationCache.getUnchecked(getSpecializedFunctionKey(signature));
  OperatorFactory operatorFactory = new HashAggregationOperatorFactory(accumulatorFactories);

从上图可以看出,accumulators来源于accumulatorFactories,accumulatorFactories是由buildAccumulatorFactory构造出来的。接下来可以看下buildAccumulatorFactory的实现。

其中getAggregateFunctionImplementation是重点,其实现是从缓存中取出,其放入缓存的过程如下:

key -> ((SqlAggregationFunction) key.getFunction()).specialize(key.getBoundVariables(), key.getArity(), typeManager, this);
	return generateAggregation(type);
		GenericAccumulatorFactoryBinder factory = AccumulatorCompiler.generateAccumulatorFactoryBinder(metadata, classLoader);
			generateAccumulatorClass()
				generateEvaluateFinal()
        generateEvaluateIntermediate()
        ...
		return new InternalAggregationFunction(NAME, inputTypes, intermediateType, BIGINT, true, false, factory);

小结

在查询的时候,去缓存里找相关的SqlAggrFun,(如果没有找到,就由compile类去生成SqlAggregationFunction,其实现了evaluate方法,作为AccumulatorFacoties一部分),查询执行到HashAggr的部分的时候,遍历AggrFun,调用相对应的evaluate方法完成计算。

所以可以看出,真正完成计算的方法是在如下代码中完成能的。

这两个方法的实现用反射来完成,generateEvaluateFinal/generateEvaluateIntermediate;如下面这种

BytecodeBlock body = method.getBody();        
Variable thisVariable = method.getThis();        
BytecodeExpression state = thisVariable.getField(stateField);        
body.comment("output(state, out)");        
body.append(state);        
body.append(out);        
body.append(invoke(callSiteBinder.bind(outputFunction), "output"));        
body.ret();    }

但是从body.append(invoke(callSiteBinder.bind(outputFunction), "output")); 完全看不出该方法的细节???

另一个重要的方法是outputPages = aggregationBuilder.buildResult(); 该方法也是类似上面这种。

##############

InMemoryHashAggregationBuilder.buildResult()  //遍历aggregators  prepare  
  InMemoryHashAggregationBuilder.buildResult(consecutiveGroupIds());    // 关键代码***    
Aggregator.evaluate(groupId, output);    //遍历aggregators 调用evaluate      
GroupedAccumulator.evaluate(); //有两个实现类,DistinctingGroupedAccumulator和OrderingGroupedAccumulator。这两个实现类的evaluate的方法都是由其GroupedAccumulator属性调用的//即该evaluate方法也是由创建DistinctingGroupedAccumulator的构造函数中的Accumulator决定的。
//构造函数中的Accumulator是由instantiateGroupedAccumulator方法搞出来的。

真正的聚合操作在getOutput

  • 那么addInput完成是那些东西的聚合或者说在哪个层面的聚合。

全量上卷会调用LocalExecutionPlanner.planGlobalAggregation但是不会hashJoin了。

3 PartitionedOutputOperator

顾名思义这是一个分区操作,主要是把上一步聚合分组和聚合的结果根据分组的key(比如本例中的nationkey)进行分区,写到不同的分区文件中(类似spark的shuffle操作中shuffle write)。

该Operator重要方法是addInput:核心操作有两句

page = pagePreprocessor.apply(page);
partitionFunction.partitionPage(page);

第一句是:是对page的每一行进行处理,本例的pagePreprocessor实例是PageChannelSelector,其处理方法如下: (从下述代码没看出处理了什么???)

    public Page apply(Page page)    {
      requireNonNull(page, "page is null");
      Block[] blocks = new Block[channels.length];
      for (int i = 0; i < channels.length; i++) { 
        int channel = channels[i];
        blocks[i] = page.getBlock(channel);}        
      return new Page(page.getPositionCount(), blocks);    }

第二句:实现page的shuffle,

int partition = partitionFunction.getPartition(partitionFunctionArgs, position);
appendRow(pageBuilders[partition], page, position);

shuffle之后序列化

 List<SerializedPage> serializedPages = splitPage(pagePartition, DEFAULT_MAX_PAGE_SIZE_IN_BYTES).stream()
   .map(serde::serialize)
   .collect(toImmutableList());
outputBuffer.enqueue(partition, serializedPages);xxxx

另外在PartitionedOutputOperator还中还涉及slice相关;

.map(serde::serialize)
	PartitionedOutputOperator:SliceOutput serializationBuffer = new DynamicSliceOutput(toIntExact((page.getSizeInBytes() + Integer.BYTES))); // block length is an int
		DynamicSliceOutput:DynamicSliceOutput()
      this.slice = Slices.allocate(estimatedSize);
				new Slice(new byte[capacity]);

4 ExchangeOperator

exchangeOperator作为Driver的Operators的第一个操作节点,是不需要addInput的。重点方法是getOutput。

首先用exchangeClient从pageBuffer中弹出序列化后的page。进行反序列化,得到正常的page对象,然后后续Operator就正常处理page。

SerializedPage page = exchangeClient.pollPage(); 
if (page == null) { 
  return null;  }        
operatorContext.recordGeneratedInput(page.getSizeInBytes(), page.getPositionCount());        
return serde.deserialize(page);

8 TaskOutputOperator

一般作为task的最后一个操作符,用于数据的输出;

TaskOutputOperator作为一个Driver的Operators的最后一个操作的节点,不需要getOutput。重点方法是addInput,方法重点代码如下:

首先对page进行序列化,然后放入outputBuffer的队列中。

 List<SerializedPage> serializedPages = splitPage(page, DEFAULT_MAX_PAGE_SIZE_IN_BYTES).stream()                .map(serde::serialize)                .collect(toImmutableList());        outputBuffer.enqueue(serializedPages);

序列化

类PagesSerde是page的序列化类,使用了该类的序列化方法的有TaskOutputOperator

9 Operators

一般顺序:

draw.io

0 参考资料

https://blog.csdn.net/sinat_27545249/article/details/52450689#3-filterandprojectoperator

代码生成 Presto-Code Generation

猜你喜欢

转载自blog.csdn.net/xinjianwuhen1991/article/details/81429835
今日推荐