66、Flink 的 DataStream Connectors 支持的 Formats 详解

支持的 Formats
1.概述

Format 定义如何对 Record 进行编码以进行存储,目前支持以下格式:

复制代码
Avro
Azure Table
Hadoop
Parquet
Text files
2.Avro format

Flink 内置支持 Apache Avro 格式,Flink 的序列化框架可以处理基于 Avro schemas 生成的类,为了能够使用 Avro format,需要添加如下依赖到项目中。

复制代码
<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-avro</artifactId>
  <version>1.19.0</version>
</dependency>

如果读取 Avro 文件数据,必须指定 AvroInputFormat

示例

复制代码
AvroInputFormat<User> users = new AvroInputFormat<User>(in, User.class);
DataStream<User> usersDS = env.createInput(users);

注意User 是一个通过 Avro schema 生成的 POJO 类,Flink 允许选择 POJO 中字符串类型的键。

复制代码
usersDS.keyBy("name");

注意

  • 在 Flink 中可以使用 GenericData.Record 类型,但是不推荐使用;因为该类型的记录中包含了完整的 schema,导致数据非常密集,使用起来可能很慢;
  • Flink 的 POJO 字段选择也适用于从 Avro schema 生成的 POJO 类;但只有将字段类型正确写入生成的类时,才能使用;
  • 如果字段是 Object 类型,则不能将该字段用作 join 键或 grouping 键;
  • 在 Avro 中如 {"name": "type_double_test", "type": "double"}, 指定字段是可行的,但是如 ({"name": "type_double_test", "type": ["double"]},) 指定包含一个字段的复合类型就会生成 Object 类型的字段;
  • ({"name": "type_double_test", "type": ["null", "double"]},) 指定 nullable 类型字段也是可能产生 Object 类型的。
3.CSV format
a)概述

如果要使用 CSV 格式,需要在项目中添加如下依赖:

复制代码
<dependency>
	<groupId>org.apache.flink</groupId>
	<artifactId>flink-csv</artifactId>
	<version>1.19.0</version>
</dependency>

Flink 支持使用 CsvReaderFormat 读取CSV文件,读取器利用 Jackson 库,并允许传递 CSV schema 和解析配置。

CsvReaderFormat 初始化和使用示例如下

复制代码
CsvReaderFormat<SomePojo> csvFormat = CsvReaderFormat.forPojo(SomePojo.class);
FileSource<SomePojo> source = 
        FileSource.forRecordStreamFormat(csvFormat, Path.fromLocalFile(...)).build();

此时 CSV 解析的模式是使用 Jackson 库基于 SomePojo 类的字段自动派生的。

注意 :需要将 @JsonPropertyOrder({field1,field2,...})注解添加到类定义中,使字段顺序与 CSV 文件列的字段顺序完全匹配。

b)高级配置

如果需要对 CSV schema 或解析配置进行更细粒度的控制,可以使用 CsvReaderFormat 的更低级的 forSchema 静态工厂方法:

复制代码
CsvReaderFormat<T> forSchema(Supplier<CsvMapper> mapperFactory, 
                             Function<CsvMapper, CsvSchema> schemaGenerator, 
                             TypeInformation<T> typeInformation) 

以下是使用自定义列分隔符读取 POJO 的示例:

复制代码
//必须匹配 CSV 文件中列的顺序
@JsonPropertyOrder({"city","lat","lng","country","iso2","adminName","capital","population"})
public static class CityPojo {
    public String city;
    public BigDecimal lat;
    public BigDecimal lng;
    public String country;
    public String iso2;
    public String adminName;
    public String capital;
    public long population;
}

Function<CsvMapper, CsvSchema> schemaGenerator = mapper ->
        mapper.schemaFor(CityPojo.class).withoutQuoteChar().withColumnSeparator('|');

CsvReaderFormat<CityPojo> csvFormat =
        CsvReaderFormat.forSchema(() -> new CsvMapper(), schemaGenerator, TypeInformation.of(CityPojo.class));

