写点什么

Presto 设计与实现(十四):SQL 查询过程总结

作者:冰心的小屋
  • 2023-09-08
    北京
  • 本文字数:3072 字

    阅读完需:约 10 分钟

Presto 设计与实现(十四):SQL 查询过程总结

对于 Presto SQL 语句执行过程,之前的几篇文章已经介绍过 Presto JDBC、基于 ANTLR 4 的词法分析、语法分析、抽象语法树生成和 SQL 查询状态机,今天整体回顾下 SQL 语句完整的执行过程,做个全面的总结。

1. 客户端

在项目中实际集成 Presto 时,通常会使用 Presto 的 presto-jdbc 执行 SQL 查询。


  1. 准备阶段:客户端引入 presto-jdbc 依赖;

  2. 注册 PrestoDriver:校验 PrestoDriver 包的版本,向 DriverManager 注册;

  3. 获取 PrestoConnection: 调用 DriverManager.getConnection 方法传入 Presto 连接地址、用户名和密码获取 Connection 实例,因为之前已经注册过 PrestoDriver,此时 DriverManager 会委托 PrestoDriver 获取,PrestoDriver 在获取 Connection 的过程中会依次创建 OkHttpClient、QueryExecutor 和 PrestoConnection 对象,而 QueryExecutor 依赖 OkHttpClient,PrestoConnection 依赖 QueryExecutor;

  4. 创建 Statement,执行 SQL:通过 PrestoConnection 创建 Statement 会返回 PrestoStatement 实例,PrestoStatement 执行 SQL 时会委托通过 PrestoConnection 创建的 StatementClientV1 执行 SQL;

  5. StatementClientV1 执行 SQL :

  6. 构建 HTTP POST SQL 查询请求,请求地址: http://coordinator:port/v1/statement,QueuedStatementResource 会接受请求,但不会立即执行,会返回此次查询的 queryId;

  7. 提交请求成功后,StatementClientV1 会通过 queryId 轮询构建 HTTP GET 请求获取查询结果,请求地址:http://coordinator:port/v1/statement/queued/{queryId}/{token},直到成功获取到查询结果;

  8. PrestoStatement 将查询结果封装在 AtomicReference 作为此次查询的最终结果;

  9. 客户端通过 PrestoStatement 返回的 ResultSet 解析结果。

2. Coordinator

客户端通过调用 Statement 的 executeQuery 方法执行 SQL 查询,在获取结果时客户端处于阻塞状态直到 SQL 查询结束,SQL 查询结束的状态:FINISHED 和 FAILED,有众多情况可以导致查询的失败,例如校验执行权限失败、SQL 词法语法错误、资源不足、SQL 各种情况的超时、SQL 执行超过了重试次数、服务端异常或用户取消任务等。


在调用 Statement 的 executeQuery 方法后,StatementClientV1 提交 HTTP SQL 查询任务,根据服务端返回的 QueryId 轮询获取查询状态,当查询状态为最终状态后返回给 Statement,客户端获取到查询结果执行后续操作。


