[toc]
Binary Row是blink开源版本https://github.com/apache/flink/tree/blink中提到的一个runtime层面优化的特性,主要是应用于sql模块,简单来说,由于sql本身自带schema,在上下游数据传输的时候就可以利用这个schema信息来简化序列化和反序列化的过程,本文就来具体分析这个特性的实现。
主要实现代码在
1 | flink-table org.apache.flink.table.typeutils |
Binary Row
我们要理解sql层的数据传输是用的什么结构,只需要去观察runtime层实现的算子的传输数据类型即可,通过查看代码可以发现中间算子传输的均为BaseRow
1 | public TwoInputSelection processElement1(StreamRecord<BaseRow> element) throws Exception {} |
而之前版本的数据传输的是一个Row,内部是一个Object[],在传输的过程中使用RowSerializer进行每一个字段的序列化和反序列化
1 | public void serialize(Row record, DataOutputView target) throws IOException { |
新的实现中以BaseRow代替了Row,baserow是一个基类,在不同的场景下有不同的子类去实现相应的功能。
GenericRow
能够方便的用以更新字段,其内部实现也是一个Object数组,
1 | // kafka source deserialization schema 从source处就解析成一个BaseRow |
JoinedRow
主要能够方便的将两个row进行拼接成一个baserow
1 | // windowoperator中发送一个key和aggRes的组合的row到下游 |
BinaryRow
BaseRow序列化是先转化成BinaryRow,然后再通过BinaryRowSerializer进行序列化
1 | // BaseRowSerializer.java |
可以看到baseToRow的过程中首先是会通过codeGen生成映射函数,然后将baserow转成binaryrow,测试将以下的GenericRow转化成BinaryRow生成如下代码
1 | GenericRow gR = new GenericRow(3); |
1 | public class BaseRowSerializerProjection$0 extends org.apache.flink.table.codegen.Projection<org.apache.flink.table.dataformat.BaseRow, org.apache.flink.table.dataformat.BinaryRow> { |
通过codeGen的代码可以得出,对于普通的baserow,通过BinaryRowWriter,将baserow的每个字段写入到BinaryRow中,写入完成后,序列化的工作就都通过BinaryRowSerializer来完成。这样的好处有以下几个:
- 如果某个中间算子只需要获取上游传输下来的某几个字段的值,那么只需要通过getXXX来直接获取,减少反序列化的量
- 如果中间结果不发生改变,只需要将binaryRow直接拷贝出去,也减少了序列化的量
BinaryRow序列化的过程,可以看到就是直接的内存拷贝的过程
1 | public void serialize(BinaryRow record, DataOutputView target) throws IOException { |
BinaryString
在上面codegen的一段代码中,关于string的处理引入了一个概念BinaryString
1 | field$4 = org.apache.flink.table.dataformat.BinaryString.EMPTY_UTF8; |
1 | // GenericRow的getBinaryString的实现 |
那么BinaryString是什么作用呢? 看注释
A utf8 string which is backed by {@link MemorySegment} instead of String. Its data may span multiple {@link MemorySegment}s.
一个直接存储在MemorySegment上的utf8的字符串,一个字符串数据可能会跨segment
1 | private MemorySegment[] segments; |
针对string类型,会在codegen阶段,将其转化成一个binaryString。从binarystring初始化的时候没有存储在MemorySegment之上,而是仅仅只是保存string的字符串信息,等到有对string的操作的时候,才会通过这个方法将其序列化,并包装成memorysegments
1 | public void ensureEncoded() { |
序列化
1 | public void serialize(BinaryString record, DataOutputView target) throws IOException { |
BinaryString有什么好处呢?
- 仅仅序列化一次
- 是可以修改的string,而不会产生中间对象
- 序列化的时候仅仅是内存的拷贝
BinaryArray
同样的基于memorysegment实现的还有BinaryMap和BinaryArray,这两者都有一个Generic的实现用以快速的更新, GenericArray要求数据类型都是相同的类型
BinaryArray的存储格式:
[numElements(int)] + [null bits(4-byte word boundaries)] + [values or offset&length] + [variable length part].
1 | public void serialize(BaseArray record, DataOutputView target) throws IOException { |
从BinaryArraySerializer可以看到,处理方式和BaseRow很像,先baseToBinary,然后直接从segments拷贝byte。
DataStructureConverters
以上的类型和这个类型convert搭配使用才发挥出相应的效果,这个工具类的作用在于在codegen的阶段,根据输入的类型去转化为相对应的InternalType
1 | def genToInternal(ctx: CodeGeneratorContext, t: DataType): String => String = { |
这样在codegen阶段就完成了相应类型的替换
BinaryRow和BinaryArray的底层存储
上面我们看到了binaryRow的使用方式,通过writeXXX的方式将数据写入到一个row中,一个row中可以写入基本类型,也可以写入binaryString, binaryArray,binaryMap等等变长的数据结构,其存储方式如下所示:

A Row has two part: Fixed-length part and variable-length part.
Fixed-length part contains null bit set and field values. Null bit set is used for null tracking and is aligned to 8-byte word boundaries.Field valuesholds fixed-length primitive types and variable-length values which can be stored in 8 bytes inside. If it do not fit the variable-length field, then store the length and offset of variable-length part. Fixed-length part will certainly fall into a MemorySegment, which will speed up the read and write of field.
Variable-length part may fall into multiple MemorySegments.
在将其他格式通过baseRowToBinaryRow的时候,确定了BinaryRow包含的field个数,
1 | public BinaryRowWriter(BinaryRow row, int initialSize) { |
1 | public static int calculateBitSetWidthInBytes(int arity) { |
这个函数是计算出null值得Flag位需要多少Byte来表示,这里的+63是将其对其到8的倍数(向上去整的意思),+8和spark代码相比其实是因为flink多了一个header存储回撤消息的标志位,null bits中第一个byte存储了header位的信息,这里的null bits的作用主要是用在runtime处理时可以快速判断一条数据是不是null值。
1 | public boolean anyNull() { |
1 | public int getFixedLengthPartSize() { |
获取整个固定长度字段的长度,再写变长区时就从这个offset写起。
写定长数据
1 | public void writeByte(int pos, byte value) { |
写不定长的数据1
2
3
4
5
6
7
8
9
10public void writeString(int pos, String input) {
byte[] bytes = StringUtf8Utils.allocateBytes(input.length() * MAX_BYTES_PER_CHAR);
int len = StringUtf8Utils.encodeUTF8(input, bytes);
if (len <= 7) {
// 小于记录length的长度时直接写在固定长度区
writeLittleBytes(segment, getFieldOffset(pos), bytes, len);
} else {
writeBigBytes(pos, bytes, len);
}
}
BinaryRow/BinaryRowWriter的实现和Spark中UnsafeRow/UnsafeRowWriter的实现非常相似,spark中的对此数据结构解释更为清晰一些。
1 | /** |
题外话
看代码的时候遵循的是一种推理加源码追踪的手段,但是代码在初始设计的时候应该是另一种维度的思考,因此应该换一种思路去想如果自己来实现这个feature需要考虑什么地方,多多思考。