FileSource<CityPojo> source =
        FileSource.forRecordStreamFormat(csvFormat, Path.fromLocalFile(...)).build();

相应的 CSV 文件:

复制代码
Berlin|52.5167|13.3833|Germany|DE|Berlin|primary|3644826
San Francisco|37.7562|-122.443|United States|US|California||3592294
Beijing|39.905|116.3914|China|CN|Beijing|primary|19433000

还可以使用细粒度的 Jackson 设置读取更复杂的数据类型:

复制代码
public static class ComplexPojo {
    private long id;
    private int[] array;
}

CsvReaderFormat<ComplexPojo> csvFormat =
        CsvReaderFormat.forSchema(
                CsvSchema.builder()
                        .addColumn(
                                new CsvSchema.Column(0, "id", CsvSchema.ColumnType.NUMBER))
                        .addColumn(
                                new CsvSchema.Column(4, "array", CsvSchema.ColumnType.ARRAY)
                                        .withArrayElementSeparator("#"))
                        .build(),
                TypeInformation.of(ComplexPojo.class));

相应的 CSV 文件:

复制代码
0,1#2#3
1,
2,1

与 TextLineInputFormat 类似,CsvReaderFormat 既可以用于流处理模式,也可以用于批处理模式。

4.Hadoop formats
a)项目配置

将以下依赖添加到 pom.xml 中使用 hadoop:

复制代码
<dependency>
	<groupId>org.apache.flink</groupId>
	<artifactId>flink-hadoop-compatibility_2.12</artifactId>
	<version>1.19.0</version>
</dependency>

如果想在本地运行 Flink 应用,需要将 hadoop-client 依赖也添加到 pom.xml

复制代码
<dependency>
    <groupId>org.apache.hadoop</groupId>
    <artifactId>hadoop-client</artifactId>
    <version>2.10.2</version>
    <scope>provided</scope>
</dependency>
b)使用 Hadoop InputFormats

在 Flink 中使用 Hadoop InputFormats,首先必须使用 HadoopInputs 工具类的 readHadoopFilecreateHadoopInput 包装 Input Format;前者用于从 FileInputFormat 派生的 Input Format,而后者用于通用的 Input Format。

生成的 InputFormat 可通过使用 ExecutionEnvironment#createInput 创建数据源。

生成的 DataStream 包含 2 元组,其中第一个字段是 key,第二个字段是从 Hadoop InputFormat 接收的值。

示例 :如何使用 Hadoop 的 TextInputFormat

复制代码
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
 
KeyValueTextInputFormat textInputFormat = new KeyValueTextInputFormat();

DataStream<Tuple2<Text, Text>> input = env.createInput(
    HadoopInputs.readHadoopFile(textInputFormat, Text.class, Text.class, textPath)
);
5.JSON
a)概述

需要添加如下依赖:

复制代码
<dependency>
	<groupId>org.apache.flink</groupId>
	<artifactId>flink-json</artifactId>
	<version>1.19.0</version>
	<scope>provided</scope>
</dependency>

Flink 支持通过 JsonSerializationSchema/JsonDeserializationSchema 读取/写入 JSON 记录,它们使用 Jackson 库,并支持 Jackson 支持的任何类型,包括但不限于 POJO 和 ObjectNode。

JsonDeserializationSchema 可以与任何支持该 Deserialization Schema 的连接器一起使用。

示例:将其与 KafkaSource 一起使用以反序列化 POJO:

复制代码
JsonDeserializationSchema<SomePojo> jsonFormat = new JsonDeserializationSchema<>(SomePojo.class);
KafkaSource<SomePojo> source =
    KafkaSource.<SomePojo>builder()
        .setValueOnlyDeserializer(jsonFormat)
        ...

JsonSerializationSchema 可以与任何支持 SerializationSchema 的连接器一起使用。

示例:将其与 KafkaSink 一起使用来序列化POJO:

