这篇文章主要介绍impala-backend是怎么执行一个SQL Query的。

在Impala中SQL Query的入口函数是:

void ImpalaServer::query(QueryHandle& query_handle, const Query& query)

  • 生成一个QueryExecState伴随这个SQL执行的生命周期,代表正在执行的这个SQL;
  • 调用Execute函数启动执行流程;
  • 启动一个Wait线程等待结果。

这个Execute()函数首先是通过JNI向impala-fe请求SQL解析和执行计划生成(已经在上一篇文章中讲了这个过程),得到该Query对应的TExecRequest对象,交由impala-backend执行。

从下面这个函数开始backend执行,同时开始fragment status report。

Status ImpalaServer::QueryExecState::Exec(TExecRequest* exec_request)

因为我们知道在impala里面,一个Query是分配到多个节点执行的,我们把其中负责分配和协调这个Query执行的组件叫Coordinator;参与这个Query执行的每个节点叫backend instance,每个backend instance上面会执行一个或者多个PlanFragment。那么每个Query就对应一个Coordinator对象和多个backend instance,同时Coordinator中的query_profile_ 变量是用来统计这个query的执行的整个profile的。

Coordinator

这里首先生成Coordinator用于协调这个Query的执行,然后调用

Status Coordinator::Exec(

const TUniqueId& query_id, TQueryExecRequest* request,

const TQueryOptions& query_options)

启动异步的执行过程:说白了这个Coordinator就是老板,把活(PlanFragment)都给各个下属(backend instance)安排好了,发出去,然后自己下班走人了,才不会等着下属干完了才走呢。因为老板早就安排好自己的秘书(ImpalaServer::Wait())去盯着结果呢。

这个函数里面最重要的两个步骤:

  • ComputeScanRangeAssignment(*request);
  • ComputeFragmentExecParams(*request);

其中ComputeScanRangeAssignment(const TQueryExecRequest& exec_request) 用于填充std::vector<FragmentScanRangeAssignment> scan_range_assignment_ 这个数组是以PlanFragment为索引的。

typedef boost::unordered_map<THostPort, PerNodeScanRanges> FragmentScanRangeAssignment表示某个PlanFragment的backend instance以及其对应的PerNodeScanRanges的映射。而PerNodeScanRanges表示某个PlanFragment所涉及到的所有PlanNode到ScanRange的映射。

另外一个函数ComputeFragmentExecParams (const TQueryExecRequest& exec_request) 用于填充std::vector<FragmentExecParams> fragment_exec_params_ 。这个参数中每个FragmentExecParams对应着一个PlanFragment执行中用到的参数。

  • Status Coordinator::ComputeFragmentHosts(const TQueryExecRequest& exec_request):为每个PlanFragment找到执行所在的backend instance。如果一个PlanFragment是UNPARTITIONED,那么就在这个Coordinator所在的host上运行;如果一个PlanFragment含有ScanNode,那么就调度这个PlanFragment到HDFS/HBase数据块所在的那些DataNodes上,也就是这些DataNodes就成为了执行这个Query的backend
    instance。
  • 计算TQueryExecRequest.fragments中每个PlanFragment会在哪些hosts上得到执行,填充到fragment_exec_params_ 中。
  • 依次给每个PlanFragment执行的每个host分配一个instance_id。
  • 填充每个 FragmentExecParams 的destinations(即Data Sink的目的地PlanFragment)和per_exch_num_senders(这个ExchangeNode会接收来自多少个PlanFragment的数据)

回到Coordinator::Exec()函数中,下面就该把各个PlanFragment分配干活了。

  • 如果有Coordinator PlanFragment,那么先new PlanFragmentExecutor()生成这个PlanFragment所对应的PlanFragmentExecutor。然后填充其对应的TExecPlanFragmentParams。
  • 下面是个双层循环:外层遍历PlanFragment,内层遍历backend instance,生成与每个instance关联的BackendExecState(主要是生成TExecPlanFragmentParams用于Coordinator与多个backend instance交互时的参数),并加入backend_exec_states_列表,用于Coordinator对所有的backend instance执行状况的管理。然后向每个instance发起RPC请求开始执行,请求协议是ImpalaInternalService::
    ExecPlanFragment(TExecPlanFragmentParams)

Status fragments_exec_status = ParallelExecutor::Exec(

bind<Status>(mem_fn(&Coordinator::ExecRemoteFragment), this, _1),

reinterpret_cast<void**>(&backend_exec_states_[backend_num - num_hosts]),

num_hosts);

每个Coordinator,PlanFragmentExecutor和ExecNode都会有一个RuntimeProfile,所有的RuntimeProfile会构成树状结构来记录每个执行节点的执行过程中的信息。

在Coordinator有个成员变量boost::scoped_ptr<RuntimeProfile> query_profile_用于表示这个query过程中的所有的profile信息。

每个Coordinator还有个aggregate_profile_专门负责aggregate相关的profile。