客户端的执行过程已经清楚,让我们来看看服务端 Coordinator 的过程:

  1. QueuedStatementResource:SQL 查询入口

  2. 对外提供了 SQL 查询的提交、状态获取、重试和取消的一系列接口;

  3. 通过创建 Query 类的实例来表示此次提交的 SQL 查询,使用 Map<QueryId, Query> queries 维护查询任务,后台还有一个 fqueryPurger 调度线程池周期性的校验 queries 中的状态,维护 Query 的有效性;

  4. Query 创建后会由 QueryIdGenerator 分配统一的 QueryId,客户端可通过 QueryId 调用 SQL 查询状态获取接口。

  5. DispatchManager:Query 分发处理

  6. 校验 Query 的执行权限和身份认证;

  7. 创建此次会话的 Session 对象会涉及到创建 Session 上下文和重写会话等操作;

  8. 如果 Query 支持事务,通过 transactionManager 开启事务;

  9. 通过 DispatchQueryFactory 创建分布式查询任务 DispatchQuery,DispatchQuery 只是用来前期申请资源,资源申请成功时会调用 QueryExecutionFactory 创建实际的 QueryExecution 执行后续 SQL 查询;

  10. 分布式任务 DispatchQuery 监控:向 ClusterStatusSender 进行注册,会定期监控 DispatchQuery 的心跳和状态变更;

  11. DispatchQuery 最终会被分配到 InternalResourceGroup 执行。

  12. InternalResourceGroup:任务入队、等待资源分配和执行

  13. 从当前 InternalResourceGroup 开始遍历所有父类校验是否 canRun 和 canQueue;

  14. 如果 canRun 和 canQueue 都为 false 则查询失败;

  15. 如果 canRun 并且队列为空直接运行任务,否则进行入队操作;

  16. DispatchQuery 任务实际运行时会调用 startInBackground 方法,方法内部会首先将 DispatchQuery 加入到 runningQueries 队列中,并注册了监控集群是否有可用工作节点的回调函数;

  17. 集群有可用的工作节点回调函数触发,QueryExecution 执行;

  18. QueryExecution:SQL 的词法分析、语法分析、逻辑计划生成、逻辑计划优化和物理计划生成都在这里

  19. 在 createLogicalPlanAndOptimize 方法内通过 QueryAnalyzer 完成词法分析、语法分析和抽象语法树生成,通过创建 Optimizer 实例完成抽象语法树的重写优化,解析原始输入输出,构建表达式列表,构建子查询任务,最终完成逻辑计划 PlanRoot 的构建;

  20. 逻辑计划的执行分为多个阶段,会根据系统配置的 use_legacy_scheduler 参数创建 LegacySqlQueryScheduler 或者 SqlQueryScheduler 实例。

  21. 通过具体的 SqlQueryScheduler 生成有上下依赖关系的 StageExecutionAndScheduler,通过 SqlQuerySchedulerInterface 方法调用 initialize -> start -> schedule 执行每个阶段,而在实际 StageExecutionAndScheduler 的执行过程中会选择具体的 StageScheduler 生成多个 RemoteTask,RemoteTask 又转化为多个 ScheduledSplit;

  22. 最终 PlanRoot -> 多个依赖的 StageExecutionAndScheduler -> 多个 RemoteTask -> 多个 ScheduledSplit;

  23. RemoteTask 执行:SqlQueryScheduler 调用 StageScheduler 的 schedule 方法,最终 RemoteTask 的 start 方法执行,接着 start -> scheduleUpdate -> sendUpdate, 在 sendUpdate 内部提交异步 HTTP 请求到指定的工作节点: POST /v1/task/{taskId},并注册用于监听请求的回调函数;

  24. 随着 RemoteTask 执行完毕,StageExecutionAndScheduler 的执行完毕,并通过 QueryResourceUtil 根据查询的 Query 对象构建返回结果。

3. Worker

Worker 节点通过 TaskResource 对外提供任务的创建、查询和删除等一系列接口,用于处理 Coordinator 的任务分发。


  1. TaskResource 通过 createOrUpdateTask 对外提供基于 HTTP 的任务提交:POST /v1/task/{taskId} ,使用 TaskUpdateRequest 接收提交的请求,并调用 SqlTaskManager 的 updateTask 方法执行更新操作;

  2. SqlTaskManager 会构建查询的上下文,设置查询上下文可用的资源,执行 SqlTask.updateTask,创建用于执行 SqlTask 的 SqlTaskExecution;

  3. SqlTaskExecution 会根据传入的 scheduledSplit 创建 DriverSplitRunner;

  4. DriverSplitRunner 有运行时的上下文 DriverContext,有生命周期管理的 Lifespan,还有用于创建 Driver 的 DriverSplitRunnerFactory,DriverSplitRunnerFactory 根据 DriverContext 和 ScheduledSplit 创建 Driver,Driver 包含多个 Operator,Operator 就是实际的数据库操作;

  5. TaskExecutor 管理 DriverSplitRunner 的实际运行,在 DriverSplitRunner 创建完毕会调用 TaskExecutor 的 enqueueSplits 方法执行入队操作,队列类型为 MultilevelSplitQueue 1 个优先级队列;有了入队对应的还有出队,TaskExecutor 在初始化时通过 Executors.newCachedThreadPool 创建了线程池,创建指定数量(runnerThreads)TaskRunner 任务实例提交到线程池,TaskRunner 是 TaskExecutor 的内部类,方法 run 内部是一个 while 循环,不断的从 MultilevelSplitQueue 获取 DriverSplitRunner 任务,并最终调用 DriverSplitRunner.processFor -> Driver.processFor -> 多个 Operator.getOutput -> 实际的 Page 数据;

  6. 当此次任务对应的所有 DriverSplitRunner 执行完毕,此次任务执行完毕,SqlTaskExecution 会更新任务状态,而 SqlTaskManager 会缓存查询结果,当客户端再次通过 GET /v1/task/{taskId} 获取结果时直接返回。

发布于: 刚刚阅读数: 3
用户头像

分享技术上的点滴收获! 2013-08-06 加入

一杯咖啡,一首老歌,一段代码,欢迎做客冰屋,享受编码和技术带来的快乐! 欢迎关注公众号:冰心的小屋

评论

发布
暂无评论
Presto 设计与实现(十四):SQL 查询过程总结_sql_冰心的小屋_InfoQ写作社区