Presto 源码解析:RESTful 处理和客户端查询流程

397 阅读20分钟

早期学习了《Presto》实战这本书,最近空余时间,结合网上资料,翻阅源码阅读了一遍,做个学习总结

一、Presto 的概念

Presto 是一个用于大数据分析的高性能、基于内存的、分布式SQL查询引擎,主要基于 Java 开发,最初由 Facebook 开源,后项目一分为二,其中之一名称为Trino,官网:trino.io/

二、Presto 的特点

● 多数据源。目前Presto可以支持MySQL、Hive、Kafka、MongoDB等数据源。

● 支持SQL。Presto 支持ANSI SQL(标准SQL)。

● 扩展性。Presto 不仅自身自带很多Connector 连接不同的数据源。还支持你自定义Connector。

● 混合计算。在Presto中针对每一个Connector 配置一个或多个Catalog 并查询其中的数据。用户可以混合多个Catalog进行join计算。

● 高性能。Presto经测试性能是hive的10倍。

● 流水线设计。使用Presto不必等到所有数据都处理完才能看到结果。一部分数据处理好就可以先看到结果。

三、Presto 的基本概念及架构



● Coordinator

  • 说明:部署于集群中的一个单独节点上,是集群的管理节点。
  • 职责:接收查询请求(从Client处接收)、解析查询语句、生成查询执行计划、任务调度、Worker管理(与Worker节点进行通信后获取Worker 的信息)。

● Worker

  • 说明:Worker 是集群中的工作节点,进行数据的处理、Task的执行。每隔一段时间向 Coordinator 发出心跳告知自己还活着,Worker 在执行每个Task的时候会对当前Task读入的每一个Split 进行一系列的操作和处理。
  • 职责:执行被Coordinator分解后的查询计划。

● Connector

  • 说明:可以将 Connector 看成是不同数据源的驱动程序。只要实现了Presto中标准的SPI接口,就可以定制自己的Connector。

  • 使用:在$PRESTO_HOME/etc/catalog中创建一个文件,example.properties(文件名无限制,扩展名必须为.properties),在该文件中写一个属性:connector.name,根据这个属性决定要访问哪一个数据源。比如,connector.name = Hive-cdh4,这样Presto 就会使用内置的 Hive Connector 去访问Hive。

● Catalog

  • 说明:类似于MySQL中的数据库实例。Schema类似于MySQL的一个DB,通过访问Catalog中指定的数据源,一个Catalog中可以包含多个Schema。
  • 使用:定义一个Catalog只需要在$PRESTO_HOME/etc/catalog中创建一个文件(如上所述)。配置文件名称就是Catalog的名称。此配置文件定义了诸如Hive metastore 的URI 等需要访问所需的配置项。
  • 例如:当你访问Catalog的某个表时,该表的名称总是以Catalog的名字开始,例如名字为 example.schema1.table1 的表,指的是表table1位于名为schema1的schema中,而schema1又位于名为 example 的Catalog中。

●Schema

  • 说明:Schema类似于MySQL的一个DB。一个Catalog名称和一个Schema名称确定了可以查询的的一系列表的集合。

●Table

  • 说明:与传统关系型数据库的Table概念是一致的。



图-2说明:一个Query分为多个Stage,每个Stage组成上下游关系,一个Stage分为多个Task,每个Task在Worker节点上执行,一个Task分为多个Driver,一个Driver处理一个Split,每个Driver由一系列Operator组成,每个 Operator 代表对 Split 的一种操作。

● Statement

  • 说明:Statement 语句,即输入的SQL语句。Presto 支持ANSI SQL —— 由子句、表达式和断言组成。

● Query

  • 说明:Query 即查询。当Presto接收一个SQL语句并执行时,会解析该SQL语句,将其转变为一个查询执行和相关的查询执行计划。“查询执行”代表了它可以在集群中运行,它是由运行在各个Worker上且各自之间的相互关联的阶段(Stage)组成的。
  • 查询执行:为了完成SQL语句而实例化的配置信息、组件、查询执行计划和优化信息等。一个查询执行由Stage、Task、Driver、Split、Operator 和Datasource组成。

● Stage

  • 说明:Stage,即查询执行阶段。Presto将Query拆分成多个有关系的Stage,一个Stage就代表查询执行计划的一部分。执行查询时,Presto会创建一个Root Stage(Single Stage),此Stage聚合其上游Stage的输出数据,并将结果输出给Coordinator。Stage通常是树状的结构,每个Stage(除了Single Stage 和 Source Stage)都会有输入和输出,都会从上游Stage读取数据,然后将产生结果输出给下游Stage。

  • 注意:Source Stage 没有上游Stage,它从Connector获取数据。Single Stage 没有下游 Stage,它的结果直接输出给Coordinator,并由 Coordinator 输出给终端用户。

  • 类型:

  • Coordinator_Only :这种类型的Stage 用于执行DDL 或 DML语句中最终的表结构创建或更改。

  • Single:用于聚合子Stage 的输出数据,并将最终数据输出给终端用户。

  • Fixed:用于接收子Stage 产生的数据并在集群中对这些数据进行分布式的聚合或分组计算。

  • Source:用于直连数据源,从数据源读取数据,在读取数据时,该阶段也会根据Presto对查询执行计划的优化完成相关的断言下发(PUSHDOWN)和条件过滤。Source Stage 直接通过 Source Operator(Driver)与 Connector 交互。

越靠近数据源的Stage越处于上游,越远离数据源的Stage越处于下游。

● Exchange

  • 说明:Presto 的Stage通过Exchange来连接另一个Stage,Exchange用于完成有上下有关系的Stage的数据交换。

  • 类型:

  • Output Buffer。生产数据的Stage 通过名为Output Buffer 的Exchange将数据传送给下游的Stage。

  • Exchange Client。消费数据的Stage通过名为Exchange Client 的Exchange从上游Stage读取数据。

● Task

  • 说明:Stage在逻辑上被划分为一系列的Task。这些 Task 是实际需要运行在Worker节点上的。每个Task处理一个或多个Split。每个Task都有对应的输入和输出。一个Task也可以被分为一个或多个Driver,从而并行地执行一个Task。

● Driver

  • 说明:一个 Driver 就是作用于一系列Operator的集合。因此一个Driver用于处理一个Split,并且生成对应的输出,这些输出由Task收集并且传送给其下游Stage中的一个Task。一个Driver拥有一个输入和一个输出。