PlanFragmentExecutor和ExecNode

无论是在Coordinator端还是在backend instance端执行的PlanFragment都是由一个PlanFragmentExecutor控制的。下面我们看看PlanFragment在backend instance是怎么执行的?

在RPC的server端调用了ImpalaServer::ExecPlanFragment()->ImpalaServer::StartPlanFragmentExecution()

生成FragmentExecState里面含有一个PlanFragmentExecutor。那么下面就是分析PlanFragmentExecutor怎么控制Query的执行的了。

  • FragmentExecState::Prepare()调用PlanFragmentExecutor::Prepare()
  • FragmentExecState::Exec()调用PlanFragmentExecutor::Open(),这个是PlanFragment执行的主循环,block直到该PlanFragment执行结束。

真正控制PlanFragment执行的是PlanFragmentExecutor,主要由Prepare()/Open()/GetNext()/Close()这几个函数组成。

1,  PlanFragmentExecutor::Prepare(TExecPlanFragmentParams):准备执行,主要流程如下:

  • 设定这个query能够使用的内存mem_limit;
  • DescriptorTbl::Create():初始化descriptor table;
  • ExecNode::CreateTree():生成执行树的结构(父子关系)。执行树由ExecNode组成,每一个ExecNode也提供了Prepare(), Open(), GetNext()函数。后面执行ExecNode::Prepare/Open/GenNext /EvalConjuncts/Close函数都是按照这个树状结构递归下去的。初始化完成后,PlanFragmentExecutor ::plan_指向了执行树的根节点。在这棵树中,root节点被最后执行,叶子节点被最先执行;
  • 设置该PlanFragment的Exchange Node会接收来自多少个sender的数据;
  • 调用plan_->Prepare():从根节点开始递归初始化执行树,主要是初始化runtime_profile等统计信息和conjuncts的LLVM本地代码生成 (adding functions to the LlvmCodeGen object);
  • 如果使用本地代码生成,调用runtime_state_->llvm_codegen()->OptimizedModule()进行优化;
  • 把所有的ScanNode对应的Scan Range映射到file/offset/length;
  • DataSink::CreateDataSink();
  • set up profile counter;
  • 生成RowBatch用于存储结果。

2,PlanFragmentExecutor::Open()

先是start the profile-reporting thread,然后调用OpenInternal()

(1)     调用plan_->Open()沿着生成的ExecNode执行树依次调用ExecNode:: Open()

下面以HdfsScanNode::Open()为例说明:

  • 调用DiskIoMgr:: RegisterReader初始化与HDFS的连接hdfs_connection_;
  • 把要读取的File 和Split加入HdfsScanNode的队列queued_ranges_中;
  • 调用HdfsScanNode::DiskThread驱动HdfsScanNode::StartNewScannerThread()->HdfsScanNode::ScannerThread->HdfsScanner:: ProcessSplit()去读取数据(目前一个scanner thread只能读取一个scan range);
  • 调用IssueQueuedRanges()把上面加入queued_ranges_中的预读取Range发送给DiskIoMgr。由于上一步中已经启动了disk thread,所以就可以读取数据了。

(2)     如果当前这个PlanFragmen有sink,那么需要把这个PlanFragment要发给其他PF的数据都发出去。在发出去之前肯定得获取要发的东西吧,调用PlanFragmentExecutor ::GetNextInternal()从上到下递归调用执行树的ExecNode::GetNext()获取执行结果。

上面说到对于ExecNode::Open()不同种类的ExecNode的逻辑是不一样的,对于GetNext()也是一样的,可以参考下HdfsScanNode::GetNext()或者HashJoinNode::GetNext()看看具体是怎么获取查询结果的。

3,  PlanFragmentExecutor::GextNext(RowBatch** batch)

显示触发执行树的ExecNode::GetNext()函数获取查询结果。当其标记PlanFragmentExecutor::done_==true时,则表明所有数据已经被处理完,该PlanFragmentExecutor可以退出了。

至此,impala-backend也分析完了。总的来说impala在执行过程中和MapReduce及Hive的不同可以概括为一拉一推。

  • 在MapReduce中,Map的输出结果要等着Reduce去拉;而impala中各个PlanFragment执行结束之后DataSink是推送到其他PlanFragment的。这样能更加有效利用带宽,加快Job执行速度。
  • 在Hive中,逻辑上下游节点是由上游节点推送给下游节点的;而impala中是下游节点通过递归调用GetNext()向上游节点拉取的。

