执行计划

Impala执行DML查询的执行计划与普通SELECT相同,从EXPLAIN的结果中可以看出,执行计划基本没有区别,左边为普通SELECT查询的执行计划,右边为CTAS建表的执行计划,只是多了一个写入hdfs的部分。




portgres 执行计划 impala执行计划_portgres 执行计划


执行过程

分析代码可以发现,Impala在接收查询的入口处将查询分为多种,大致如以下伪代码所示:

switch (exec_request_.stmt_type) {
  case TStmtType::QUERY:   //普通查询和DML一起处理
  case TStmtType::DML:       //DML查询
   	RETURN_IF_ERROR(ExecAsyncQueryOrDmlRequest(exec_request_.query_exec_request));
   	break;
  case TStmtType::EXPLAIN:   //只展示执行计划的EXPLAIN查询
   	set_explain_results();break;
  case TStmtType::TESTCASE:  //根据输入数据进行测试
   	do_test_case();break;
  case TStmtType::DDL:             //DDL查询
   	do_ddl();break;
  case TStmtType::LOAD:           //LOAD操作
   	do_load();break;
  case TStmtType::SET:           //query option执行
   	do_set();break;
  case TStmtType::ADMIN_FN:     //管理员操作,目前只有granceful shutdown功能
   	do_shutdown();break;
  default:
   	return errmsg;             //未知查询类型,返回报错
 }

在这里,CTAS和INSERT OVERWRITE的处理稍有不同,CTAS其实是作为DDL执行的,即作为TStmtType::DDL传入,但在具体的处理逻辑中,完成元数据初始化相关操作后Impala还是会调用到DML处理中的ExecAsyncQueryOrDmlRequest函数,相当于CTAS多了一个建表的过程,但后续操作与INSERT OVERWRITE或INSERT INTO相同,具体可见DDL操作调用的ExecDdlRequest函数实现伪代码:

