早期学习了《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等。