复制代码
JsonSerializationSchema<SomePojo> jsonFormat = new JsonSerializationSchema<>();
KafkaSink<SomePojo> source =
    KafkaSink.<SomePojo>builder()
        .setRecordSerializer(
            new KafkaRecordSerializationSchemaBuilder<>()
                .setValueSerializationSchema(jsonFormat)
                ...
b)Custom Mapper

这两个架构都有接受 SerializableSupplier<ObjectMapper> 的构造函数,充当对象映射器的工厂。

使用此工厂,可以完全控制创建的映射器,并可以启用/禁用各种 Jackson 功能或注册模块,以扩展一组受支持的类型或添加其他功能。

复制代码
JsonSerializationSchema<SomeClass> jsonFormat = new JsonSerializationSchema<>(
    () -> new ObjectMapper()
        .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS))
        .registerModule(new ParameterNamesModule());
6.Parquet format
a)概述

Flink 支持读取 Parquet 文件并生成 Flink RowData 和 Avro 记录,使用 Parquet format,需要将 flink-parquet 依赖添加到项目中:

复制代码
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-parquet</artifactId>
    <version>1.19.0</version>
</dependency>

要使用 Avro 格式,需要将 parquet-avro 依赖添加到项目中:

复制代码
<dependency>
    <groupId>org.apache.parquet</groupId>
    <artifactId>parquet-avro</artifactId>
    <version>1.12.2</version>
    <optional>true</optional>
    <exclusions>
        <exclusion>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-client</artifactId>
        </exclusion>
        <exclusion>
            <groupId>it.unimi.dsi</groupId>
            <artifactId>fastutil</artifactId>
        </exclusion>
    </exclusions>
</dependency>

此格式与新的 Source 兼容,可以同时在批和流模式下使用。

  • 有界数据:列出所有文件并全部读取;
  • 无界数据:监控目录中出现的新文件。

当开启一个 File Source,会被默认为有界读取;如果想在连续读取模式下使用 File Source,必须额外调用 AbstractFileSource.AbstractFileSourceBuilder.monitorContinuously(Duration)

Vectorized reader

复制代码
// Parquet rows are decoded in batches
FileSource.forBulkFileFormat(BulkFormat,Path...)

// Monitor the Paths to read data as unbounded data
FileSource.forBulkFileFormat(BulkFormat,Path...)
.monitorContinuously(Duration.ofMillis(5L))
.build();

Avro Parquet reader

复制代码
// Parquet rows are decoded in batches
FileSource.forRecordStreamFormat(StreamFormat,Path...)

// Monitor the Paths to read data as unbounded data
FileSource.forRecordStreamFormat(StreamFormat,Path...)
        .monitorContinuously(Duration.ofMillis(5L))
        .build();

下面的案例都是基于有界数据的。 如果想在连续读取模式下使用 File Source,必须额外调用 AbstractFileSource.AbstractFileSourceBuilder.monitorContinuously(Duration)

在此示例中,将创建由 Parquet 格式的记录构成的 Flink RowDatas DataStream。

把 schema 信息映射为只读字段("f7"、"f4" 和 "f99"),每个批次读取 500 条记录;

  • 第一个布尔类型的参数用来指定是否需要将时间戳列处理为 UTC。

  • 第二个布尔类型参数用来指定在进行 Parquet 字段映射时,是否要区分大小写。

  • 这里不需要水印策略,因为记录中不包含事件时间戳。

    final LogicalType[] fieldTypes =
    new LogicalType[] {
    new DoubleType(), new IntType(), new VarCharType()};
    final RowType rowType = RowType.of(fieldTypes, new String[] {"f7", "f4", "f99"});

    final ParquetColumnarRowInputFormat<FileSourceSplit> format =
    new ParquetColumnarRowInputFormat<>(
    new Configuration(),
    rowType,
    InternalTypeInfo.of(rowType),
    500,
    false,
    true);
    final FileSource<RowData> source =
    FileSource.forBulkFileFormat(format, /* Flink Path */)
    .build();
    final DataStream<RowData> stream =
    env.fromSource(source, WatermarkStrategy.noWatermarks(), "file-source");