● Operator

  • 说明:一个 Operator 代表对一个Split 的一种操作,例如过滤、加权和转换。一个Operator 依次读取一个Split 的数据,将 Operator 所代表的计算和操作作用于Split 的数据上,并产生输出。每个Operator 均会以Page为最小处理单位分别读取和产生输出数据,Operator 每次只会读取一个Page 对象,产生一个Page 对象。

● Split

  • 说明:Split 即分片。一个分片是一个大的数据集中的一个小的子集。Driver 则是作用于一个分片上的一系列操作的集合,每个节点上运行的Task包含多个Driver,所以一个Task 可以处理多个Split,其中每种操作均由一个Operator表示。
  • 处理过程:分布式查询执行Source Stage 通过Connector 从数据源处获取多个分片。Source Stage 对 Split 处理完毕后,会将输出传递给其下游的Stage(通常其下游Stage的类型为Fixed 或 Single)。当Presto执行查询的时候首先从Coordinator得到一个表对应的所有Split,然后Presto就会根据查询执行计划,选择合适的节点运行相应的Task处理 Split。

● Page

  • 说明:Page 是Presto 中的最小数据单元。一个Page对象包含多个Block对象,每个Block对象是一个字节数组,存储一个字段的若干行。多个Block横切的一行是真实的一行数据。一个Page最大为1MB,最多16×1024行数据。



四、Presto 的查询执行流程





Presto 基于Pipeline进行设计,每当查询启动的时候,就会在每个Worker上启动相应的Task,每个Task处理一个或多个Split,并且每处理一个Buffer大小的数据量时,就会将结果传输到下游 Stage的Task中,这样可以基本保证数据可以实时动态地传输。

在Presto中执行查询一共分为7步:

(1)客户端通过HTTP协议发送一个查询语句给Presto集群的Coordinator。

(2)Coordinator 对查询语句进行解析,生成查询计划,根据查询计划依次生成SqlQueryExecution、SqlStageExecution、HttpRemoteTask。Coordinator 会根据数据本地性生成对应的HttpRemoteTask。

(3)Coordinator 将Task分发给Worker执行,这个过程HttpRemoteTask中的HttpClient 将创建或者更新Task的请求发送给数据所在节点上的TaskResource所提供的Rest接口,TaskResource接收到请求之后最终会在对应的Worker上启动一个SqlTaskExecution对象需要处理的Split。

(4)执行处于上游的Source Stage中的Task,这些Task通过各种Connector从相应的数据源中读取数据。

(5)处于下游的Stage会读取上游Stage产生的输出结果,并在该Stage每个Task所在Worker的内存中进行后续的计算和处理。

(6)Coordinator 从分发Task之后,就会一直不断地从Single Stage 中的Task中获取计算结果,并将结果缓存到Buffer中,直到所有计算结束。

(7)Client 自从提交了查询语句后,就会不停地从Coordinator中获取本次查询的计算结果,直到获得了所有的计算结果。并不是等到所有的查询结果都计算完之后一次显示出来,而是每产生一部分,就会显示一部分,直到所有的查询结果都显示完毕。

五、Presto RESTful 框架解析

源码地址:Presto源码地址

在Presto中,几乎所有的操作都需要依赖基于Airlift框架构建的RESTful服务来完成,包括Worker阶段的管理、查询语句的提交、查询执行状态的显示、各个Task之间数据的传递等。Presto一共提供了4类RESTful接口,分别是 Statement接口、Query接口、Stage接口、Task接口。

1.Statement 相关类

从用户的角度看来,一个数据库查询在结束前主要经历两个状态,Queued(已提交,排队中)和Executing(执行中)

与SQL语句相关的查询均由Statement相关类处理,包括接收提交的SQL、获取查询执行的结果、取消查询等。Statement相关类有QueuedStatementResuource 和 ExecutingStatementResource,此类源码如下

@Path("/")
@RolesAllowed(USER)
public class QueuedStatementResource{
    @POST
    @Path("/v1/statement")
    @Produces(APPLICATION_JSON)
    public Response postStatement(
        String statement,
        @DefaultValue("false") @QueryParam("binaryResults") boolean binaryResults,
        @HeaderParam(X_FORWARDED_PROTO) String xForwardedProto,
        @HeaderParam(PRESTO_PREFIX_URL) String xPrestoPrefixUrl,
        @Context HttpServletRequest servletRequest,@Context UriInfo uriInfo) {...}

}

@Path("/")
@RolesAllowed(USER)
public class ExecutingStatementResource{
    @GET
    @Path("/v1/statement/executing/{queryId}/{token}")
    @Produces(MediaType.APPLICATION_JSON)
    public void getQueryResults(        
        @PathParam("queryId") QueryId queryId,        
        @PathParam("token") long token,        
        @QueryParam("slug") String slug,        
        @QueryParam("maxWait") Duration maxWait,        
        @QueryParam("targetResultSize") DataSize targetResultSize,        
        @DefaultValue("false") 
        @QueryParam("binaryResults") boolean binaryResults,        
        @HeaderParam(X_FORWARDED_PROTO) String proto,       
        @HeaderParam(PRESTO_PREFIX_URL) String xPrestoPrefixUrl,        
        @Context UriInfo uriInfo,        
        @Suspended AsyncResponse asyncResponse){...}
}

从上面可以看出,请求路径为"/v1/statement"的RESTful URL 均由这个类QueuedStatementResource处理,"/v1/statement/executing"的RESTful URL 由ExecutingStatementResource处理。

1.1 QueuedStatementResource

1.1.1 初始化

先看看QueuedStatementResource初始化的代码,除了GroupProvider、DispatchManager、Executor、ScheduledExecutorService赋值进行初始化以外,还启动了一个定时任务的线程池:

private final ScheduledExecutorService queryPurger = newSingleThreadScheduledExecutor(threadsNamed("dispatch-query-purger"));

queryPurger.scheduleWithFixedDelay(       
                 () -> {            
                            try {                // snapshot the queries before checking states to avoid registration race               
                                 purgeQueries(queries);               
                                 purgeQueries(retriedQueries);            
                                }catch (Throwable e) {                
                                       log.error(e, "Error removing old queries");            
                                }        
                        },        
                        200,        
                        200,        
                    MILLISECONDS);

