2021SC@SDUSC
山大软工实践hive(2)-解析输入(OPTree是什么)
目标
我的分析任务如下图
可以看见,这一层输入为OP Tree,经过逻辑优化后输出也为OP Tree。所以我要先搞明白OP Tree是什么
分析输入
首先要了解我的任务上一步传入的输入Operator Tree是个什么东西,才能理解这一步干什么
根据其他博客内容,应该与类Operator(算子)有关。Operator是个抽象类,后面被如CommonJoinOperator继承
(org.apache.hadoop.hive.ql.exec.Operator)
下面的代码段的三个变量证明Operator类最终会构成一个有向无环图(DAG),每个节点有节点id,有记录它们的父节点与子节点的两个数组
......
protected List<Operator<? extends OperatorDesc>> childOperators;
protected List<Operator<? extends OperatorDesc>> parentOperators;
protected String operatorId;
......
public void initOperatorId() {
this.operatorId = getName() + "_" + this.id;
}
还有个初始化方法initOperatorId(),可以看到operatorId的结构,但实际没找到有调用。疑是在做查询计划(我的任务的上一步)把id分配好了
根据博客说法,在实际执行时,会调用initialize方法,然后process方法
在Operator类里process方法是接口,所以先看initialize方法
下面的initialize方法是初始化数组,并调用实际初始化方法
protected void initialize(Configuration hconf, ObjectInspector inputOI,
int parentId) throws HiveException {
......
if (parentId >= inputObjInspectors.length) {
int newLength = inputObjInspectors.length * 2;
while (parentId >= newLength) {
newLength *= 2;
}
inputObjInspectors = Arrays.copyOf(inputObjInspectors, newLength);
}
inputObjInspectors[parentId] = inputOI;
上面在给原数组扩容直到大小大于parentId,然后在数组parentId处赋予inputOI
下面为实际初始化
initialize(hconf, null);
}
实际初始化方法会 最后会尝试初始化子节点,最后执行结束初始化方法
public final void initialize(Configuration hconf, ObjectInspector[] inputOIs)
throws HiveException {
// String className = this.getClass().getName();
this.done = false;
this.runTimeNumRows = 0; // initializeOp can be overridden
// Initializing data structures for vectorForward
this.selected = new int[VectorizedRowBatch.DEFAULT_SIZE];
if (state == State.INIT) {
return;
}
if (inputOIs != null) {
inputObjInspectors = inputOIs;
}
Serilizable 让对象可被序列化,反序列化(输入输出流,文件系统) transient标识让相应变量不被序列化
这里的如childOperatorsArray就是transient变量,下面有几个transient对应的变量
// while initializing so this need to be done here instead of constructor
childOperatorsArray = new Operator[childOperators.size()];
上面有个setchildOperator方法(parent也有),childOperator的输入估计从那来的?
for (int i = 0; i < childOperatorsArray.length; i++) {
childOperatorsArray[i] = childOperators.get(i);
} 深拷贝
multiChildren = childOperatorsArray.length > 1;确认是否有多个子节点
childOperatorsTag = new int[childOperatorsArray.length];
下面的循环确认每个子节点的父节点的包括本节点
for (int i = 0; i < childOperatorsArray.length; i++) {
List<Operator<? extends OperatorDesc>> parentOperators =
childOperatorsArray[i].getParentOperators();
childOperatorsTag[i] = parentOperators.indexOf(this);
}
......
outputObjInspector = inputObjInspectors[0];
确认输出对象
......
initializeChildren(hconf);
......
// let's wait on the async ops before continuing
completeInitialization(asyncInitOperations);
}
protected void initializeChildren(Configuration hconf) throws HiveException {
state = State.INIT;
......
for (int i = 0; i < childOperatorsArray.length; i++) {
子节点们也要执行initialize方法
childOperatorsArray[i].initialize(hconf, outputObjInspector, childOperatorsTag[i]);
if (reporter != null) {
childOperatorsArray[i].setReporter(reporter);
}
}
}
private void completeInitialization(Collection<Future<?>> fs) throws HiveException {
......
completeInitializationOp(os);接口方法
}
这些代码表明有向无环图在这之前已被创建好,initialize方法只是在确认图的完整性、各种可能出现的问题、以及传递inputOI
看代码难以分析输入是什么,但幸运的是找到了宏观解析算子的博客
Operator是基本的抽象类,在此之上实现了各种算子如SEL(SelectOperator,用于执行投影,对应select),TS(TableScanOperator,每个对应一张表),FIL(对应where语句),LIM(对应limit),FS(对应数据输出),GBY(对应groupby)等等。这些算子对应HQL语句中各部分的操作以及输入输出。它们会构成DAG,代表如何运行
同时找到如SEL的process方法
@Override
public void process(Object row, int tag) throws HiveException {
......
int i = 0;
try {
for (; i < eval.length; ++i) {
output[i] = eval[i].evaluate(row);
}
}
......
forward(output, outputObjInspector);
}
对于个输入的行进行运算,然后foward方法把结果输出给下一个节点处理。不同类型的算子不同地实现process方法
但是在opeartor类里,initialize时并没有调用process,那么process在什么时候被调用呢?
根据查找,如MuxOperator,会调用子节点的process方法
@Override
public void process(Object row, int tag) throws HiveException {
......
int childrenDone = 0;
for (int i = 0; i < childOperatorsArray.length; i++) {
Operator<? extends OperatorDesc> child = childOperatorsArray[i];
.......
child.process(handlers[tag].process(row), handlers[tag].getTag());
.......
}
......
}
之所以拿上面举例因为还发现了该类的注释,让我意识到逻辑优化干了什么----就是改变DAG让处理起来更快
再说回宏观结构。博主举了几个例子,比如下面HQL语句对应的算子的DAG的结构
select* from a where id>100 limit 10
上图的逻辑是明显的,第一个算子不断从表读取一条元组,然后给FIL节点筛选掉不需要的,然后再让SEL对过滤下来的元组做投影,再让LIM负责限制得到的条目数,达到数目后终止任务,FS负责把处理好的结果写入文件
然后一个复杂点的转换
from student a join score b on a.id=b.id
insert overwrite table d1 selecta.id,count(b.score) group by a.id
insert overwrite table d2 select a.id,b.scoreorder by b.score limit 10
上图由于涉及join以及两条insert,产生了一个复杂些的DAG
左半侧上下部分分别对student,score表的元组读取过滤,由于涉及到join,所以需要RS算子(处理数据分发,排序等问题)。对于join筛选出的数据,再分叉分别执行两个insert语句
这里面的问题是RS到底做了什么,为什么在后半段还会出现SEL->RS->SEL这种重复SEL的操作。根据源代码的注释,说的是’RS将输出发送到reduce阶段’。可以认为的是,RS负责把map阶段的数据传给正确的reduce,比如根据某种映射区分reduce任务,但仍不知SEL->RS->SEL干了什么
下一步做什么
目前的问题是,不同的算子的运行逻辑,比如RS算子。只有对这些有一定了解,才能理解逻辑优化的细节之处