c)Avro Records

Flink 支持三种方式来读取 Parquet 文件并创建 Avro records (PyFlink 只支持 Generic record)

  • Generic record
  • Specific record
  • Reflect record

Generic record

使用 JSON 定义 Avro schemas,此示例使用了一个在 official Avro tutorial 中描述的示例相似的 Avro schema:

复制代码
{"namespace": "example.avro",
  "type": "record",
  "name": "User",
  "fields": [
    {"name": "name", "type": "string"},
    {"name": "favoriteNumber",  "type": ["int", "null"]},
    {"name": "favoriteColor", "type": ["string", "null"]}
  ]
}

这个 schema 定义了一个具有三个属性的的 user 记录:name,favoriteNumber 和 favoriteColor。

在此示例中,将创建包含由 Avro Generic records 格式构成的 Parquet records 的 DataStream,Flink 会基于 JSON 字符串解析 Avro schema;也有很多其他的方式解析 schema,例如基于 java.io.File 或 java.io.InputStream。

然后可以通过 AvroParquetReaders 为 Avro Generic 记录创建 AvroParquetRecordFormat

复制代码
// 解析 avro schema
final Schema schema =
        new Schema.Parser()
        .parse(
        "{\"type\": \"record\", "
        + "\"name\": \"User\", "
        + "\"fields\": [\n"
        + "        {\"name\": \"name\", \"type\": \"string\" },\n"
        + "        {\"name\": \"favoriteNumber\",  \"type\": [\"int\", \"null\"] },\n"
        + "        {\"name\": \"favoriteColor\", \"type\": [\"string\", \"null\"] }\n"
        + "    ]\n"
        + "    }");

final FileSource<GenericRecord> source =
        FileSource.forRecordStreamFormat(
        AvroParquetReaders.forGenericRecord(schema), /* Flink Path */)
        .build();

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.enableCheckpointing(10L);

final DataStream<GenericRecord> stream =
        env.fromSource(source, WatermarkStrategy.noWatermarks(), "file-source");

Specific record

基于之前定义的 schema,可以通过利用 Avro 代码生成来生成类;一旦生成了类,就不需要在程序中直接使用 schema。

可以使用 avro-tools.jar 手动生成代码,也可以直接使用 Avro Maven 插件对配置的源目录中的任何 .avsc 文件执行代码生成。

此示例使用了样例 schema testdata.avsc :

复制代码
[
  {"namespace": "org.apache.flink.formats.parquet.generated",
    "type": "record",
    "name": "Address",
    "fields": [
      {"name": "num", "type": "int"},
      {"name": "street", "type": "string"},
      {"name": "city", "type": "string"},
      {"name": "state", "type": "string"},
      {"name": "zip", "type": "string"}
    ]
  }
]

可以使用 Avro Maven plugin 生成 Address Java 类。

复制代码
@org.apache.avro.specific.AvroGenerated
public class Address extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord {
    // 生成的代码...
}

可以通过 AvroParquetReaders 为 Avro Specific 记录创建 AvroParquetRecordFormat, 然后创建一个包含由 Avro Specific records 格式构成的 Parquet records 的 DateStream。

复制代码
final FileSource<GenericRecord> source =
        FileSource.forRecordStreamFormat(
                AvroParquetReaders.forSpecificRecord(Address.class), /* Flink Path */)
        .build();

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.enableCheckpointing(10L);
        
final DataStream<GenericRecord> stream =
        env.fromSource(source, WatermarkStrategy.noWatermarks(), "file-source");

Reflect record

除了需要预定义 Avro Generic 和 Specific 记录, Flink 还支持基于现有 Java POJO 类从 Parquet 文件创建 DateStream。