private void purgeQueries(Map<QueryId, Query> queries){    
    // 在检查状态之前快照查询以避免注册竞争
    for (Entry<QueryId, Query> entry : ImmutableSet.copyOf(queries.entrySet())) {       
             if (!entry.getValue().isSubmissionFinished()) {     
                   continue;        
                }
            // 如果查询管理器不再跟踪此查询,移除它        
            // forget about this query if the query manager is no longer tracking it        
            if (!dispatchManager.isQueryPresent(entry.getKey())) {           
                     queries.remove(entry.getKey());       
             }    
    }
}

Presto是将查询作为一个对象Query(Query被定义为一个静态内部类)看待的,这一段代码的作用是将那些未提交、未注册到QueryTracker的Query从queries(Map<QueryId, Query> queries = new ConcurrentHashMap<>())移除掉。

1.1.2****postStatement()方法

@POST
@Path("/v1/statement")
@Produces(APPLICATION_JSON)
public Response postStatement(        
    String statement,        
    @DefaultValue("false") 
    @QueryParam("binaryResults") boolean binaryResults,        
    @HeaderParam(X_FORWARDED_PROTO) String xForwardedProto,        
    @HeaderParam(PRESTO_PREFIX_URL) String xPrestoPrefixUrl,        
    @Context HttpServletRequest servletRequest,        
    @Context UriInfo uriInfo){    
        if (isNullOrEmpty(statement)) {       
             throw badRequest(BAD_REQUEST, "SQL statement is empty");    
        }    
        abortIfPrefixUrlInvalid(xPrestoPrefixUrl);    
        // TODO: For future cases we may want to start tracing from client. Then continuation of tracing    
        //    will be needed instead of creating a new trace here.   
         SessionContext sessionContext = new HttpRequestSessionContext(
                        servletRequest, 
                        sqlParserOptions,            
                        tracerProviderManager.getTracerProvider(),            
                        Optional.of(sessionPropertyManager));  
        // 以上代码是为了构建会话上下文SessionContext,接着构建Query,并把它放入queries  
        Query query = new Query(statement, sessionContext, dispatchManager, executingQueryResponseProvider, 0);   
         queries.put(query.getQueryId(), query);    
        return withCompressionConfiguration(Response.ok(query.getInitialQueryResults(uriInfo, xForwardedProto, xPrestoPrefixUrl, binaryResults)), compressionEnabled).build();
}

此方法的作用是提交一个新的查询,若成功,会返回新查询的ID、nextUri、token等。其中,nextUrl可以直接用于下面的getStatus()和cancelQuery接口方法

