在 StarRocks 中,SQL 查询的生命周期分为三个阶段:查询解析(Parsing)、查询规划(Planning)和查询执行(Execution)。查询计划由 Frontend (FE) 生成并拆分为多个 fragment,这些 fragment 被分发到多个 BE 节点并行执行。每个 BE 节点接收到的 fragment 包含具体的执行逻辑,例如扫描数据、执行算子(比如 JOIN、AGGREGATE)以及结果返回。本文主要分析在 BE 侧对 fragment的执行流程,基于StarRocks3.4版本。
ExecNode 和 DataSink
通过 explain sql 查看拆分成几个 fragment 和 fragment 的结构。比如,查询SQL:select dt, avg(imp_cnt) from xxx group by dt limit 3; 通过explain语句可以看到分成了3个fragment,每个fragment都至少会有一个 ExecNode 和 DataSink 节点。
每个plan_fragment 都会有ExecNode节点和DataSink节点。ExecNode节点执行该fragment需要完成的动作,DataSink节点上报ExecNode节点执行产生的结果。
ExecNode
ExecNode 是一个抽象基类,为查询执行计划树中的所有节点提供通用接口和功能。每种节点类型(例如 ScanNode、JoinNode、AggregateNode)都继承自 ExecNode,并实现特定行为。
- 初始化和准备节点以进行执行。
- 管理查询执行的生命周期,包括 open、get_next、reset 和 close 操作。
DataSink
DataSink 是一个抽象基类,为查询执行计划中的数据接收器提供通用接口和功能。子类(如 ResultSink、ExportSink、TableFunctionTableSink)实现特定类型的数据输出逻辑。
- 初始化和准备数据接收器。
- 管理数据发送、打开和关闭的生命周期。
我们可以根据explain查看对应的sql语句,知道对应的node类型,然后直接在每个fragment所对应节点类型的ExecNode类的子类中查看其prepare、open、get_next等函数的实现来分析其行为。
调度
BE 在接收到 FE 发来的 fragment 信息以及做对应的处理主要由internal_service,fragment_executor,pipeline_driver,pipeline_driver_exectuor 这几个组件做的处理,具体如下:
internal_service
接收 fragment 执行请求:FE 将查询计划的 fragment(TExecPlanFragmentParams)通过 gRPC 传递给 BE,InternalService 负责接收并触发执行。
fragment_executor
- 将 fragment 的执行计划分解为多个 pipeline,并为每个 pipeline 创建对应的 PipelineDriver。
- 协调执行:通过 PipelineDriverExecutor 调度所有 PipelineDriver,确保 fragment 的所有 pipeline 按依赖关系正确执行。
pipeline_driver
- 表示一个 pipeline 的执行实例。pipeline 是 StarRocks 中查询执行的基本单元,包含一系列算子(operators,如 ScanOperator、JoinOperator),这些算子以流式方式处理数据。
- 每个 PipelineDriver
负责执行一个 pipeline 的完整逻辑,包括从数据读取到结果输出。
pipeline_driver_exectuor
- 是一个全局的管理组件,负责调度和管理多个 PipelineDriver 的执行。
- 它维护一个线程池或工作队列,将 PipelineDriver 分配到线程中执行,并处理任务的并发、优先级和资源管理。
案例分析:查询Hive表并导出到Hdfs
以一个查询hive表然后导出到hdfs上的sql为例,看看execnode和datasink 的创建
insert into files("path" = "hdfs://xxx/xx", "format" = "csv") select * from xxx limit 1;
通过如下堆栈:fragment_executor:: _prepare_exec_plan -> exec_node:: create_tree -> create_vectorized_node,在create_vectorized_node方法里,创建对应的 exec_node 对象,如下:
在这个例子里,最终会创建 ConnectorScanNode,简单分析下堆栈:
get_next -> ::_start_scan_thread -> ::_submit_scanner -> ::_scanner_thread -> ::open --> HiveDataSource::open. --> _init_scanner 在这里会判断是生成哪种Scanner,走JNI还是不走JNI,然后读取数据源数据。
而data sink则是fragment_executor在方法 _prepare_pipeline_driver --> create_data_sink,具体代码如下,在这里会创建对应的 data sink 对象:
在这个例子里创建的是 TableFunctionTableSink
这里准备 sink context 上下文信息,包括 hdfs path地址和 hdfs conf的一些配置信息
根据format创建CSVFileWriterFactory。
到这里就找到了fs实例的创建,通过fs把数据写到hdfs上。