Impala源代码分析(3)-backend查询执行过程的更多相关文章

  1. Impala源代码分析---1

    2.Impala源代码分析 參考链接:http://www.sizeofvoid.net/wp-content/uploads/ImpalaIntroduction2.pdf 本章開始进入源代码分析阶 ...

  2. mysql体系结构和sql查询执行过程简析

    一: mysql体系结构 1)Connectors 不同语言与 SQL 的交互 2)Management Serveices & Utilities 系统管理和控制工具 备份和恢复的安全性,复 ...

  3. MySQL查询执行过程

    MySQL查询执行路径 1. 客户端发送一条查询给服务器: 2. 服务器先会检查查询缓存,如果命中了缓存,则立即返回存储在缓存中的结果.否则进入下一阶段: 3. 服务器端进行SQL解析.预处理,再由优 ...

  4. 详细分析SQL语句逻辑执行过程和相关语法

    本文目录: 1.SQL语句的逻辑处理顺序 1.2 各数据库系统的语句逻辑处理顺序 1.2.1 SQL Server和Oracle的逻辑执行顺序 1.2.2 MariaDB的逻辑执行顺序 1.2.3 M ...

  5. 精尽MyBatis源码分析 - MyBatis 的 SQL 执行过程(一)之 Executor

    该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...

  6. Impala查询执行过程

  7. 第8章2节《MonkeyRunner源代码剖析》MonkeyRunner启动执行过程-解析处理命令行參数

    MonkeyRunnerStarter是MonkeyRunner启动时的入口类,由于它里面包括了main方法.它的整个启动过程主要做了以下几件事情: 解析用户启动MonkeyRunner时从命令行传输 ...

  8. Red5源代码分析 - 关键类及其初始化过程

    原文地址:http://semi-sleep.javaeye.com/blog/348768 Red5如何响应rmpt的请求,中间涉及哪些关键类? 响应请求的流程如下: 1.Red5在启动时会调用RT ...

  9. 第8章4节《MonkeyRunner源代码剖析》MonkeyRunner启动执行过程-启动AndroidDebugBridge

    上一节我们看到在启动AndroidDebugBridge的过程中会调用其start方法,而该方法会做2个基本的事情: 715行startAdb:开启AndroidDebugBridge 722-723 ...

  10. 第8章5节《MonkeyRunner源代码剖析》MonkeyRunner启动执行过程-执行測试脚本

    MonkeyRunner在准备好AndroidDebugBridge和DeviceMonitor等服务之后,就基本上是攻克了和目标设备通信的问题了,那往下须要做的就是把測试脚本执行起来了. 178 p ...

随机推荐

  1. due to missing onError handler in the subscribe() method call.

    某日,APP端忽然与后台,一建立连接,就报错,然后断开, 之前都好好的,十分确信代码没有问题,可是跑着就是报错,百思不得其解, 终于发现,不知道怎么回事,配置文件里的stomp的地址配的居然不是我印象 ...

  2. 【YashanDB知识库】outline固化执行计划

    [问题分类]性能优化,功能使用 [关键字]outline [问题描述]防止SQL执行计划突变,用outline固化执行计划 [问题原因分析]防止SQL执行计划突变,用outline固化执行计划 [解决 ...

  3. Qml 实现瀑布流布局

    [写在前面] 最近在刷掘金的时候看到一篇关于瀑布流布局的文章,然鹅他们的实现都是前端的那套,就想着 Qml 有没有类似实现. 结果百度了一圈也没有( T_T Qml 凉了凉了 ),于是,我按照自己理解 ...

  4. EF Core – Owned Entity Types & Complex Types

    前言 EF Core 8.0 推出了 Complex Types,这篇要来介绍一下. 由于它和 Owned Entity Types 傻傻分不清楚,加上我之前也没有写过 Owned Entity Ty ...

  5. SQL Server Temporary Table & Table Variable (临时表和表变量)

    参考: 在数据库中临时表什么时候会被清除呢 Temporary Tables And Table Variables In SQL 基本常识 1. 局部临时表(#开头)只对当前连接有效,当前连接断开时 ...

  6. Oracle 到 MySQL 函数替换方案汇总

    常用函数和语法转换     NVL函数 Oracle语法: NVL(COUNT(*), 0) MySQL语法: IFNULL(COUNT(*), 0)   转字符串 Oracle语法: to_char ...

  7. Servlet——xml配置Servlet

    XML配置方式编写 Servlet   步骤:   1.编写 Servlet 类   2.在 web.xml 中配置该Servlet     

  8. 暑假集训SCP提高模拟10

    我(看着百度百科):我已经知道这场谁组的题了 CTH: 谁 我:你想想,能在模拟赛里塞四道数学题还玩邦的,还能有谁 CTH: 我不知道 我:我不知道 CTH: 我知道了 我:我知道了 我:我是 Bob ...

  9. [OI] throw

    throw 主要是用来抛出异常. throw 可以直接向主程序 throw 一个东西,可以是各种数据类型,显示在界面上就是抛出的数据类型. int main(){ throw 1; } termina ...

  10. 巅峰对话在线研讨 Q&A:Oracle Database 21c vs openGauss 2.0新特性解读和架构演进

    2021年11月11日,墨天轮<巅峰对话>栏目邀请到了两位数据库领域的巅峰人物:云和恩墨创始人盖国强老师,和来自清华大学计算机与技术系的李国良教授,为大家带来了在线研讨<Oracle ...