zoukankan      html  css  js  c++  java
  • Impala源代码分析(3)-backend查询执行过程

    这篇文章主要介绍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()向上游节点拉取的。
  • 相关阅读:
    sql over(partition by) 开窗函数的使用
    利用curl函数处理GET数据获取微信公众号的access_token
    2018.4.12
    字段和属性
    C#实现回车键登录
    判断DataTable里面数据是否有重复数据
    一个强大的人民币大写转换的正则表达式
    C#将image中的显示的图片转换成二进制
    遍历Dev LayoutControl中的所有控件信息
    遍历窗体中所有控件的信息
  • 原文地址:https://www.cnblogs.com/daichangya/p/12959093.html
Copyright © 2011-2022 走看看