private URI getNextUri(long token, UriInfo uriInfo, String xForwardedProto, String xPrestoPrefixUrl, DispatchInfo dispatchInfo, boolean binaryResults){   
     // if failed, query is complete    
    if (dispatchInfo.getFailureInfo().isPresent()) {
            return null;    
    }    
    // if dispatched, redirect to new uri
    return dispatchInfo.getCoordinatorLocation()
    .map(coordinatorLocation -> getRedirectUri(coordinatorLocation, uriInfo))
    .orElseGet(() -> getQueuedUri(queryId, slug, token, uriInfo));

private URI getRedirectUri(CoordinatorLocation coordinatorLocation, UriInfo uriInfo)
{
    URI coordinatorUri = coordinatorLocation.getUri(uriInfo);
    return UriBuilder.fromUri(coordinatorUri)
      .replacePath("/v1/statement/executing")
      .path(queryId.toString())
      .path(slug.makeSlug(EXECUTING_QUERY, 0))
      .path("0")
      .build();
}

private static URI getQueuedUri(QueryId queryId, Slug slug, long token, UriInfo uriInfo)
{
  return uriInfo.getBaseUriBuilder()
    .replacePath("/v1/statement/queued/")
    .path(queryId.toString())
    .path(slug.makeSlug(QUEUED_QUERY, token))
    .path(String.valueOf(token))
    .replaceQuery("")
    .build();
}

当以/v1/statement/executing 为前缀时,可以调用ExecutingStatementResuource的相关接口;当以/v1/statment/queued为前缀时,可以重复调用getStatus接口和cancelQuery接口(这两个接口都是以/v1/statment/queued为前缀的)

1.1.3 getStatus() 方法

@GET
@Path("/v1/statement/queued/{queryId}/{token}")
@Produces(APPLICATION_JSON)
public void getStatus(       
     @PathParam("queryId") QueryId queryId,        
     @PathParam("token") long token,        
     @QueryParam("slug") String slug,        
     @QueryParam("maxWait") Duration maxWait,        
     @DefaultValue("false") 
     @QueryParam("binaryResults") boolean binaryResults,        
     @HeaderParam(X_FORWARDED_PROTO) String xForwardedProto,        
     @HeaderParam(PRESTO_PREFIX_URL) String xPrestoPrefixUrl,        
     @Context UriInfo uriInfo,        
     @Suspended AsyncResponse asyncResponse){    
            abortIfPrefixUrlInvalid(xPrestoPrefixUrl); 
            第一步:查询Query   
            Query query = getQuery(queryId, slug);    
            ListenableFuture<Double> acquirePermitAsync = queryRateLimiter.acquire(queryId);    
            // 第二步:等待查询被分发,直至超时
            ListenableFuture<?> waitForDispatchedAsync = transformAsync(          
                  acquirePermitAsync,            
                  acquirePermitTimeSeconds -> { 
                            queryRateLimiter.addRateLimiterBlockTime(new Duration(acquirePermitTimeSeconds, SECONDS)); 
                            return query.waitForDispatched();            
                   },            
                responseExecutor);    
            ListenableFuture<?> futureStateChange = addTimeout(
                waitForDispatchedAsync,            
                () -> null,            
                WAIT_ORDERING.min(MAX_WAIT_TIME, maxWait),           
                timeoutExecutor);    
            // 第三步:当状态改变时,获取QUery结果
            ListenableFuture<Response> queryResultsFuture = transformAsync( 
                   futureStateChange,            
                    ignored -> query.toResponse(token, uriInfo, xForwardedProto, xPrestoPrefixUrl, WAIT_ORDERING.min(MAX_WAIT_TIME, maxWait), compressionEnabled, nestedDataSerializationEnabled, binaryResults),            
                    responseExecutor);   
           // 第四步:转化为响应结果  
           bindAsyncResponse(asyncResponse, queryResultsFuture, responseExecutor);
}

这个方法的用户为获取查询当前的状态,返回的响应中包含一个nextUri。根据查询的当前状态,这个nextUri的路径可以是/v1/statement/executing 或者/v1/statment/queued为前缀。

第一步根据queryId获取查询:

private Query getQuery(QueryId queryId, String slug){    
    Query query = queries.get(queryId);    
    if (query == null || !query.getSlug().equals(slug)) {       
         throw badRequest(NOT_FOUND, "Query not found");    
        }    
    return query;
}

第二步等待Query被分发,直至超时

private ListenableFuture<?> waitForDispatched(){    
    // 如果查询提交未结束,等待它结束
    synchronized (this) {        
        if (querySubmissionFuture == null) {            
            querySubmissionFuture = dispatchManager.createQuery(queryId, slug, retryCount, sessionContext, query);        
        }        
        if (!querySubmissionFuture.isDone()) {           
             return querySubmissionFuture;       
         }    
    }    
// 如果查询提交已结束,等待它查询结束
 return dispatchManager.waitForDispatched(queryId);
}

第三步:当状态改变时,获取下一个result:

public ListenableFuture<Response> toResponse(
    long token, 
    UriInfo uriInfo,        
    String xForwardedProto,        
    String xPrestoPrefixUrl,        
    Duration maxWait,        
    boolean compressionEnabled,        
    boolean nestedDataSerializationEnabled,        
    boolean binaryResults){    
        long lastToken = this.lastToken.get();    
        // token should be the last token or the next token   
        if (token != lastToken && token != lastToken + 1) {        
                    throw new WebApplicationException(Response.Status.GONE);   
         }    
        // advance (or stay at) the token    
        this.lastToken.compareAndSet(lastToken, token);    
        synchronized (this) {        
            // 如果查询提交未完成,返回一个空结果        
            if (querySubmissionFuture == null || !querySubmissionFuture.isDone()) {
                QueryResults queryResults = createQueryResults(token + 1,uriInfo,xForwardedProto,xPrestoPrefixUrl,DispatchInfo.waitingForPrerequisites(NO_DURATION, NO_DURATION),binaryResults);            
                return immediateFuture(withCompressionConfiguration(Response.ok(queryResults), compressionEnabled).build());        
            }    
        }    
    Optional<DispatchInfo> dispatchInfo = dispatchManager.getDispatchInfo(queryId);    
    if (!dispatchInfo.isPresent()) {        
        // 如果查询未找到      
        return immediateFailedFuture(new WebApplicationException(Response.status(NOT_FOUND).build()));    
    }    
    if (waitForDispatched().isDone()) 
    {       
         Optional<ListenableFuture<Response>> executingQueryResponse = executingQueryResponseProvider.waitForExecutingResponse(queryId,slug,dispatchInfo.get(),uriInfo,xPrestoPrefixUrl,getScheme(xForwardedProto, uriInfo),                maxWait,                TARGET_RESULT_SIZE,                compressionEnabled,                nestedDataSerializationEnabled,                binaryResults);       
         if (executingQueryResponse.isPresent()) {           
                 return executingQueryResponse.get();        
        }    
    }  
// 返回结果  
return immediateFuture(withCompressionConfiguration(Response.ok(createQueryResults(token + 1, uriInfo, xForwardedProto, xPrestoPrefixUrl, dispatchInfo.get(), binaryResults)), compressionEnabled)            .build());
}

第四步转化为响应结果

public static AsyncResponseHandler bindAsyncResponse(AsyncResponse asyncResponse, ListenableFuture<?> futureResponse, Executor httpResponseExecutor) {
  Futures.addCallback(futureResponse, toFutureCallback(asyncResponse), httpResponseExecutor);
  return new AsyncResponseHandler(asyncResponse, futureResponse);
}

1.1.4 cancelQuery()方法

@DELETE
@Path("/v1/statement/queued/{queryId}/{token}")
@Produces(APPLICATION_JSON)
public Response cancelQuery(@PathParam("queryId") QueryId queryId,
                             @PathParam("token") long token,        
                             @QueryParam("slug") String slug){    
                    getQuery(queryId, slug).cancel();    
                    return Response.noContent().build();
}

利用getQuery()方法从queries中获取Query,然后调用cancel方法

public synchronized void cancel()
{   
 querySubmissionFuture.addListener(() -> dispatchManager.cancelQuery(queryId), directExecutor());
}

往下挖dispatchManager 的cancelQuery方法

public void cancelQuery(QueryId queryId)
{    
queryTracker.tryGetQuery(queryId).ifPresent(DispatchQuery::cancel);
}

发现是从queryTracker中获取查询,然后调用DispatchQuery的cancel方法,最终状态机进行事务性取消:

@Override
public void cancel()
{
  stateMachine.transitionToCanceled();
}

public boolean transitionToCanceled()
{
  cleanupQueryQuietly();
  queryStateTimer.endQuery();

  // NOTE: The failure cause must be set before triggering the state change, so
  // listeners can observe the exception. This is safe because the failure cause
  // can only be observed if the transition to FAILED is successful.
  failureCause.compareAndSet(null, toFailure(new PrestoException(USER_CANCELED, "Query was canceled")));

  boolean canceled = queryState.setIf(FAILED, currentState -> !currentState.isDone());
  // 取消成功,则利用事务管理器继续取消
  if (canceled) {
    session.getTransactionId().ifPresent(transactionId -> {
      // 如果已经自动提交了的话,那么终止废弃,否则按失败处理
      if (transactionManager.isAutoCommit(transactionId)) {
        transactionManager.asyncAbort(transactionId);
      }
      else {
        transactionManager.fail(transactionId);
      }
    });
  }

  return canceled;
}

其中queryState.setIf()方法,使用的是CAS方式进行状态的变更:

private static <T> boolean setIf(AtomicReference<T> atomicReference, T newValue, Predicate<T> predicate){    
while (true) {       
     T current = atomicReference.get();       
     if (!predicate.test(current)) 
            {            
                return false;       
            }       
     if (atomicReference.compareAndSet(current, newValue)) 
        {           
                 return true;        
        }    
    }
}

1.2 ExecutingStatmentResource类

1.2.1 初始化

ExecutingStatementResource类的初始化和QueueStatmentResource类的初始化类似,为queryManager、exchangeClientSupplier、blockEncodingSerde、responseExecutor、timeoutExecutor等变量赋值,并启动一个queryPurger定时任务固定线程池,将那些queryManager不再追踪的query移除不再注册:

@Path("/")
@RolesAllowed(USER)
public class ExecutingStatementResource
{   
     private static final Duration MAX_WAIT_TIME = new Duration(1, SECONDS);   
     private static final Ordering<Comparable<Duration>> WAIT_ORDERING = Ordering.natural().nullsLast();   
     private static final DataSize DEFAULT_TARGET_RESULT_SIZE = new DataSize(1, MEGABYTE);    
    private static final DataSize MAX_TARGET_RESULT_SIZE = new DataSize(128, MEGABYTE);   
     private final BoundedExecutor responseExecutor;   
     private final LocalQueryProvider queryProvider;   
     private final boolean compressionEnabled;   
     private final boolean nestedDataSerializationEnabled;   
     private final QueryBlockingRateLimiter queryRateLimiter;   
     @Inject    
    public ExecutingStatementResource(
        @ForStatementResource BoundedExecutor responseExecutor,           
        LocalQueryProvider queryProvider,            
        ServerConfig serverConfig,            
        QueryBlockingRateLimiter queryRateLimiter)    
        {       
               this.responseExecutor = requireNonNull(responseExecutor, "responseExecutor is null");        
               this.queryProvider = requireNonNull(queryProvider, "queryProvider is null");       
               this.compressionEnabled = requireNonNull(serverConfig, "serverConfig is null").isQueryResultsCompressionEnabled();        
               this.nestedDataSerializationEnabled = requireNonNull(serverConfig, "serverConfig is null").isNestedDataSerializationEnabled();        
               this.queryRateLimiter = requireNonNull(queryRateLimiter, "queryRateLimiter is null");    
               queryPurger.scheduleWithFixedDelay(
                () -> {
                    try {
                        for (Entry<QueryId, Query> entry : queries.entrySet()) {
                            // 如果查询管理器不再跟踪该查询,忽略该查询
                            try {
                                queryManager.getQueryState(entry.getKey());
                            }
                            catch (NoSuchElementException e) {
                                // 查询不再注册
                                queries.remove(entry.getKey());
                            }
                        }
                    }
                    catch (Throwable e) {
                        log.warn(e, "Error removing old queries");
                    }
                },
                200,
                200,
                MILLISECONDS);      
    }
                
}

1.2.2 getQueryResult() 方法

 @ResourceSecurity(PUBLIC)
    @GET
    @Path("{queryId}/{slug}/{token}")
    @Produces(MediaType.APPLICATION_JSON)
    public void getQueryResults(
            @PathParam("queryId") QueryId queryId,
            @PathParam("slug") String slug,
            @PathParam("token") long token,
            @QueryParam("maxWait") Duration maxWait,
            @QueryParam("targetResultSize") DataSize targetResultSize,
            @Context UriInfo uriInfo,
            @Suspended AsyncResponse asyncResponse)
    {
        Query query = getQuery(queryId, slug, token);
        asyncQueryResults(query, token, maxWait, targetResultSize, uriInfo, asyncResponse);
    }

第一步先获取Query,通过getQuery()方法,从queries()Map获取,然后根据queryId获取Session和Slug,方便后面Query构建。然后new 一个ExchangeClient 传入Query.create()方法,Query.create()构建一个Query返回,返回后将其放入queries:

public Query getQuery(QueryId queryId, String slug){   
         Query query = queries.get(queryId);    
        if (query != null) {        
                if (!query.isSlugValid(slug)) {            
                    throw notFound("Query not found");       
                 }       
                 return query;    
        }    
        //这是第一次在coordinator上访问Query
       Session session;    
       try {       
             if (!queryManager.isQuerySlugValid(queryId, slug)) 
                {          
                      throw notFound("Query not found");       
                }        
                session = queryManager.getQuerySession(queryId);    
            }    catch (NoSuchElementException e) {       
                 throw notFound("Query not found");   
             }    
            // computeIfAbsent() 方法:判断一个map中是否存在这个key,如果存在则处理value的数据,如果不存在
            // 则创建一个满足value要求的数据结构放到value中
            query = queries.computeIfAbsent(queryId, id -> {      
            ExchangeClient exchangeClient = exchangeClientSupplier.get(new SimpleLocalMemoryContext(newSimpleAggregatedMemoryContext(), LocalQueryProvider.class.getSimpleName()));        
            return Query.create(               
                 session,               
                 slug,                
                 queryManager,                
                 transactionManager,                
                 exchangeClient,                
                 responseExecutor,                
                 timeoutExecutor,               
                 blockEncodingSerde,                
                retryCircuitBreaker);    
                });    
            return query;
}

ExecutingStatementResource创建一个Query对象以维护查询状态,并异调用该对象的waitForResults以获取查询结果,当查询结果未就绪或者未全部返回,getQueryResults依然会返回一个nextUri(executingUri),用户可以通过这个nextUri循环获取所有结果数据

第二步:通过asyncQueryResults()获取异步查询结果

private void asyncQueryResults(
  Query query,
  long token,
  Duration maxWait,
  DataSize targetResultSize,
  UriInfo uriInfo,
  AsyncResponse asyncResponse)
{
  Duration wait = WAIT_ORDERING.min(MAX_WAIT_TIME, maxWait);
  if (targetResultSize == null) {
    targetResultSize = DEFAULT_TARGET_RESULT_SIZE;
  }
  else {
    targetResultSize = Ordering.natural().min(targetResultSize, MAX_TARGET_RESULT_SIZE);
  }
  ListenableFuture<QueryResults> queryResultsFuture = query.waitForResults(token, uriInfo, wait, targetResultSize);
	
  ListenableFuture<Response> response = Futures.transform(queryResultsFuture, queryResults -> toResponse(query, queryResults), directExecutor());
	// 异步构建响应结果
  bindAsyncResponse(asyncResponse, response, responseExecutor);
}

1.2.3 cancelQuery()方法

@DELETE
@Path("/v1/statement/executing/{queryId}/{token}")
@Produces(MediaType.APPLICATION_JSON)
public Response cancelQuery(        
    @PathParam("queryId") QueryId queryId,        
    @PathParam("token") long token,        
    @QueryParam("slug") String slug){   
         queryProvider.cancel(queryId, slug);   
         return Response.noContent().build();
}

第一步,先根据queryId从queries中将Query获取出来,然后调用它的cancel方法():

public void cancel(QueryId queryId, String slug)
{   
     Query query = queries.get(queryId);   
     if (query != null) {        
            if (!query.isSlugValid(slug)) {            
                    throw notFound("Query not found");       
             }       
         query.cancel();   
     }    
     // cancel the query execution directly instead of creating the statement client   
     try {      
         if (!queryManager.isQuerySlugValid(queryId, slug)) {          
              throw notFound("Query not found");        
          }       
        queryManager.cancelQuery(queryId);    
    }  catch (NoSuchElementException e) {       
             throw notFound("Query not found");   
     }
}

cancel的方法跟QueuedStatmentResouce类似,都是走queryTracker.tryGetQuery(queryId).ifPresent(QueryExecution:cancelQuery)这个逻辑

public void cancelQuery(QueryId queryId)
{
  log.debug("Cancel query %s", queryId);

  queryTracker.tryGetQuery(queryId)
    .ifPresent(QueryExecution::cancelQuery);

1.2.4 partialCancel() 方法

了解Presto的基本概念可以知道,一个Query被分解到多个Stage执行,Presto支持部分取消,即取消一个Query中的一个Stage

public void partialCancel(int id) {
    StageId stageId = new StageId(queryid, id);
    queryManager.cancelStage(stageid);
}

2 Query相关类

2.1 QueryResource类

Query相关的核心类是QueryResource,此类源码如下:

@Path("/v1/query")
@RolesAllowed({USER, ADMIN})
public class QueryResource{
    @Injectpublic QueryResource(
        ServerConfig serverConfig,        
        DispatchManager dispatchManager,        
        QueryManager queryManager,        
        InternalNodeManager internalNodeManager,        
        Optional<ResourceManagerProxy> proxyHelper){    
            this.resourceManagerEnabled = requireNonNull(serverConfig, "serverConfig is null").isResourceManagerEnabled();   
             this.dispatchManager = requireNonNull(dispatchManager, "dispatchManager is null");   
             this.queryManager = requireNonNull(queryManager, "queryManager is null");    
            this.internalNodeManager = requireNonNull(internalNodeManager, "internalNodeManager is null");   
             this.proxyHelper = requireNonNull(proxyHelper, "proxyHelper is null");
    }
}

可以看到,以"/v1/query"开头的请求路径是由QueryResource处理

2.1.1 getQueryInfo() 方法

@GET
@Path("{queryId}")
public void getQueryInfo(       
         @PathParam("queryId") QueryId queryId,       
         @HeaderParam(X_FORWARDED_PROTO) String xForwardedProto,       
         @Context UriInfo uriInfo,       
         @Context HttpServletRequest servletRequest,       
         @Suspended AsyncResponse asyncResponse)
    {    
            requireNonNull(queryId, "queryId is null");   
             if (resourceManagerEnabled && !dispatchManager.isQueryPresent(queryId)) {       
                     proxyResponse(servletRequest, asyncResponse, xForwardedProto, uriInfo);     
                      return;    
            }    
            try {       
                 QueryInfo queryInfo = queryManager.getFullQueryInfo(queryId);        
                  asyncResponse.resume(Response.ok(queryInfo).build());   
             }    catch (NoSuchElementException e) {        
                    try {           
                         BasicQueryInfo basicQueryInfo = dispatchManager.getQueryInfo(queryId);           
                         asyncResponse.resume(Response.ok(basicQueryInfo).build());      
                      }  catch (NoSuchElementException ex) {         
                           asyncResponse.resume(Response.status(Status.GONE).build());        
                        }    
            }
}

这个方法较为简单,主要是根据queryId从dispatchManager中获取QueryInfo,然后将其返回

2.2.2 cancelQuery() 方法

@DELETE
@Path("{queryId}")
public void cancelQuery(       
         @PathParam("queryId") QueryId queryId,    
         @HeaderParam(X_FORWARDED_PROTO) String xForwardedProto,      
         @Context UriInfo uriInfo,      
         @Context HttpServletRequest servletRequest,       
         @Suspended AsyncResponse asyncResponse){    
                requireNonNull(queryId, "queryId is null");  
                if (resourceManagerEnabled && !dispatchManager.isQueryPresent(queryId)) {       
                     proxyResponse(servletRequest, asyncResponse, xForwardedProto, uriInfo);       
                     return;   
                 }   
                 dispatchManager.cancelQuery(queryId);    
            asyncResponse.resume(Response.status(NO_CONTENT).build());
}

这里的取消查询方法还是调用dispatchManager的cancelQuery()方法

2.2.3 killQuery() 方法

@PUT
@Path("{queryId}/killed")
public void killQuery(        
        @PathParam("queryId") QueryId queryId,        
        String message,        
        @HeaderParam(X_FORWARDED_PROTO) String xForwardedProto,     
        @Context UriInfo uriInfo,     
        @Context HttpServletRequest servletRequest,       
        @Suspended AsyncResponse asyncResponse){    
        if (resourceManagerEnabled && !dispatchManager.isQueryPresent(queryId)) {
                proxyResponse(servletRequest, asyncResponse, xForwardedProto, uriInfo);      
                return;   
         }    
        asyncResponse.resume(failQuery(queryId, createKillQueryException(message)));
}

主要调用failQuery()方法,核心在于委托给dispatchMaager的failQuery()方法:

public void failQuery(QueryId queryId, Throwable cause){   
     requireNonNull(cause, "cause is null");   
     queryTracker.tryGetQuery(queryId).ifPresent(query -> query.fail(cause));
}

3. Task 相关类

3.1 TaskResource 类

Task相关的核心类是TaskResource 类,此类源码如下

@Path("/v1/task")
@RolesAllowed(INTERNAL)
public class TaskResource{    
     private static final Duration ADDITIONAL_WAIT_TIME = new Duration(5, SECONDS);   
     private final TaskManager taskManager;   
     private final SessionPropertyManager sessionPropertyManager;  
     private final Executor responseExecutor;   
     private final ScheduledExecutorService timeoutExecutor;  
     private final Codec<PlanFragment> planFragmentCodec;    
     private final HandleResolver handleResolver;   
     private final ConnectorTypeSerdeManager connectorTypeSerdeManager;  
    
     @Inject    
    public TaskResource(         
            TaskManager taskManager,
            SessionPropertyManager sessionPropertyManager,        
            @ForAsyncRpc BoundedExecutor responseExecutor,       
            @ForAsyncRpc ScheduledExecutorService timeoutExecutor,    
            JsonCodec<PlanFragment> planFragmentJsonCodec,          
            HandleResolver handleResolver,       
           ConnectorTypeSerdeManager connectorTypeSerdeManager)    {     
               this.taskManager = requireNonNull(taskManager, "taskManager is null");     
               this.sessionPropertyManager = requireNonNull(sessionPropertyManager, "sessionPropertyManager is null");    
               this.responseExecutor = requireNonNull(responseExecutor, "responseExecutor is null");    
               this.timeoutExecutor = requireNonNull(timeoutExecutor, "timeoutExecutor is null");   
               this.planFragmentCodec = planFragmentJsonCodec;     
               this.handleResolver = requireNonNull(handleResolver, "handleResolver is null");    
               this.connectorTypeSerdeManager = requireNonNull(connectorTypeSerdeManager, "connectorTypeSerdeManager is null");    
}

以"/v1/task"开头的请求路径是由TaskResource处理

3.1.1 getAllTaskInfo() 方法

@GET
@Consumes({APPLICATION_JSON, APPLICATION_JACKSON_SMILE})
@Produces({APPLICATION_JSON, APPLICATION_JACKSON_SMILE})
public List<TaskInfo> getAllTaskInfo(@Context UriInfo uriInfo){   
     List<TaskInfo> allTaskInfo = taskManager.getAllTaskInfo();  
      if (shouldSummarize(uriInfo)) {     
           allTaskInfo = ImmutableList.copyOf(transform(allTaskInfo, TaskInfo::summarize));   
       }  
  return allTaskInfo;
}

获取所有Task信息的方法就是调用TaskManager 的getAllTaskInfo() 方法,其实就是将SqlTaslManager的LoadingCache<TaskId, sqlTask> tasks 返回回去

3.1.2 createOrUpdateTask() 方法

创建或者更新Task均由此方法处理,若存在taskId对应的Task,就根据taskUpdateRequest中的内容更新Task,否则就根据taskUpdateRequest中的内容创建一个新的Task,然后最终返回的是被创建或者更新的Task信息:

@POST
@Path("{taskId}")
@Consumes({APPLICATION_JSON, APPLICATION_JACKSON_SMILE})
@Produces({APPLICATION_JSON, APPLICATION_JACKSON_SMILE})
public Response createOrUpdateTask(
    @PathParam("taskId") TaskId taskId, 
    TaskUpdateRequest taskUpdateRequest, 
    @Context UriInfo uriInfo){    
        requireNonNull(taskUpdateRequest, "taskUpdateRequest is null");   
        Session session = taskUpdateRequest.getSession().toSession(sessionPropertyManager, taskUpdateRequest.getExtraCredentials());   
        TaskInfo taskInfo = taskManager.updateTask(
                session,taskId,taskUpdateRequest.getFragment().map(planFragmentCodec::fromBytes),           
                taskUpdateRequest.getSources(),           
                taskUpdateRequest.getOutputIds(),            
                taskUpdateRequest.getTableWriteInfo());    
        if (shouldSummarize(uriInfo)) {       
                 taskInfo = taskInfo.summarize();   
             }   
         return Response.ok().entity(taskInfo).build();
}

后面调用的是taskManager.updateTask()方法:

public TaskInfo updateTask(      
          Session session,       
         Optional<PlanFragment> fragment,      
         List<TaskSource> sources,      
         OutputBuffers outputBuffers,      
         Optional<TableWriteInfo> tableWriteInfo){   
         try {      
           // The LazyOutput buffer does not support write methods, so the actual     
           // output buffer must be established before drivers are created (e.g.       
           // a VALUES query).       
          outputBuffer.setOutputBuffers(outputBuffers);       
         // assure the task execution is only created once      
          SqlTaskExecution taskExecution;      
          synchronized (this) {        
            // is task already complete?         
               TaskHolder taskHolder = taskHolderReference.get();           
                 if (taskHolder.isFinished()) {              
                      return taskHolder.getFinalTaskInfo();         
                   }           
                 taskExecution = taskHolder.getTaskExecution();         
               if (taskExecution == null) {               
                     checkState(fragment.isPresent(), "fragment must be present");       
                     checkState(tableWriteInfo.isPresent(), "tableWriteInfo must be present");    
                    taskExecution = sqlTaskExecutionFactory.create(                  
                                          session,                    
                                          queryContext,       
                                          taskStateMachine,              
                                          outputBuffer,                
                                          taskExchangeClientManager,            
                                           fragment.get(),       
                                           sources,
                                         tableWriteInfo.get());            
                    taskHolderReference.compareAndSet(taskHolder, new TaskHolder(taskExecution));  
                    needsPlan.set(false);          
                      }     
               }      
              if (taskExecution != null) {       
                     taskExecution.addSources(sources);     
               }    
            }    catch (Error e)
                 {       
                         failed(e);      
                          throw e;   
                 }   
             catch (RuntimeException e) {   
                         failed(e);   
             }   
             return getTaskInfo();
}

3.1.3 getTaskInfo() 方法

获取TaskInfo的接口,如果当前版本和最大等待时间为null,那么直接根据taskId从taskManager中获取然后返回,否则等待waitTime时间,最后再异步构建结果:

@GET
@Path("{taskId}")
@Consumes({APPLICATION_JSON, APPLICATION_JACKSON_SMILE, APPLICATION_THRIFT_BINARY, APPLICATION_THRIFT_COMPACT, APPLICATION_THRIFT_FB_COMPACT})
@Produces({APPLICATION_JSON, APPLICATION_JACKSON_SMILE, APPLICATION_THRIFT_BINARY, APPLICATION_THRIFT_COMPACT, APPLICATION_THRIFT_FB_COMPACT})
public void getTaskInfo(        
            @PathParam("taskId") final TaskId taskId,       
            @HeaderParam(PRESTO_CURRENT_STATE) TaskState currentState,       
            @HeaderParam(PRESTO_MAX_WAIT) Duration maxWait,      
            @Context UriInfo uriInfo,   
            @Context HttpHeaders httpHeaders,     
            @Suspended AsyncResponse asyncResponse) {    
                requireNonNull(taskId, "taskId is null");   
                 boolean isThriftRequest = isThriftRequest(httpHeaders);  
                  if (currentState == null || maxWait == null) {     
                           TaskInfo taskInfo = taskManager.getTaskInfo(taskId);    
                            if (shouldSummarize(uriInfo)) {      
                                      taskInfo = taskInfo.summarize();     
                             }      
                            if (isThriftRequest) { 
                                   taskInfo = convertToThriftTaskInfo(taskInfo, connectorTypeSerdeManager, handleResolver);       
                            }       
                   asyncResponse.resume(taskInfo);   
                   return;   
                 }    
                Duration waitTime = randomizeWaitTime(maxWait);   
                ListenableFuture<TaskInfo> futureTaskInfo = addTimeout(          
                                                      taskManager.getTaskInfo(taskId, currentState), 
                                                       () -> taskManager.getTaskInfo(taskId),     
                                                       waitTime,          
                                                      timeoutExecutor);  
               if (shouldSummarize(uriInfo)) {      
                      futureTaskInfo = Futures.transform(futureTaskInfo, TaskInfo::summarize, directExecutor());    
                }   
                 if (isThriftRequest) {       
                             futureTaskInfo = Futures.transform(         
                                                       futureTaskInfo,            
                                                       taskInfo -> convertToThriftTaskInfo(taskInfo, connectorTypeSerdeManager, handleResolver),           
                                                         directExecutor());   
                  }  
                  // For hard timeout, add an additional time to max wait for thread scheduling contention and GC   
                 Duration timeout = new Duration(waitTime.toMillis() + ADDITIONAL_WAIT_TIME.toMillis(), MILLISECONDS);  
                  bindAsyncResponse(asyncResponse, futureTaskInfo, responseExecutor)          
                      .withTimeout(timeout);
}

3.1.4 acknowledgeResults****方法

preSto的动态过滤支持分区表和非分区表,Presto的动态分区包含Partition Pruning(分区表)以及Row filtering(非分区表),acknowledgeResults 方法主要用来获取新的过滤领域

@GET
@Path("{taskId}/results/{bufferId}/{token}/acknowledge")
public void acknowledgeResults(       
             @PathParam("taskId") TaskId taskId,     
             @PathParam("bufferId") OutputBufferId bufferId,     
             @PathParam("token") final long token){    
                requireNonNull(taskId, "taskId is null");   
                requireNonNull(bufferId, "bufferId is null");   
                 taskManager.acknowledgeTaskResults(taskId, bufferId, token);
}

3.1.5 deleteTask() 方法

主要用来抛弃或者删除Task,根据TaskId 在TaskManager 中操作

@DELETE
@Path("{taskId}")
@Consumes({APPLICATION_JSON, APPLICATION_JACKSON_SMILE, APPLICATION_THRIFT_BINARY, APPLICATION_THRIFT_COMPACT, APPLICATION_THRIFT_FB_COMPACT})
@Produces({APPLICATION_JSON, APPLICATION_JACKSON_SMILE, APPLICATION_THRIFT_BINARY, APPLICATION_THRIFT_COMPACT, APPLICATION_THRIFT_FB_COMPACT})
public TaskInfo deleteTask(       
             @PathParam("taskId") TaskId taskId,       
             @QueryParam("abort") @DefaultValue("true") boolean abort,       
             @Context UriInfo uriInfo,      
             @Context HttpHeaders httpHeaders){    
                    requireNonNull(taskId, "taskId is null");   
                    TaskInfo taskInfo;  
                    if (abort) {     
                           taskInfo = taskManager.abortTask(taskId);   
                     }    else {  
                          taskInfo = taskManager.cancelTask(taskId);   
                     }  
                    if (shouldSummarize(uriInfo)) {    
                            taskInfo = taskInfo.summarize();   
                     }   
                     if (isThriftRequest(httpHeaders)) {     
                           taskInfo = convertToThriftTaskInfo(taskInfo, connectorTypeSerdeManager, handleResolver);   
                     }   
                 return taskInfo;
}

具体如何取消的,我们可以往下追溯下源码:

@Override
public TaskInfo cancelTask(TaskId taskId){    
    requireNonNull(taskId, "taskId is null");    
    return tasks.getUnchecked(taskId).cancel();
}

public TaskInfo cancel(){   
     taskStateMachine.cancel();    
    return getTaskInfo();
}
public TaskInfo abort(){  
      taskStateMachine.abort();    
      return getTaskInfo();
}

public void cancel(){   
     transitionToDoneState(TaskState.CANCELED);
}

private void transitionToDoneState(TaskState doneState){  
      requireNonNull(doneState, "doneState is null");   
      checkArgument(doneState.isDone(), "doneState %s is not a done state", doneState);  
      taskState.setIf(doneState, currentState -> !currentState.isDone());
}

public boolean setIf(T newState, Predicate<T> predicate){    
        checkState(!Thread.holdsLock(lock), "Can not set state while holding the lock");   
        requireNonNull(newState, "newState is null");   
         while (true) {      
              // check if the current state passes the predicate    
             T currentState = get();       
             // change to same state is not a change, and does not notify the notify listeners     
             if (currentState.equals(newState)) {            return false;        }      
            // do not call predicate while holding the lock     
            if (!predicate.test(currentState)) {            return false;        }       
             // if state did not change while, checking the predicate, apply the new state       
             if (compareAndSet(currentState, newState)) {            return true;        }    }
}

可以看到,取消Task其实就是利用Task的状态机改变Task的状态TaskState.CANCELED,如何改变状态:
如果当前状态和新状态相等或者不符合断言(这里断言是判断状态是否已完成isDone),那么不必改变且返回false,否则使用CAS的方式改变状态

3.1.6 abortResults() 方法

这个方法用于清除TaskId标识的Task生成的,用于输出给下游Task(该Task由output标识)的数据

@DELETE
@Path("{taskId}/results/{bufferId}")
@Produces(APPLICATION_JSON)
public void abortResults(
          @PathParam("taskId") TaskId taskId,
          @PathParam("bufferId") OutputBufferId bufferId, 
          @Context UriInfo uriInfo){   
                 requireNonNull(taskId, "taskId is null");  
                 requireNonNull(bufferId, "bufferId is null");  
                taskManager.abortTaskResults(taskId, bufferId);
}

六、小结

Presto集群中的数据传输、节点通信、心跳感应、计算监控、计算调度和计算分布全部都是基于RESTFul服务实现的。Presto中的RESTFul服务是所有服务的基石

本本先是介绍了Presto的一些基本概念、基本架构及客户端查询流程,然后从源码视角查看了处理RESTFul请求的几个核心接口:QueuedStatementResource、ExecutingStatmentResource、QueryResource、TaskResource等。