Status ClientRequestState::ExecDdlRequest() {
 `Print(op_type);    //打印操作类型
 if (TCatalogOpType::RESET_METADATA) {
  	do_reset_metadata();    //执行元数据重置(刷新)操作,一般为refresh或invalidate metadata
  	return;
 }
 if (ddl_type() == TDdlType::COMPUTE_STATS) {
  	do_compute_stats();     //执行统计信息计算
  	return;
 }
 Status status = catalog_op_executor_->Exec(exec_request_.catalog_op_request);    //在执行过上面的操作后更新当前元数据状态
 if (TDdlType::CREATE_TABLE_AS_SELECT
	&& !catalog_op_executor_->ddl_exec_response()->new_table_created) {
  	do_nothing();       //当用户提交的CTAS包含了IF NOT EXISTS,且表已存在时,不进行任何操作
  	return;
 }
 if (TDdlType::CREATE_TABLE_AS_SELECT) {
  // CTAS真正执行的位置
  RETURN_IF_ERROR(ExecAsyncQueryOrDmlRequest(exec_request_.query_exec_request));
 }
  return;
}

从以上代码可以看出,Impala其实把DML的操作实现统一在一个函数内实现了,然后通过一个统一的query_exec_request对象来保存查询信息,查询如何执行要根据这个对象的成员来进行判断。上面我们提到了DML最终都调用了同一个函数ExecAsyncQueryOrDmlRequest,我们来看下这个函数内部的伪代码实现:

Status ClientRequestState::ExecAsyncQueryOrDmlRequest(

  const TQueryExecRequest& query_exec_request) {

	query_plan_init();     //进行一些参数判断,并初始化执行计划

	if_stats_missing_or_corrupt();    //确定哪些表的统计信息缺失或崩溃,以及是否有scan range的block元数据丢失

	if (is_cancelled_) return Status::CANCELLED;      //如果此时用户cancel查询,则退出

  //对于每个查询,起一个处理线程用来执行查询,直到查询完成或退出

    RETURN_IF_ERROR(Thread::Create("query-exec-state", "async-exec-thread",

   &ClientRequestState::FinishExecQueryOrDmlRequest, this, &async_exec_thread_, true));

 return Status::OK();

}

由上可见,ExecAsyncQueryOrDmlRequest函数其实是初始化了一些统计信息相关的信息到查询profile中,这就是我们在profile的执行计划中看到的Impala提示统计信息缺失等提示的来源,如下图所示:



portgres 执行计划 impala执行计划_大数据_02


在统计信息相关处理完成后,Impala会对每个查询单独起一个线程用来处理这个查询,直到查询完成。线程函数的实现在FinishExecQueryOrDmlRequest中。这个函数我们可以简化来看,除一些参数检查、状态注册、executor注册外,主要有两个比较重要的地方,一个是SubmitForAdmission(),另一个是coord_->Exec(),即队列准入和正式开始coordinator上的查询执行。随后开始一些be端和执行计划分片的初始化,在开始执行之前把Runtime filter广播到其他impalad上。至此,所有执行前的准备工作就执行完成了,后面通过StartBackendExec()函数正式开始执行,其中通过调用ExecAsync()函数中的ExecQueryFInstancesAsync()函数来rpc调用control-service.cc中的ExecQueryFInstances()函数,ExecQueryFInstancesAsync函数是通过protobuf生成出来的,具体逻辑在control_service.proto中定义,这个protobuf文件中还定义了一些rpc相关的状态变量、DML相关的状态及统计函数等等。其中调用了QueryExecMgr类中的StartQuery函数,QueryExecMgr是一个管理查询执行和执行状态的类,所有查询的注册均会通过此类中的方法来进入到impalad中执行。StartQuery函数中会起一个线程用来管理此次查询的执行,线程会调用ExecuteQueryHelper函数,其中调用了QueryState类的StartFInstances函数,针对每个执行计划分片,Impala都会启动一个线程来进行执行,调用ExecFInstance函数,并最终调用到FragmentInstanceState类中的Exec()和ExecInternal函数。其中核心的执行部分如下所示:

do {
  Status status;
  row_batch_->Reset();
  {
    SCOPED_TIMER(plan_exec_timer);
    RETURN_IF_ERROR(
        exec_tree_->GetNext(runtime_state_, row_batch_.get(), &exec_tree_complete));
        event_sequence_->MarkEvent("Row batch got");
  }
  UpdateState(StateEvent::BATCH_PRODUCED);
  if (VLOG_ROW_IS_ON) row_batch_->VLogRows("FragmentInstanceState::ExecInternal()");
  COUNTER_ADD(rows_produced_counter_, row_batch_->num_rows());
  RETURN_IF_ERROR(sink_->Send(runtime_state_, row_batch_.get()));
  event_sequence_->MarkEvent("Row batch written");
  UpdateState(StateEvent::BATCH_SENT);
} while (!exec_tree_complete);

执行过程中,Impala会不停地调用GetNext来扫描数据,存储到row_batch_对象中,每个rowbatch扫描完成后都会执行Send()函数,根据查询类型的不同,Send()函数有不同的实现(均继承了DataSink类),在HDFS上的DML查询一般会用到HdfsTableSink::Send(),执行过程中每个executor会将每个执行计划分片扫描到的row_batch_写入临时文件,直到全部执行计划执行完成。在Send()函数中,Impala根据不同情况会使用不同的写入方式,具体实现在hdfs-table-sink.cc(写kudu表则为kudu-table-sink.cc),大致可分为三种情况:1)静态分区写入;2)动态分区写入;3)聚簇写入(Clustered Insert)。每种方式其实最终调用了同一个函数进行写入,但参数有所不同,下面分别介绍:

静态/动态分区写入

Impala通过dynamic_partition_key_expr_evals_这个vector是否为空来判断是否进行静态分区写入,这个参数的初始化根据SQL中的分区表达式是否指定了常量值来进行。举例来讲,如果是如下SQL,则dynamic_partition_key_expr_evals_中就会有dt='2021-01-01’的分区表达式;

INSERT INTO DB.TABLE PARTITION (dt='2021-01-01') SELECT * FROM DB.TABLE2

如果像如下SQL这样没有指定分区值,则dynamic_partition_key_expr_evals_就是空的,会进入静态分区写入的处理:

INSERT INTO DB.TABLE PARTITION (dt) SELECT * FROM DB.TABLE2

这种动态分区的情况下,INSERT最后跟着的SELECT语句中select列表的字段数或VALUES子句中的字段数必须完全匹配被INSERT表的字段数(包括未指定分区值的分区字段),未指定分区值的分区字段将以select或values子句列表的最后一列作为分区值进行插入。

聚簇写入(Clustered Insert)

当INSERT语句中加入了/* +CLUSTERED */的HINT时,会执行聚簇写入的逻辑。在写入前,Impala会先对数据按分区进行排序,确保写入时是按照一个分区一个分区的顺序进行写入的,在写入Parquet文件时这样可以在一定程度上降低文件数。

无论哪种写入方式,最终都会分别调用两个函数进行写入操作:GetOutputPartition和WriteRowsToPartition。GetOutputPartition会先在Impala的临时写入目录(默认在表目录)创建并保持打开要写入的文件,然后根据存储类型来初始化TEXT或Parquet的writer;WriteRowsToPartition会经过多层调用,最终调用到Write函数进行文件的写入(同样根据TEXT和Parquet有两种写入方式)。所有写入完成后,Impala将之前打开的文件逐一关闭。此时,所有SQL执行基本结束,剩余的处理基本就是变量的释放、metric数据记录等等。最终返回到执行入口处的处理函数Execute()执行完成后,会调用request_state->Wait()以及GetCoordinator()->Wait()函数进行后续处理。在coordinator中,如果查询为DML,那么会等待所有executor执行完成并返回结果,然后coordinator开始进行文件搬运,将写入的临时文件移动到最终的表目录中,这里操作完成后,查询就可以说执行完毕了。整体SQL的执行过程可见下图:



portgres 执行计划 impala执行计划_hadoop_03


总结

Impala的DML大致可分为三个阶段:1)coordinator下发执行计划到executor(CTAS这种DDL也会执行类似DML的逻辑);2)executor执行执行计划中的SELECT部分,并将row batch结果写入临时文件;3)所有executor执行完成,coordinator搬运数据文件到最终的表目录。DML查询的执行计划基本与普通查询相同,只是在处理过程中根据查询类型会额外执行一些操作,并在所有executor全部执行完才会开始文件的移动操作,所以coordinator会有一个同步等待executor的过程。需要注意的是,当查询(包括DML)规模很小时(如带有LIMIT 1子句),Impala会优化执行计划,不将执行计划下发到executor,而是直接在coordinator上执行。