Impala前端代码分析
1 概述
前端代码使用java。感觉使用java的原因是,本身语法分析不会占用太多时间,毫秒级可以完成,不是性能瓶颈。而且语法分析的代码通常比较复杂,逻辑较多,如果再自己管理内存的话,会影响开发效率。
Impala通过jni调用前端java代码,前后端数据传递采用thrift格式。
前端的代码主要包括以下几个功能:
- 将用户提交的query转换成语法树,这个通过jflex和cup完成(hive使用的是antlr)
- 语法分析
- 生成执行计划
- 在编译中访问元数据
2 语法分析和ParseNode
ParseNode定义了一个接口。jflex和cup分析完的语法树中,每一个节点都实现了这个接口。语法分析就是在这个ParseNode组成的树中进行的。
ParseNode的实现和继承关系如下:
- Expr (表达式)
- Predicate (谓词,出现在where语句中)
- BetweenPredicate
- BinaryPredicate (=, !=, <=, >=, <, >)
- CompoundPredicate (&&, ||, !)
- InPredicate
- IsNullPredicate
- LikePredicate
- LiteralPredicate
- BoolLiteral
- DateLiteral
- FloatLiteral
- IntLiteral
- NullLiteral
- StringLiteral
- AggregateExpr (count, min, max, distince, sum, avg)
- ArithmeticExpr (*, /, %, DIV, +, -, &, |, ^, ~)
- CaseExpr
- CastExpr
- FunctionCallExpr
- LiteralExpr
- TimestampArithmeticExpr
- Predicate (谓词,出现在where语句中)
- QueryStmt
- SelectStmt
- UnionStmt
- InsertStmt
- ShowDbStmt
- ShowTablesStmt
- DescribeStmt
- UseStmt
- TableRef
- BaseTableRef
- InlineViewTableRef (alias)
其中,Expr继承了TreeNode类,可以组成一个求值树。而其他ParseNode虽然没有继承TreeNode,但也都有类似的数据结构。可以访问孩子节点。这样ParseNode就组成了一颗语法树。
语法分析阶段主要进行了一些基本的语法检查,类型检查,并抽取一些信息,放到Analyzer类中,例如alias映射表等等。
3 Analyzer
Analyzer是语法分析的一个产出。Analyzer用于收集某一个QureyBlock内的信息。类似Hive中的QB类。
其主要成员变量包括:
- DescriptorTable descTbl;
- Catalog catalog;
- String defaultDb;
- IdGenerator<ExprId> conjunctIdGenerator;
- Analyzer parentAnalyzer;
- Map<String, TupleDescriptor> aliasMap;
- Map<String, SlotDescriptor> slotRefMap;
- Map<ExprId, Predicate> conjuncts;
- Map<TupleId, List<ExprId> > tuplePredicates;
- Map<SlotId, List<ExprId> > slotPredicates;
- Map<TupleId, List<ExprId> > eqJoinConjuncts;
- Set<ExprId> assignedConjuncts;
- Map<TupleId, TableRef> outerJoinedTupleIds;
- Map<TableRef, List<ExprId> > conjunctsByOjClouse;
- Map<ExprId, TableRef> ojClouseByConjunct;
- Set<ExprId> whereClauseConjuncts;
4 生成执行计划和Planner
Planner负责将分析后的语法树转换成执行计划,即PlanFragments。
Planner的入口函数是Planner::createPlanFragments()。其主要流程如下:
- createSinglePlan:
根据语法分析的结果,遍历并生成一个逻辑执行计划树。树中的节点是PlanNode。树的根节点是最后被执行的节点。
- createPlanFragments:
将生成的执行计划切割成多个PlanFragment。这个过程类似Hive中将执行计划分割成多个MapRed阶段。目前Impala划分阶段的规则是:如果遇到ScanNode, HashJoinNode, MergeNode, AggregationNode, SortNode,则产生一个新的PlanFragment。每一个PlanFragment之间通过ExchangeNode相连。ExchangeNode在运行时中会负责跨Impalad的数据传输。
- computeMemLayout:
对于输入表,计算在内存中的layout。主要目的是为了计算一条记录在内存中会占用多少字节(由TupleDescriptor描述),以及每一个字段在内存中如何分布(由SlotDescriptor描述)。
最后PlanFragments就会成为物理执行计划,转成thrift后返回给前端。
5 Catalog
Impala直接使用了Hive的HiveMeteStoreClient类,用于访问元数据。这就意味着,必须还要启动一个HiveMetaStore进程,用于提供元数据服务。目前Catalog默认采用lazy模式,只在需要时加载database和table对象。
Catalog主要在语法分析阶段使用,用于获取Hive元数据,目前在Planner中没有使用Catalog。
Impala学习–Impala后端代码分析
1 代码结构
- service: 连接前端,并接受client的请求
- runtime: 运行时需要的类,包括coordinator, datastream, mem-pool, tuple等
- exec: ExecNode,执行节点
- expr: 表达式求值
- transport: Thrift SASL: Simple Authentication and Security Layer
- statestore: 调度,nameservice,资源池
- codegen: 代码生成
2 StateStore
StateStore是一个C/S结构的信息订阅服务,在Impala里面,主要用于管理当前集群的membership状态,并用于调度和发现故障进程。StateStore是一个独立的进程,每一各impalad会建立一个或多个于StateStore的连接。
StateStore提供四个接口:
- RegisterService:向某个服务进行注册,就是加入并成为这个服务的成员
- UnregisterService:取消在某个服务的注册
- RegisterSubscription:订阅某一各服务的成员信息
- UnregisterSubscription:取消对某个服务成员信息的注册
StateStore会通过心跳来检查已经注册的成员是否还活着,判断条件可以是心跳超时,连续丢失n个心跳。StateStore定期向subscriber更新其所订阅服务的成员信息。目前的更新策略是全量更新,未来会考虑增量更新。通常在Impala集群中只存在一个服务,每个impalad都会注册这个服务。注册服务后,这个impalad就可以对其他impalad可见,这样就可以接受其他impalad发来的任务。
3 Scheduler
Coordinator在得到执行计划后,通过Scheduler得到可以执行的后端,并向这些后端发送执行命令
Scheduler提供两个接口
- GetHosts:提供一组访问数据所在的机器地址,返回一组和数据尽量接近的机器地址
- GetAllKnownHosts:返回所有还存活的机器地址。
SimpleScheduler是目前唯一的Scheduler的实现。Coordinator通过调用SimpleScheduler的GetHosts方法,调度和远端任务分配。在GetHosts方法中,采用的算法是:优先寻找已经和数据位置相同的后端,如果没有相同的,则采用roud-robin算法。目前SimpleScheduler没有考虑机器实时的负载情况。返回的后端数目取决于输入数据分布的机器数目。
4 impalad启动流程
- 初始化LLVM,hdfs,jni,hbase,前端
- 启动ImpalaServer
- 启动thriftserver,接受thrift请求
- 启动ExecEnv
- 启动webserver
- 启动SubscriptionManager
- 启动Scheduler
- 向StateStore订阅,并注册回调函数SimpleScheduler::UpdateMembership,用于调度时提供当前可用的后端
- SubscriptionManager::RegisterService
- StateStore检查service是否存在,如果不存在,则建立一个新的service_instance
- 检查客户端是否存在于这个service_instance的membership中,如果不存在,则添加一个
- SubscriptionMangaer::RegisterSubscription
- StateStore添加一个Subscriber,订阅这个service的membership,并注册回调函数MembershipCallback
- 当有update回调时,更新impala-server的membership状态,用于failure detector
Impalad启动后,就可以接受query请求,也可以接受其他impalad的请求,执行一个PlanFragment。
5 Coordinator
负责执行一组PlanFragments。同时负责响应client的请求。coordinator fragment在本地执行,其他发送到远程的impalad执行。coordinator同时监控整个执行状态。
Exec()函数是其最主要的函数,简要介绍一下这个函数中的流程::
- ComputeFragementExecParams():
- ComputeFragmentHosts():对于每一个Fragment,根据输入数据所在的节点,调用Scheduler的GetHosts方法,得到每个阶段在那些后端上执行
- 对于每一个Fragment,计算其ExchangeNode的参数
- ComputeScanRangeAssignment():计算每一个后端应该扫描多少数据。
- executor_ = new PlanFragmentExecutor()创建一个新的PlanFragmentExecutor。
- executor_->Prepare()
- 对于每一各fragment,对于每一个远程后端,调用ExecRemoteFragment。
- ProgressUpdater:定期更新状态。
6 ExecNode
所有ExecNode的父类。主要方法有Prepare(), Open(), GetNext(), Close(), CreateTree()ExecNode是真正在Impalad上处理数据的类,包括hash-join,聚合,scan等等。多个ExecNode组成了一颗执行树。root节点被最后执行,叶子节点被最先执行。
Impala中的执行顺序和Hive中相反。在Impala中,采用拖的方式,而Hive中采用推的方式。Impala中,执行入口是根节点的Open方法。Open方法会调用孩子节点的Open方法和GetNext方法。
主要数据结构包括:
- ObjectPool* pool_
- vector<Expr*> conjuncts_
- vector<ExecNode*> children_
- RowDescriptor row_descriptor_
主要函数包括:
- Prepare()在Open前被调用。code generation
- Open()在GetNext前被调用,准备工作。调用孩子节点的GetNext()
- GetNext()返回一组row,并标记eos
- EvalConjuncts()对所有表达式进行求值,并返回布尔结果
7 PlanFragmentExecutor
执行一个PlanFragment。包括初始化和清理。清理包括释放资源,关闭data stream。每一个Executor会有一个callback,用于汇报执行状态。
最主要的有三个函数,分别是:
- Status Prepare(TExecPlanFragmentParams):准备执行,主要流程如下:
- DescriptorTbl::Create():初始化descriptor table.
- ExecNode::CreateTree():初始化执行树。执行树由ExecNode组成。每一个ExecNode也提供了Prepare(), Open(), GetNext()三个函数。初始化完成后,plan_指向了执行树的根节点。
- plan_->Prepare():初始化执行树
- 如果可以使用代码生成,则调用runtime_state_->llvm_codegen()->OptimizedModule()进行优化
- set scan ranges
- set up sink, if required
- set up profile counter
- Status Open():开始执行,并启动一个独立的线程向coordinator汇报状态:
- plan_->Open()从根节点开始调用Open函数,开始执行。
- if has sink: sink_->Send()如果有写回操作,例如query中包含insert语句,则主动将计算结果推送到hdfs或hbase中。
- Status GetNext(RowBatch)用于触发执行树的GetNext函数。当GetNext返回done时,则表明所有数据已经被处理完,Executor可以退出了。