1 Spark-Codegen 1.1 背景介绍 SparkSQL的优越性能背后有两大技术支柱:Optimizer和Runtime。前者致力于寻找最优的执行计划,后者则致力于把既定的执行计划尽可能快地执行出来。Runtime的多种优化可概括为两个层面:
1. 全局优化 。
从提升全局资源利用率、消除数据倾斜、降低IO等角度做优化,包括自适应执行(Adaptive Execution), Shuffle Removal等。
2. 局部优化 。
优化具体的Task的执行效率,主要依赖Codegen技术,具体包括Expression级别和WholeStage级别的Codegen。 本文介绍Spark Codegen的技术原理。
1.2 Case Study 本节通过两个具体case介绍Codegen的做法。
1.2.1 Expression级别 考虑下面的表达式计算:x + (1 + 2),用scala代码表达如下:
1 Add (Attribute (x), Add (Literal (1 ), Literal (2 )))
语法树如下:
递归求值这棵语法树的常规代码如下:
1 2 3 4 5 tree.transformUp { case Attribute (idx) => Literal (row.getValue(idx)) case Add (Literal (c1),Literal (c2)) => Literal (c1+c2) case Literal (c) => Literal (c) }
太复杂了
执行上述代码需要做很多类型匹配、虚函数调用、对象创建等额外逻辑,这些overhead远超对表达式求值本身。 为了消除这些overhead,Spark Codegen直接拼成求值表达式的java代码并进行即时编译。具体分为三个步骤:
1. 代码生成。
根据语法树生成java代码,封装在wrapper类中:
1 2 3 ... row.getValue(idx) + (1 + 2 ) ...
2. 即时编译。
使用Janino框架把生成代码编译成class文件。
3. 加载执行。
最后加载并执行。 优化前后性能有数量级的提升。
1.2.2 WholeStage级别 考虑如下的sql语句:
1 2 select count (* ) from store_saleswhere ss_item_sk= 1000 ;
生成的物理执行计划如下:
执行该计划的常规做法是使用火山模型(vocano model),每个Operator都继承了Iterator接口,其next()方法首先驱动上游执行拿到输入,然后执行自己的逻辑。
代码示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Agg extends Iterator [Row ] { def doAgg () { while (child.hasNext()) { val row = child.next(); ... } } def next (): Row { if (!doneAgg) { doAgg(); } return aggIter.next(); } } class Filter extends Iterator [Row ] { def next (): Row { var current = child.next() while (current != null && !predicate(current)) { current = child.next() } return current; } }
从上述代码可知,火山模型会有大量类型转换和虚函数调用。虚函数调用会导致CPU分支预测失败,从而导致严重的性能回退。 为了消除这些overhead,Spark WholestageCodegen会为该物理计划生成类型确定的java代码,然后类似Expression的做法即时编译和加载执行。 本例生成的java代码示例如下(非真实代码,真实代码片段见后文):
1 2 3 4 5 6 var count = 0 for (ss_item_sk in store_sales) { if (ss_item_sk == 1000 ) { count += 1 } }
优化前后性能提升数据如下:
Spark Codegen框架有三个核心组成部分
1. 核心接口/类 2. CodegenContext 3. Produce-Consume Pattern
1.2.2.1 四个核心接口 1. CodegenSupport(接口)
实现该接口的Operator可以将自己的逻辑拼成java代码。重要方法:
实现类包括但不限于: ProjectExec, FilterExec, HashAggregateExec, SortMergeJoinExec。
2. WholeStageCodegenExec(类) CodegenSupport的实现类之一,Stage内部所有相邻的实现CodegenSupport接口的Operator的融合,产出的代码把所有被融合的Operator的执行逻辑封装到一个Wrapper类中,该Wrapper类作为Janino即时compile的入参。
3. InputAdapter(类) CodegenSupport的实现类之一,胶水类,用来连接WholeStageCodegenExec节点和未实现CodegenSupport的上游节点。
4. BufferedRowIterator(接口) WholeStageCodegenExec生成的java代码的父类,重要方法:
1 2 public InternalRow next() public void append(InternalRow row)
1.2.2.2 CodegenContext 管理生成代码的核心类。主要涵盖以下功能:
**1.命名管理。**保证同一Scope内无变量名冲突。
**2.变量管理。**维护类变量,判断变量类型(应该声明为独立变量还是压缩到类型数组中),维护变量初始化逻辑等。
**3.方法管理。**维护类方法。
**4.内部类管理。**维护内部类。
**5.相同表达式管理。**维护相同子表达式,避免重复计算。
**6.size管理。**避免方法、类size过大,避免类变量数过多,进行比较拆分。如把表达式块拆分成多个函数;把函数、变量定义拆分到多个内部类。
**7.依赖管理。**维护该类依赖的外部对象,如Broadcast对象、工具对象、度量对象等。
**8.通用模板管理。**提供通用代码模板,如genComp, nullSafeExec等。
1.2.2.3 Produce-Consume Pattern 相邻Operator通过Produce-Consume模式生成代码。 Produce生成整体处理的框架代码,例如aggregation生成的代码框架如下:
1 2 3 4 5 6 7 8 9 10 11 12 if (!initialized) { # create a hash map, then build the aggregation hash map # call child.produce() initialized = true ; } while (hashmap.hasNext()) { row = hashmap.next(); # build the aggregation results # create variables for results # call consume(), which will call parent.doConsume() if (shouldStop()) return ; }
Consume生成当前节点处理上游输入的Row的逻辑。如Filter生成代码如下:
1 2 3 4 # code to evaluate the predicate expression, result is isNull1 and value2 if (!isNull1 && value2) { # call consume(), which will call parent.doConsume() }
1.2.3 WholeStageCodegen 下图比较清晰地展示了 WholestageCodegen 生成java代码的call graph:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
Case Study的示例,生成的真实代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 == Subtree 1 / 2 == *(2 ) HashAggregate (keys=[], functions=[count(1 )], output=[count(1 )#326 L]) +- Exchange SinglePartition +- *(1 ) HashAggregate (keys=[], functions=[partial_count(1 )], output=[count#329 L]) +- *(1 ) Project +- *(1 ) Filter (isnotnull(ss_item_sk#13 L) && (ss_item_sk#13 L = 1000 )) +- *(1 ) FileScan parquet [ss_item_sk#13 L] Batched : true , Format : Parquet , Location : InMemoryFileIndex [file:/home/admin/zhoukeyong/workspace/tpc/tpcds/data/parquet/10 /store_sales/par..., PartitionFilters : [], PushedFilters : [IsNotNull (ss_item_sk), EqualTo (ss_item_sk,1000 )], ReadSchema : struct<ss_item_sk:bigint> Generated code: public Object generate(Object [] references) { return new GeneratedIteratorForCodegenStage2 (references); } final class GeneratedIteratorForCodegenStage2 extends org .apache .spark .sql .execution .BufferedRowIterator { private Object [] references; private scala.collection.Iterator [] inputs; private boolean agg_initAgg_0; private boolean agg_bufIsNull_0; private long agg_bufValue_0; private scala.collection.Iterator inputadapter_input_0; private org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter [] agg_mutableStateArray_0 = new org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter [1 ]; public GeneratedIteratorForCodegenStage2 (Object [] references) { this .references = references; } public void init(int index, scala.collection.Iterator [] inputs) { partitionIndex = index; this .inputs = inputs; inputadapter_input_0 = inputs[0 ]; agg_mutableStateArray_0[0 ] = new org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter (1 , 0 ); } private void agg_doAggregateWithoutKey_0() throws java.io.IOException { agg_bufIsNull_0 = false ; agg_bufValue_0 = 0 L; while (inputadapter_input_0.hasNext() && !stopEarly()) { InternalRow inputadapter_row_0 = (InternalRow ) inputadapter_input_0.next(); long inputadapter_value_0 = inputadapter_row_0.getLong(0 ); agg_doConsume_0(inputadapter_row_0, inputadapter_value_0); if (shouldStop()) return ; } } private void agg_doConsume_0(InternalRow inputadapter_row_0, long agg_expr_0_0) throws java.io.IOException { long agg_value_3 = -1 L; agg_value_3 = agg_bufValue_0 + agg_expr_0_0; agg_bufIsNull_0 = false ; agg_bufValue_0 = agg_value_3; } protected void processNext() throws java.io.IOException { while (!agg_initAgg_0) { agg_initAgg_0 = true ; long agg_beforeAgg_0 = System .nanoTime(); agg_doAggregateWithoutKey_0(); ((org.apache.spark.sql.execution.metric.SQLMetric ) references[1 ] ).add((System .nanoTime() - agg_beforeAgg_0) / 1000000 ); ((org.apache.spark.sql.execution.metric.SQLMetric ) references[0 ] ).add(1 ); agg_mutableStateArray_0[0 ].reset(); agg_mutableStateArray_0[0 ].zeroOutNullBytes(); agg_mutableStateArray_0[0 ].write(0 , agg_bufValue_0); append((agg_mutableStateArray_0[0 ].getRow())); } } } == Subtree 2 / 2 == *(1 ) HashAggregate (keys=[], functions=[partial_count(1 )], output=[count#329 L]) +- *(1 ) Project +- *(1 ) Filter (isnotnull(ss_item_sk#13 L) && (ss_item_sk#13 L = 1000 )) +- *(1 ) FileScan parquet [ss_item_sk#13 L] Batched : true , Format : Parquet , Location : InMemoryFileIndex [file:/home/admin/zhoukeyong/workspace/tpc/tpcds/data/parquet/10 /store_sales/par..., PartitionFilters : [], PushedFilters : [IsNotNull (ss_item_sk), EqualTo (ss_item_sk,1000 )], ReadSchema : struct<ss_item_sk:bigint> Generated code: public Object generate(Object [] references) { return new GeneratedIteratorForCodegenStage1 (references); } final class GeneratedIteratorForCodegenStage1 extends org .apache .spark .sql .execution .BufferedRowIterator { private Object [] references; private scala.collection.Iterator [] inputs; private boolean agg_initAgg_0; private boolean agg_bufIsNull_0; private long agg_bufValue_0; private long scan_scanTime_0; private boolean outputMetaColumns; private int scan_batchIdx_0; private org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter [] scan_mutableStateArray_3 = new org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter [3 ]; private org.apache.spark.sql.vectorized.ColumnarBatch [] scan_mutableStateArray_1 = new org.apache.spark.sql.vectorized.ColumnarBatch [1 ]; private scala.collection.Iterator [] scan_mutableStateArray_0 = new scala.collection.Iterator [1 ]; private org.apache.spark.sql.execution.vectorized.OffHeapColumnVector [] scan_mutableStateArray_2 = new org.apache.spark.sql.execution.vectorized.OffHeapColumnVector [1 ]; public GeneratedIteratorForCodegenStage1 (Object [] references) { this .references = references; } public void init(int index, scala.collection.Iterator [] inputs) { partitionIndex = index; this .inputs = inputs; scan_mutableStateArray_0[0 ] = inputs[0 ]; outputMetaColumns = false ; scan_mutableStateArray_3[0 ] = new org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter (1 , 0 ); scan_mutableStateArray_3[1 ] = new org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter (1 , 0 ); scan_mutableStateArray_3[2 ] = new org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter (1 , 0 ); } private void agg_doAggregateWithoutKey_0() throws java.io.IOException { agg_bufIsNull_0 = false ; agg_bufValue_0 = 0 L; if (scan_mutableStateArray_1[0 ] == null ) { scan_nextBatch_0(); } while (scan_mutableStateArray_1[0 ] != null ) { int scan_numRows_0 = scan_mutableStateArray_1[0 ].numRows(); int scan_localEnd_0 = scan_numRows_0 - scan_batchIdx_0; for (int scan_localIdx_0 = 0 ; scan_localIdx_0 < scan_localEnd_0; scan_localIdx_0++) { int scan_rowIdx_0 = scan_batchIdx_0 + scan_localIdx_0; if (!scan_mutableStateArray_1[0 ].validAt(scan_rowIdx_0)) { continue ; } do { boolean scan_isNull_0 = scan_mutableStateArray_2[0 ].isNullAt(scan_rowIdx_0); long scan_value_0 = scan_isNull_0 ? -1 L : (scan_mutableStateArray_2[0 ].getLong(scan_rowIdx_0)); if (!(!scan_isNull_0)) continue ; boolean filter_value_2 = false ; filter_value_2 = scan_value_0 == 1000 L; if (!filter_value_2) continue ; ((org.apache.spark.sql.execution.metric.SQLMetric ) references[2 ] ).add(1 ); agg_doConsume_0(); } while (false ); } scan_batchIdx_0 = scan_numRows_0; scan_mutableStateArray_1[0 ] = null ; scan_nextBatch_0(); } ((org.apache.spark.sql.execution.metric.SQLMetric ) references[1 ] ).add(scan_scanTime_0 / (1000 * 1000 )); scan_scanTime_0 = 0 ; } private void scan_nextBatch_0() throws java.io.IOException { long getBatchStart = System .nanoTime(); if (scan_mutableStateArray_0[0 ].hasNext()) { scan_mutableStateArray_1[0 ] = (org.apache.spark.sql.vectorized.ColumnarBatch )scan_mutableStateArray_0[0 ].next(); ((org.apache.spark.sql.execution.metric.SQLMetric ) references[0 ] ).add(scan_mutableStateArray_1[0 ].numRows()); scan_batchIdx_0 = 0 ; scan_mutableStateArray_2[0 ] = (org.apache.spark.sql.execution.vectorized.OffHeapColumnVector ) (outputMetaColumns ? scan_mutableStateArray_1[0 ].column(0 , true ) : scan_mutableStateArray_1[0 ].column(0 )); } scan_scanTime_0 += System .nanoTime() - getBatchStart; } private void agg_doConsume_0() throws java.io.IOException { long agg_value_1 = -1 L; agg_value_1 = agg_bufValue_0 + 1 L; agg_bufIsNull_0 = false ; agg_bufValue_0 = agg_value_1; } protected void processNext() throws java.io.IOException { while (!agg_initAgg_0) { agg_initAgg_0 = true ; long agg_beforeAgg_0 = System .nanoTime(); agg_doAggregateWithoutKey_0(); ((org.apache.spark.sql.execution.metric.SQLMetric ) references[4 ] ).add((System .nanoTime() - agg_beforeAgg_0) / 1000000 ); ((org.apache.spark.sql.execution.metric.SQLMetric ) references[3 ] ).add(1 ); scan_mutableStateArray_3[2 ].reset(); scan_mutableStateArray_3[2 ].zeroOutNullBytes(); scan_mutableStateArray_3[2 ].write(0 , agg_bufValue_0); append((scan_mutableStateArray_3[2 ].getRow())); } } }