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");
相关推荐
Natural_yz4 小时前
大数据学习09之Hive基础
大数据·hive·学习
Natural_yz4 小时前
大数据学习10之Hive高级
大数据·hive·学习
AI服务老曹4 小时前
建立更及时、更有效的安全生产优化提升策略的智慧油站开源了
大数据·人工智能·物联网·开源·音视频
Mephisto.java5 小时前
【大数据学习 | HBASE高级】storeFile文件的合并
大数据·sql·oracle·json·hbase·database
这样の我5 小时前
hbase集成phoenix
大数据·数据库·hbase
思通数据5 小时前
AI与OCR:数字档案馆图像扫描与文字识别技术实现与项目案例
大数据·人工智能·目标检测·计算机视觉·自然语言处理·数据挖掘·ocr
CoderJia程序员甲5 小时前
重学SpringBoot3-整合 Elasticsearch 8.x (三)使用Repository
java·大数据·spring boot·elasticsearch
东方巴黎~Sunsiny5 小时前
如何优化Elasticsearch的查询性能?
大数据·elasticsearch·搜索引擎
杭州的物联网专家6 小时前
涉密载体管控系统:DW-S402全方位守护信息安全的利器
大数据
guanpinkeji6 小时前
搭子小程序定制开发:全新找搭子之旅
大数据·小程序·小程序开发·小程序制作·找搭子·搭子小程序