Flink|《Flink 官方文档 - DataStream API - 状态与容错 - 数据类型以及序列化 - 概览》学习笔记

学习文档:《Flink 官方文档 - DataStream API - 状态与容错 - 数据类型以及序列化 - 概览》

学习笔记如下:


Flink 使用独特的方式来处理数据类型以及序列化。

支持的数据类型

因为 Flink 需要根据类型选择更有效的执行策略,所以限制了允许在 DataStream 中存在的数据类型。

以下 7 种不同的数据类型:

Java Tuples and Scala Case Classes

元组(tuples)是一个包含固定数量的不同类型的变量的组合数据结构。Flink 的 Java API 提供了 Tuple1Tuple25,其中每个字段可以是任意 Flink 支持的类型。

访问元素:可以通过类似 tuple.f4tuple.getfield(int position) 方法访问指定位置的元素(其中元素下标从 0 开始)

样例:Java Tuples

DataStream<Tuple2<String, Integer>> wordCounts = env.fromElements(
 new Tuple2<String, Integer>("hello", 1),
 new Tuple2<String, Integer>("world", 2));

wordCounts.map(new MapFunction<Tuple2<String, Integer>, Integer>() {
 @Override
 public Integer map(Tuple2<String, Integer> value) throws Exception {
     return value.f1;
 }
});

wordCounts.keyBy(value -> value.f0);
POJOs

Java 或 Scala 的类如果完全满足以下条件,则会被 Flink 视为 POJOs 数据类型:

  • 类是公有的
  • 必须包含一个公有的无参数构造器
  • 所有字段要么是公有的,要么有 getter 方法和 setter 方法;例如,对于一个 foo 属性,其 getter 和 setter 必须命名为 getFoo()setFoo()
  • 字段类型必须被注册的序列化器支持

POJOs 通常被 PojoTypeInfo 表示并使用 PojoSerializer 序列化。

Flink 会分析 POJO 类型的结构,即了解 POJO 的字段。因此,POJO 类型比一般类型更容易使用,且相较于一般类型,Flink 可以处理得更高效。

验证类型是否为 POJO 的方法:

org.apache.flink.types.PojoTestUtils#assertSerializedAsPojo();

样例:拥有 2 个公有字段的 POJOs 类型

public class WordWithCount {

 public String word;
 public int count;

 public WordWithCount() {}

 public WordWithCount(String word, int count) {
     this.word = word;
     this.count = count;
 }
}

DataStream<WordWithCount> wordCounts = env.fromElements(
 new WordWithCount("hello", 1),
 new WordWithCount("world", 2));

wordCounts.keyBy(value -> value.word);
原生类型

Flink 支持 Java 和 Scala 的所有原生类型,例如 Integer、String、Double 等。

通用类类型

Flink 支持大多数 Java 和 Scala 类,包括来自 API 的和用户自定义的类,但是不支持包含无法序列化的字段的类(如文件指针、I / O流等)。

所有未被识别为 POJO 类型的类均会被视作通用类类型进行处理。Flink 将这些类视为黑盒,无法访问其内容。 使用序列化框架 Kryo 对通用类型进行解/序列化。

Values

Values 类型手动定义了它的序列化和反序列化方法。它们不使用通用的序列化框架,而是实现了 org.apache.flink.types.Value 接口的 readwrite 方法。当通用的序列化效率较低时,使用 Values 类型将会更加合适。例如,在写入稀疏元素数组时,通过对非零元素进行特殊变化,可以更高效地完成序列化和反序列化。

Flink 提供了对应于基本数据类型的预定义 Values 类型:ByteValueShortValueIntValueLongValueFloatValueDoubleValueStringValueCharValueBooleanValue。这些 Values 类型的值可以被改变、允许重用对象并可以减轻垃圾回收器的压力。

Hadoop Writables

可以试用实现了 org.apache.hadoop.Writable 接口的类型。Flink 会使用它的 write()readFields() 方法进行序列化。

Special Types

Flink 也支持包括 Scala 的 EitherOptionTry 的特殊类型。Flink 的 Java API 也有自定义的 Either,与 Scala 的 Either 类似,它表示一个可能是两个类型中的任意一个。

Either 的适用场景:异常处理;Operator 需要输出两种不同类型的记录。

