文章目录
SparkSQL在执行物理计划操作RDD时,会全部使用RDD<InternalRow>类型进行操作。
lnternalRow 体系
在SparkSQL 内部实现中, InternalRow 就是用来表示一行行数据的类,物理算子树节点产生和转换的RDD 类即为RDD [InternalRow] 。此外, InternalRow 中的每一列都是Catalyst 内部定义的数据类型。
从类的定义来看, InternalRow 作为一个抽象类,包含numFields 和update 方法,以及各列数据对应的get 与set 方法,但具体的实现逻辑体现在不同的子类中。需要注意的是, InternalRow中都是根据下标来访问和操作列元素的。
整个InternalRow 体系比较简单,其具体的实现不多,包括BaseGenericinternalRow、UnsafeRow 和JoinedRow 3 个直接子类。
-
JoinedRow :顾名思义,该类主要用于Join 操作,将两个InternalRow 放在一起形成新的InternalRow
-
UnsafeRow :不采用Java 对象存储的方式,避免了JVM 中垃圾回收( GC )的代价。此外,UnsafeRow 对行数据进行了特定的编码,使得存储更加高效。UnsafeRow将java对象序列化为字节数组进行储存,使用JAVA Unsafe的位操作对象的fields进行set and get。
-
BaseGenericlnternalRow :同样是一个抽象类,实现了InternalRow 中定义的所有get 类型方法,这些方法的实现都通过调用类中定义的genericGet 虚函数进行,该函数的实现在下一级子类中。
-
GenericlnternalRow 构造参数是Array[ Any]类型,采用对象数组进行底层存储, genericGet 也是直接根据下标访问的。这里需要注意,数组是非拷贝的,因此一旦创建,就不允许通过set 操作进行改变。
-
SpecificlnternalRow 则是以Array[MutableValue]为构造参数的,允许通过set 操作进行修改。
-
MutableUnsafe Row 和UnsafeRow 相关,用来支持对特定的列数据进行修改
-
数据源 RDD[lnternalRow]
public static class Person implements Serializable {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
Person person = new Person();
person.setName("Andy");
person.setAge(32);
// Encoders are created for Java beans
Encoder<Person> personEncoder = Encoders.bean(Person.class);
Dataset<Person> javaBeanDS = sparkSession.createDataset(
Collections.singletonList(person),
personEncoder
);
上述代码使用sparkSession.createDataset创建Dataset<Person>.
encoderFor[T]是ExpressionEncoder对象,即传入的personEncoder。利用ExpressionEncoder将Person对象转换为InternalRow对象。
由GenerateUnsafeProjection可以看出,最终将java对象转换为了UnsafeRow对象,序列化方式为ExpressionEncoder构造参数中的serializer。
将所有的JAVA对象序列化为UnsafeRow,构造LocalRelation逻辑计划。
LocalRelation逻辑计划会转换为LocalTableScanExec物理计划(根绝注释和类名称猜测,并没有查看源码实证)。
LocalTableScanExec会将LocalRelation逻辑计划中的rows: Seq[InternalRow]使用UnsafeProjection转换为UnsafeRow。这是因为LocalRelation逻辑计划中的rows还有可能是GenericInternalRow(其他的构造方式)。
所以从这我们可以知道,LocalTableScanExec物理计划得到的RDD<InternalRow>都是UnsafeRow类型。同理FileSourceScanExec、RowDataSourceScanExec等数据源算子得到的RDD<InternalRow>都是UnsafeRow类型,即序列化的二进制字节数组。
Shuffle RDD[InternalRow]
SparkSQL中的Shuffle 使用ShuffleExchangeExec物理算子实现,
传入ShuffleDependency, 构造ShuffledRowRDD。shuffle write和shuffle read的使用的序列化器和反序列化器是ShuffleDependency构造参数中的serializer。
ShuffleExchangeExec中构造ShuffleDependency是传入的serializer是其自身内部的serializer变量
所以ShuffleExchangeExec在序列化时,shuffle write将UnsafseRow序列化为二进制字节数组写入本地文件,shuffle read再将二进制字节数组反序列化为UnsafseRow。由于shuffle时只需要传输对象数据,而对象早已被序列化存入了UnsafseRow,所以UnsafeRowSerializer只需要将UnsafseRow内部对象的序列化字节数组写出或者读取即可,节省了序列化和反序列化的消耗。
Transform RDD[InternalRow]
SparkSQL的Transform操作有两种类型,一种为强类型化转换算子,另一种为利用内置的schmea隐式转换算子。
强类型化转换算子:filter(func : scala.Function1[T, scala.Boolean]),map[U](func : scala.Function1[T, U]),需要明确申明操作的数据类型,并且传入对象的Encoder。
利用内置的schmea隐式转换算子:filter(conditionExpr : root.scala.Predef.String),select(col: String, cols: String*),不需要说明操作数据类型,直接利用内置的schmea操作数据列。
强类型化转换算子
对于强类型化转换算子,比如map操作,DataSet源码中:
可以看到map操作会MapElements逻辑算子的上下接上DeserializeToObject和SerializeFromObject逻辑算子,最终会转换为DeserializeToObjectExec和SerializeFromObjectExec物理算子,其利用Encoder对UnsafeRow进行序列化和反序列化。
DeserializeToObjectExec先将UnsafeRow中的字节数组反序列化为JAVA对象储存在GenericInternalRow中,MapElement再对GenericInternalRow中的对象进行map操作,然后SerializeFromObjectExec再将GenericInternalRow中的JAVA对象序列化字节数组储存在UnsafeRow中。
同理filter强类型算子也需要对UnsafeRow反序列化为JAVA对象(不需要DeserializeToObjectExec算子),然后进行判断,但是其不需要再进行序列化操作。
利用内置的schmea隐式转换算子
然而对于利用内置的schmea隐式转换算子,比如filter,select可以利用内置的schmea直接操作Unsafe中的二进制字节数组,而不需要反序列化整个JAVA对象,从而节省了序列化/反序列化的消耗。
连续的强类型化转换算子
如果Dataset连接了多个连续的强类型化转换算子,那么如果每一次强类型化转换算子都进行一次反序列化和序列化,那就消耗太大。Spark对此进行了优化,将反序列化操作,即DeserializeToObjectExec放到第一个强类型化转换算子前,将序列化操作SerializeFromObjectExec,放到最后一个强类型化转换算子后面。
比如:
可以看到DeserializeToObject和SerializeFromObject中间包括了连续的多个强类型化转换算子。
由于filter(“x > 2”)是利用内置的schmea隐式转换算子,其操作的是二进制序列化字节数组,所以不能包含在DeserializeToObject和SerializeFromObject之间。同理ExchangeExec操作的也是是二进制序列化字节数组,也不能包含在DeserializeToObject和SerializeFromObject之间。
Encoder对InternalRow的影响
Encoder是将JAVA对象转换为InternalRow的工具。SparkSQL提供了Encoders的很多静态工厂方法获得Encoder(实际上目前获得的都是ExpressionEncoder
)。大致可以分为几类:
- java原始类型:
Encoders.BOOLEAN
等 - scala原始类型:
Encoders.scalaBoolean
等.(多一个scala前缀) - javaBean类型:
bean[T](beanClass: Class[T])
。但目前成员只支持List容器,不支持其他的容器。支持原始类型或嵌套javaBean。 - kryo序列化类型:
kryo[T: ClassTag]
; - java序列化类型:
javaSerialization[T: ClassTag]
; - Tuple类型: 从Tuple2到Tuple5.
- Product类型: 也就是
case class
.
其中前三种是直接调用ExpressionEncoder
,第四第五种本质上是间接调用了ExpressionEncoder
。
所以第四第五后两种序列化本质上是把整个对象看做一个二进制类型,其储存在UnsafeRow中时,只有一个列value,其类型时binary。所以Spark没有办法操作其二进制数据直接获取或者设置field,只能反序列化成JAVA对象才能进行操作,不利于后续优化和减少反序列化。
所以在使用DataSet传入kryo或者javaSerialization的Encoder时,不能使用内置的schmea隐式转换算子,因为内置的schmea只有一个类型为binary的列(value),只能使用强类型转换算子,效率较低。
所以要尽量避免使用kryo或者javaSerialization的Encoder,而是用bean类型的Encoder(具有setter\getter函数)和其他类型的Encoder,其可以直接将JAVA对象的field转换为UnsafeRow中的列,从而使用利用内置的schmea隐式转换算子,减少序列化/反序列化的消耗。
总结
SparkSQL使用InternalRow优化RDD操作。对于文件数据源,直接将文件的二进制数据读取到UnsafeRow中,不需要反序列化对象。在shuffle wirte时,直接将UnsafeRow中的二进制数组直接写出,不需要序列化对象, shuffle read时,直接读入二进制数组储存在UnsafeRow中,不需要反序列化对象。当进行数据操作时,直接操作UnsafeRow中的二进制数组,而不需要反序列化整个JAVA对象。所以UnsafeRow大大减少了序列化和反序列的消耗,并且可以减少数组在内存中占用的空间,避免fullgc,可以精确的计算内存使用情况,避免OOM。
Row中的二进制数组直接写出,不需要序列化对象, shuffle read时,直接读入二进制数组储存在UnsafeRow中,不需要反序列化对象。当进行数据操作时,直接操作UnsafeRow中的二进制数组,而不需要反序列化整个JAVA对象。所以UnsafeRow大大减少了序列化和反序列的消耗,并且可以减少数组在内存中占用的空间,避免fullgc,可以精确的计算内存使用情况,避免OOM。
但是对于一些对象,其schema并不是很明确,只能使用kryo或者java方式进行序列化,对于这种类型的数据进行操作时,只能使用强类型转换算子。强类型转换算子会对UnsafeRow中二进制数据进行反序列进行操作,最终再序列化为二进制数据,储存再UnsafeRow中,所以效率较低。