在这种场景中,Avro 会使用 Java 反射为这些 POJO 类生成 schema 和协议。

本例使用了一个简单的 Java POJO 类 Datum :

复制代码
public class Datum implements Serializable {

    public String a;
    public int b;

    public Datum() {}

    public Datum(String a, int b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        Datum datum = (Datum) o;
        return b == datum.b && (a != null ? a.equals(datum.a) : datum.a == null);
    }

    @Override
    public int hashCode() {
        int result = a != null ? a.hashCode() : 0;
        result = 31 * result + b;
        return result;
    }
}

可以通过 AvroParquetReaders 为 Avro Reflect 记录创建一个 AvroParquetRecordFormat, 然后创建一个包含由 Avro Reflect records 格式构成的 Parquet records 的 DateStream。

复制代码
final FileSource<GenericRecord> source =
        FileSource.forRecordStreamFormat(
                AvroParquetReaders.forReflectRecord(Datum.class), /* Flink Path */)
        .build();

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.enableCheckpointing(10L);
        
final DataStream<GenericRecord> stream =
        env.fromSource(source, WatermarkStrategy.noWatermarks(), "file-source");
d)使用 Parquet files 必备条件

为了支持读取 Avro Reflect 数据,Parquet 文件必须包含特定的 meta 信息。为了生成 Parquet 数据,Avro schema 信息中必须包含 namespace, 以便让程序在反射执行过程中能确定唯一的 Java Class 对象。

下面的案例展示了上文中的 User 对象的 schema 信息。但是当前案例包含了一个指定文件目录的 namespace(当前案例下的包路径),反射过程中可以找到对应的 User 类。

复制代码
// avro schema with namespace
final String schema = 
                    "{\"type\": \"record\", "
                        + "\"name\": \"User\", "
                        + "\"namespace\": \"org.apache.flink.formats.parquet.avro\", "
                        + "\"fields\": [\n"
                        + "        {\"name\": \"name\", \"type\": \"string\" },\n"
                        + "        {\"name\": \"favoriteNumber\",  \"type\": [\"int\", \"null\"] },\n"
                        + "        {\"name\": \"favoriteColor\", \"type\": [\"string\", \"null\"] }\n"
                        + "    ]\n"
                        + "    }";

由上述 scheme 信息创建的 Parquet 文件包含以下 meta 信息:

复制代码
creator:        parquet-mr version 1.12.2 (build 77e30c8093386ec52c3cfa6c34b7ef3321322c94)
extra:          parquet.avro.schema =
{"type":"record","name":"User","namespace":"org.apache.flink.formats.parquet.avro","fields":[{"name":"name","type":"string"},{"name":"favoriteNumber","type":["int","null"]},{"name":"favoriteColor","type":["string","null"]}]}
extra:          writer.model.name = avro

file schema:    org.apache.flink.formats.parquet.avro.User
--------------------------------------------------------------------------------
name:           REQUIRED BINARY L:STRING R:0 D:0
favoriteNumber: OPTIONAL INT32 R:0 D:1
favoriteColor:  OPTIONAL BINARY L:STRING R:0 D:1

row group 1:    RC:3 TS:143 OFFSET:4
--------------------------------------------------------------------------------
name:            BINARY UNCOMPRESSED DO:0 FPO:4 SZ:47/47/1.00 VC:3 ENC:PLAIN,BIT_PACKED ST:[min: Jack, max: Tom, num_nulls: 0]
favoriteNumber:  INT32 UNCOMPRESSED DO:0 FPO:51 SZ:41/41/1.00 VC:3 ENC:RLE,PLAIN,BIT_PACKED ST:[min: 1, max: 3, num_nulls: 0]
favoriteColor:   BINARY UNCOMPRESSED DO:0 FPO:92 SZ:55/55/1.00 VC:3 ENC:RLE,PLAIN,BIT_PACKED ST:[min: green, max: yellow, num_nulls: 0]