Java 泛型的类型擦除及类型推理

Java 编译器会将泛型信息擦除,在运行时,一个对象的实例将不再了解其泛型信息。例如,对于 JVM 来说,DataStreamDataStream 看起来是一样的。

但是,Flink 在准备应用程序的过程中(即调用 main() 方法时)是需要类型信息的。因此,Flink 的 Java API 重建了类型信息,并将类型信息明确地添加到了数据集和 Operator 中。可以通过 DataStream.getType() 方法获取实例的类型信息。

这种类型推理他页具有局限性。例子,在使用从集合创建数据集的方法 StreamExecutionEnvironment.fromCollection() 时,需要额外传递一个描述类型的参数;又如,MapFunction 这样的泛型方法可能也需要额外的类型信息。

ResultTypeQueryable 接口可以通过输入格式和函数来明确地告诉 API 它们的返回类型。通常来说,函数的输入类型通常可以根据上游操作的结果类型来判断。

Flink 对类型的处理

Flink 推断出分布式计算过程中交换和存储的很多类型信息。在大多数情况下, Flink 可以准确地推断出所有必要的信息,从而让 Flink 可以支持如下特性:

  • Flink 对数据类型的了解,有助于更好地序列化和存储数据,优化内存使用方式
  • 在大多数情况下,用户不必担心序列化框架(serialization frameworks)以及类型注册

通常来说,在 pre-flight 阶段需要有关数据类型的信息,即在调用 execute()print()count()collect() 之前。

高频 Issues

注册子类型:如果函数仅描述了超类型,但在执行过程中实际使用了这些超类型的子类型,则让 Flink 知道这些子类型会大大提高性能。因此,对每个子类型在 StreamExecutionEnvironment 上调用 .registerType(clazz) 是有意义的。

注册自定义序列化器:对自己无法处理的类型,Flink 会使用 Kryo。并非所有类型都能被 Kryo 处理,其解决方案是为导致问题的类型注册额外的序列化器,即在 StreamExecutionEnvironment 上调用 .getConfig().addDefaultKryoSerializer(clazz, serializer)

添加类型提示:在 Java API 中,有时当 Flink 无法推断出泛型时,用户必须传递类型提示。

手动创建类型信息:因为 Java 会擦除泛型信息,导致 Flink 无法推断数据类型。因此,对于某些 API 调用,手动创建类型信息可能是必要的。

Flink 的类型信息类

TypeInformation 类是所有类型描述类的基类,它定义了一些基础属性,可以生成序列化程序以及比较器等。

TypeInformation 源码位置:flink-core/src/main/java/org/apache/flink/api/common/typeinfo/TypeInformation.java

在内部,Flink 对类型进行了以下区分:

  • 基础类型:包括所有的 Java 原生类型及其 boxed form,另外加上 voidStringDateBigDecimalBigInteger
  • 原生类型数组及对象数组
  • 符合类型
    • Flink 的 Java Tuples:包括 1 - 25 个字段的元组
    • scala cases classes
    • Row:包含任意个字段的元组,且支持 NULL值
    • POJOs:满足 bean-like 模式的类
  • 辅助类型:OptionsEitherListsMaps ……
  • 其他类型:Flink 通过 Kryo 支持其他类型的序列化

POJOs 支持创建复杂类型,在使用中透明、且能够被 Flink 高效处理,当前比较受欢迎。

POJOs 类型规则

详见:Flink|《Flink 官方文档 - 实践练习 - DataStream API 简介》学习笔记

创建 TypeInformation 或 TypeSerializer

因为 Java 会擦除泛型信息,所以需要在构造 TypeInformation 时添加类型信息。

对于非泛型,直接添加:

TypeInformation<String> info = TypeInformation.of(String.class);

对于泛型,则需要通过 TypeHint 添加:

TypeInformation<Tuple2<String, Double>> info = TypeInformation.of(new TypeHint<Tuple2<String, Double>>(){});

在 Flink 内部,将会创建一个捕获了泛型信息的泛型子类,该子类会将类型信息存储到运行时。

要创建 TypeSerializer,直接调用 TypeInformationtypeInfo.createSerializer(config) 即可。其中的 config 参数是包含注册的序列化器的 ExecutionConfig 类型。可以通过调用 DataStreamgetExecutionConfig() 方法获取它。