使用包 org.apache.flink.formats.parquet.avro 路径下已定义的 User 类:

复制代码
public class User {
    private String name;
    private Integer favoriteNumber;
    private String favoriteColor;

    public User() {}

    public User(String name, Integer favoriteNumber, String favoriteColor) {
        this.name = name;
        this.favoriteNumber = favoriteNumber;
        this.favoriteColor = favoriteColor;
    }

    public String getName() {
        return name;
    }

    public Integer getFavoriteNumber() {
        return favoriteNumber;
    }

    public String getFavoriteColor() {
        return favoriteColor;
    }
}

可以通过下面的程序读取类型为 User 的 Avro Reflect records:

复制代码
final FileSource<GenericRecord> source =
        FileSource.forRecordStreamFormat(
        AvroParquetReaders.forReflectRecord(User.class), /* Flink Path */)
        .build();
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.enableCheckpointing(10L);

final DataStream<GenericRecord> stream =
        env.fromSource(source, WatermarkStrategy.noWatermarks(), "file-source");
7.Text files format

Flink 支持使用 TextLineInputFormat 从文件中读取文本行,此 format 使用 Java 的内置 InputStreamReader 以支持的字符集编码来解码字节流。 要使用该 format,需要将 Flink Connector Files 依赖项添加到项目中:

复制代码
<dependency>
	<groupId>org.apache.flink</groupId>
	<artifactId>flink-connector-files</artifactId>
	<version>1.19.0</version>
</dependency>

此 format 与新 Source 兼容,可以在批处理和流模式下使用;可以通过两种方式使用此 format:

  • 批处理模式的有界读取
  • 流模式的连续读取:监视目录中出现的新文件

有界读取示例

在此示例中,创建了一个 DataStream,其中包含作为字符串的文本文件的行。 此处不需要水印策略,因为记录不包含事件时间戳。

复制代码
final FileSource<String> source =
  FileSource.forRecordStreamFormat(new TextLineInputFormat(), /* Flink Path */)
  .build();
final DataStream<String> stream =
  env.fromSource(source, WatermarkStrategy.noWatermarks(), "file-source");

连续读取示例:在此示例中,创建了一个 DataStream,随着新文件被添加到目录中,其中包含的文本文件行的字符串流将无限增长。每秒会进行新文件监控。 此处不需要水印策略,因为记录不包含事件时间戳。

复制代码
final FileSource<String> source =
    FileSource.forRecordStreamFormat(new TextLineInputFormat(), /* Flink Path */)
  .monitorContinuously(Duration.ofSeconds(1L))
  .build();
final DataStream<String> stream =
  env.fromSource(source, WatermarkStrategy.noWatermarks(), "file-source");
相关推荐
焦糖玛奇朵婷19 小时前
实测扭蛋机小程序:开发简单,互动有趣
java·大数据·程序人生·小程序·软件需求
瓦中空花20 小时前
大数据工具-Flink
大数据·flink
Lab_AI20 小时前
iLabPower LES与SDH科学数据基因组平台赋能光电材料研发与生产,鼎材科技与创腾科技进一步深化合作
大数据·人工智能·oled·材料设计·光电材料研发·材料创新·材料研发
渣渣盟20 小时前
Flink实现TopN URL访问量统计
大数据·flink·scala
无你想你20 小时前
Datawhale之春晚机器人跳舞复刻
大数据·elasticsearch·机器人
添柴少年yyds20 小时前
Flink的Checkpoint原理和流程
flink
wAIxiSeu20 小时前
万字长文解析Apache Paimon
大数据
网络工程小王20 小时前
【大数据技术详解】——HIVE技术(学习笔记)
大数据·hive·hadoop
刘一说20 小时前
Git 工具知识全景图:从核心概念到高效协作实践
大数据·git·elasticsearch
MarsLord20 小时前
ElasticSearch快速入门实战(1)-索引、别名、建模最佳实践
大数据·elasticsearch·搜索引擎