在函数内部,可以通过在 rich function 中使用如下方法获取它:

getRuntimeContext().getExecutionConfig()

Java API 的类型信息

Flink 除了通过反射重建了部分类型信息,还有根据输入类型判断是返回值类型的逻辑。但是,在部分场景下,Flink 无法重建类型信息,需要使用 type hints

样例:Flink 无法重建类型信息的样例

public class AppendOne<T> implements MapFunction<T, Tuple2<T, Long>> {

    public Tuple2<T, Long> map(T value) {
        return new Tuple2<T, Long>(value, 1L);
    }
}
Java API 中的 type hints

在 Flink 无法重建被擦除的泛型信息时,需要使用 Java API 的 type hints 来告诉 Flink 数据流的类型。

样例:使用 type hints 的样例

DataStream<SomeType> result = stream
    .map(new MyGenericNonInferrableFunction<Long, SomeType>())
        .returns(SomeType.class);

returns 方法中,可以接受如下类型的参数:

  • classes,例如 returns(SomeType.class),适用于无泛型的场景
  • TypeHints,例如 returns(new TypeHint>(){}),适用于有泛型的场景;TypeHints 可以捕获其泛型并将其保存到运行时。
Java 8 的 lambda 表达式的类型提取

Java 8 的 lambda 表达式的类型提取方法与非 lambda 表达式不同,因为 lambda 表达式并没有由继承自接口的类直接构成。

同样的,Flink 尝试找出执行了 lambda 表达式的类,并通过 Java 泛型标签确定参数类型和返回值类型。然而,并不是所有编译器都为 lambda 表达式生成了这种标签。

序列化 POJO 类型

POJO 类型的 TypeInfo 创建了每一个字段的序列化器。其中的标准类型使用 Flink 内置的序列化器;对于其他类型, 则尝试使用 Kryo 序列化;如果 Kryo 无法处理这些类型, 可以指定使用 Avro 序列化。

样例:强制使用 Avro 序列化

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.getConfig().enableForceAvro();

样例:强制使用 Kryo 序列化

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.getConfig().enableForceKryo();

样例:添加自定义的 Kryo 序列化器

env.getConfig().addDefaultKryoSerializer(Class<?> type, Class<? extends Serializer<?>> serializerClass);

关闭使用 Kryo 的序列化器

可以试用如下方法关闭 Kryo 的序列化器:

env.getConfig().disableGenericTypes();

使用场景:有时,我们可能希望保证所有类型都能被 Flink 自己的序列化器或用户自定义的序列化器高效地序列化,并避免退化到 Kryo 序列化器。

使用 Factory 定义 Type Information

类型信息工厂(type information factory)允许用户在 Flink 类型系统中插入用户自定义的 Type Information。

首先,需要实现 org.apache.flink.api.common.typeinfo.TypeInfoFactory 来返回自定义的 type information。而后,如果在相应的类型上注释了 @org.apache.flink.api.common.typeinfo.TypeInfo,则会在类型提取阶段调用该工厂。

类型信息工厂可以在 Java API 和 Scala API 中使用。

工厂的优先级高于 Flink 的内置类型。

样例:声明用户类型 MyTuple 并使用 factory 提供了自定义类型信息

@TypeInfo(MyTupleTypeInfoFactory.class)
public class MyTuple<T0, T1> {
  public T0 myfield0;
  public T1 myfield1;
}
public class MyTupleTypeInfoFactory extends TypeInfoFactory<MyTuple> {

  @Override
  public TypeInformation<MyTuple> createTypeInfo(Type t, Map<String, TypeInformation<?>> genericParameters) {
    return new MyTupleTypeInfo(genericParameters.get("T0"), genericParameters.get("T1"));
  }
}

方法 createTypeInfo(Type, Map>) 为工厂的目标类型提供了 type information。其参数提供了额外的类型信息,同时也提供了类型的泛型类型参数(generic type parameters)。

如果类型中包含从 Flink 函数的输入类型中派生的泛型参数,那么需要确保还实现了 org.apache.flink.api.common.typeinfo.TypeInformation#getGenericParameters,以实现泛型参数和类型信息的双向映射。

你可能感兴趣的:(Flink,flink,数据类型,序列化,泛型)