这玩意,让我丢了一次面,搞它。
文章目录
我的地盘我做主
搞它的思路是先看官网介绍,当时看了官网一遍,还是忘记了,今天是未来日子当中最好的一天,已不再少年,请不要嫌弃我废话多,我的地盘我做主。
继续说思路,官网——源码——优秀博客——实践。
官网重在看官方介绍的大纲,源码在于学习精髓(卷啊),优秀博客(站在巨人肩膀),实践(肌肉记忆,脑子毕竟废了)。
官网介绍
官网开篇就介绍这个连接器简直不得了,大致意思是:
该连接器为批和流处理提供了统一的Source和Sink,用于向Flink FileSystem抽象所支持的文件系统读写(分区)文件。这个文件系统连接器为BATCH和stream提供了相同的保证,并且被设计为为stream执行提供精确一次的语义。
连接器支持从任何(分布式)文件系统(如POSIX, S3, HDFS)以某种格式(如Avro, CSV, Parquet)读写一组文件,并生成流或记录。
File Source
File Source基于Source API,这是一个读取文件的统一数据源——以批处理和流模式。它分为以下两个部分:SplitEnumerator和SourceReader。
- SplitEnumerator负责发现和标识要读取的文件,并将它们分配给SourceReader。
- SourceReader请求它需要处理的文件,并从文件系统读取文件。
您需要将File Source与一种格式(format)结合起来,该格式允许您解析CSV、解码AVRO或读取Parquet柱状(一般列式存储)文件。
有界和无界流
有界文件源列出所有文件(通过SplitEnumerator—一个带有过滤掉的隐藏文件的递归目录列表)并读取所有文件。
在为定期文件发现配置枚举器时,将创建一个无界文件源。在这种情况下,SplitEnumerator将像有界情况一样进行枚举,但在一定的间隔后重复枚举。对于任何重复的枚举,SplitEnumerator会过滤掉以前检测到的文件,只向SourceReader发送新文件。
用法
你可以通过以下API调用之一开始构建文件源:
// reads the contents of a file from a file stream.
FileSource.forRecordStreamFormat(StreamFormat,Path...);
// reads batches of records from a file at a time
FileSource.forBulkFileFormat(BulkFormat,Path...);
这将创建一个文件源。FileSourceBuilder,您可以在其上配置文件源的所有属性。
对于有界/批处理情况,文件源处理给定路径下的所有文件。对于连续/流的情况,源会定期检查新文件的路径,并开始读取这些文件。
当您开始创建文件源(通过FileSource。通过上述方法之一创建的FileSourceBuilder),源在默认情况下是有界/批处理模式。您可以调用abstractfilesource . abstractfilesourcebuilder . monitorcontinuous (Duration)将源放入连续流模式。
final FileSource<String> source =
FileSource.forRecordStreamFormat(...)
.monitorContinuously(Duration.ofMillis(5))
.build();
文件格式类型
通过文件格式定义的文件读取器读取每个文件。这些定义了文件内容的解析逻辑。源代码支持多个类。接口是实现的简单性和灵活性/效率之间的权衡。
- StreamFormat从文件流读取文件的内容。它是实现起来最简单的格式,并且提供了许多开箱即用的特性(比如检查点逻辑),但是它可以应用的优化(比如对象重用、批处理等)受到限制。
- BulkFormat一次从文件中读取一批记录。它是要实现的最“低级”的格式,但为优化实现提供了最大的灵活性。
TextLine Format
StreamFormat阅读器格式化文件中的文本行。阅读器使用Java内置的InputStreamReader使用各种支持的字符集编码解码字节流。这种格式不支持从检查点进行优化恢复。在恢复时,它将重新读取并丢弃在最后一个检查点之前处理的行数。这是由于文件中的行偏移量不能通过具有流输入和字符集解码器状态的内部缓冲的字符集解码器跟踪。
SimpleStreamFormat抽象类
这是StreamFormat的一个简单版本,用于不可分割的格式。数组或文件的自定义读取可以通过实现SimpleStreamFormat实现:
private static final class ArrayReaderFormat extends SimpleStreamFormat<byte[]> {
private static final long serialVersionUID = 1L;
@Override
public Reader<byte[]> createReader(Configuration config, FSDataInputStream stream)
throws IOException {
return new ArrayReader(stream);
}
@Override
public TypeInformation<byte[]> getProducedType() {
return PrimitiveArrayTypeInfo.BYTE_PRIMITIVE_ARRAY_TYPE_INFO;
}
}
final FileSource<byte[]> source =
FileSource.forRecordStreamFormat(new ArrayReaderFormat(), path).build();
SimpleStreamFormat的一个例子是CsvReaderFormat。它可以像这样初始化:
CsvReaderFormat<SomePojo> csvFormat = CsvReaderFormat.forPojo(SomePojo.class);
FileSource<SomePojo> source =
FileSource.forRecordStreamFormat(csvFormat, Path.fromLocalFile(...)).build();
在本例中,CSV解析的模式是使用Jackson库基于SomePojo类的字段自动派生的。(注意:您可能需要向类定义中添加@JsonPropertyOrder({field1, field2,…})注释,其中字段的顺序与CSV文件列的顺序完全匹配)。
如果你需要对CSV模式或解析选项进行更细粒度的控制,可以使用CsvReaderFormat的更低级的forSchema静态工厂方法:
CsvReaderFormat<T> forSchema(CsvMapper mapper,
CsvSchema schema,
TypeInformation<T> typeInformation)
Bulk Format
BulkFormat一次读取和解码一批记录。批量格式的例子是ORC或Parquet格式。外部的BulkFormat类主要充当阅读器的配置持有者和工厂。实际的读取是由BulkFormat完成的。Reader,在BulkFormat#createReader(Configuration, FileSourceSplit)方法中创建。如果在检查点流执行期间基于检查点创建了一个大容量读取器,那么将在BulkFormat# restoreereader (Configuration, FileSourceSplit)方法中重新创建该读取器。
SimpleStreamFormat可以通过在StreamFormatAdapter中包装它来转换为BulkFormat:
BulkFormat<SomePojo, FileSourceSplit> bulkFormat =
new StreamFormatAdapter<>(CsvReaderFormat.forPojo(SomePojo.class));
定制文件枚举
/**
* A FileEnumerator implementation for hive source, which generates splits based on
* HiveTablePartition.
*/
public class HiveSourceFileEnumerator implements FileEnumerator {
// reference constructor
public HiveSourceFileEnumerator(...) {
...
}
/***
* Generates all file splits for the relevant files under the given paths. The {@code
* minDesiredSplits} is an optional hint indicating how many splits would be necessary to
* exploit parallelism properly.
*/
@Override
public Collection<FileSourceSplit> enumerateSplits(Path[] paths, int minDesiredSplits)
throws IOException {
// createInputSplits:splitting files into fragmented collections
return new ArrayList<>(createInputSplits(...));
}
...
/***
* A factory to create HiveSourceFileEnumerator.
*/
public static class Provider implements FileEnumerator.Provider {
...
@Override
public FileEnumerator create() {
return new HiveSourceFileEnumerator(...);
}
}
}
// use the customizing file enumeration
new HiveSource<>(
...,
new HiveSourceFileEnumerator.Provider(
partitions != null ? partitions : Collections.emptyList(),
new JobConfWrapper(jobConf)),
...);
当前的限制
对于大量的文件积压,水印并不能很好地工作。这是因为水印会在文件中快速前进,下一个文件可能包含比水印晚的数据。
对于无界文件源,枚举器当前会记住所有已经处理过的文件的路径,在某些情况下,这种状态可能会变得相当大。我们计划在未来添加一种压缩形式来跟踪已经处理过的文件(例如,通过将修改时间戳保持在边界以下)。
幕后
如果您对File Source如何通过新的数据源API设计工作感兴趣,那么您可能需要阅读这一部分作为参考。有关新的数据源API的详细信息,请参阅关于数据源和FLIP-27的文档,以获得更多描述性的讨论。
File Sink
文件接收器将传入的数据写入桶中。考虑到传入的流可以是无界的,每个桶中的数据被组织成有限大小的部分文件。桶行为是完全可配置的,使用默认的基于时间的桶,我们每小时开始写一个新桶。这意味着每个生成的桶将包含从流中间隔1小时接收到的记录的文件。
桶目录中的数据被分割为部分文件。每个桶将包含接收到该桶数据的接收器的每个子任务的至少一个部分文件。附加的零件文件将根据可配置的滚动策略被创建。对于行编码格式(请参阅文件格式),默认策略根据大小、指定文件可打开的最长时间的超时和关闭文件的最长不活动超时来滚动部分文件。对于Bulk-encoded format,我们滚动每个检查点,用户可以根据大小或时间指定附加条件。
重要提示:在流式模式下使用FileSink时需要启用检查点。零件文件只能在成功的检查点上完成。如果检查点被禁用,部件文件将永远处于正在进行或挂起状态,下游系统无法安全地读取。
Format Types
FileSink既支持行编码格式,也支持批量编码格式,例如Apache Parquet。这两个变量自带它们各自的构建器,可以用以下静态方法创建:
- Row-encoded sink: FileSink.forRowFormat(basePath, rowEncoder)
- Bulk-encoded sink: FileSink.forBulkFormat(basePath, bulkWriterFactory)
在创建行或批量编码接收器时,我们必须指定存储桶的基本路径和数据的编码逻辑。
请查看JavaDoc for FileSink以获得所有配置选项和关于不同数据格式实现的更多文档。
- Row-encoded Formats
批量编码的接收器的创建与行编码的接收器类似,但我们必须指定BulkWriter而不是Encoder。工厂。BulkWriter逻辑定义了如何添加和刷新新元素,以及如何为进一步的编码目的完成一批记录。
Flink自带四个内置的BulkWriter工厂:
ParquetWriterFactory
AvroWriterFactory
SequenceFileWriterFactory
CompressWriterFactory
OrcBulkWriterFactory
重要的批量格式只能有扩展CheckpointRollingPolicy的滚动策略。后者会在每个检查点滚动。策略可以根据大小或处理时间额外滚动。
Parquet format
Flink包含内置的方便方法,用于为Avro数据创建Parquet写入器工厂。这些方法及其相关文档可以在AvroParquetWriters类中找到。
为了写入其他与Parquet兼容的数据格式,用户需要使用ParquetBuilder接口的自定义实现创建ParquetWriterFactory。
要在您的应用程序中使用Parquet批量编码器,您需要添加以下依赖项:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-parquet_2.12</artifactId>
<version>1.15.2</version>
</dependency>
将Avro数据写入Parquet格式的FileSink可以像这样创建:
import org.apache.flink.connector.file.sink.FileSink;
import org.apache.flink.formats.parquet.avro.AvroParquetWriters;
import org.apache.avro.Schema;
Schema schema = ...;
DataStream<GenericRecord> input = ...;
final FileSink<GenericRecord> sink = FileSink
.forBulkFormat(outputBasePath, AvroParquetWriters.forGenericRecord(schema))
.build();
input.sinkTo(sink);
类似地,一个将Protobuf数据写入Parquet格式的FileSink可以像这样创建:
import org.apache.flink.connector.file.sink.FileSink;
import org.apache.flink.formats.parquet.protobuf.ParquetProtoWriters;
// ProtoRecord is a generated protobuf Message class.
DataStream<ProtoRecord> input = ...;
final FileSink<ProtoRecord> sink = FileSink
.forBulkFormat(outputBasePath, ParquetProtoWriters.forType(ProtoRecord.class))
.build();
input.sinkTo(sink);
Avro format
Flink还提供了将数据写入Avro文件的内置支持。在AvroWriters类中可以找到创建Avro写入器工厂及其相关文档的方便方法列表。
要在你的应用程序中使用Avro写入器,你需要添加以下依赖:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-avro</artifactId>
<version>1.15.2</version>
</dependency>
将数据写入Avro文件的FileSink可以像这样创建:
import org.apache.flink.connector.file.sink.FileSink;
import org.apache.flink.formats.avro.AvroWriters;
import org.apache.avro.Schema;
Schema schema = ...;
DataStream<GenericRecord> input = ...;
final FileSink<GenericRecord> sink = FileSink
.forBulkFormat(outputBasePath, AvroWriters.forGenericRecord(schema))
.build();
input.sinkTo(sink);
为了创建定制的Avro写入器,例如启用压缩,用户需要创建AvroWriterFactory与AvroBuilder接口的定制实现:
AvroWriterFactory<?> factory = new AvroWriterFactory<>((AvroBuilder<Address>) out -> {
Schema schema = ReflectData.get().getSchema(Address.class);
DatumWriter<Address> datumWriter = new ReflectDatumWriter<>(schema);
DataFileWriter<Address> dataFileWriter = new DataFileWriter<>(datumWriter);
dataFileWriter.setCodec(CodecFactory.snappyCodec());
dataFileWriter.create(schema, out);
return dataFileWriter;
});
DataStream<Address> stream = ...
stream.sinkTo(FileSink.forBulkFormat(
outputBasePath,
factory).build());
ORC Format
为了使数据能够以ORC格式进行批量编码,Flink提供了OrcBulkWriterFactory,它采用Vectorizer的具体实现。
就像任何其他以批量方式编码数据的列格式一样,Flink的OrcBulkWriter是批量写入输入元素的。它使用ORC的VectorizedRowBatch来实现这一点。
由于必须将输入元素转换为VectorizedRowBatch,因此用户必须扩展抽象Vectorizer类并覆盖vectorize(T元素,VectorizedRowBatch批处理)方法。如您所见,该方法提供了一个VectorizedRowBatch的实例供用户直接使用,因此用户只需编写将输入元素转换为columnvector的逻辑,并在提供的VectorizedRowBatch实例中设置它们。
例如,如果输入元素的类型是Person,它看起来像:
class Person {
private final String name;
private final int age;
...
}
然后转换Person类型的元素并将它们设置在VectorizedRowBatch中的子实现可以如下所示:
import org.apache.hadoop.hive.ql.exec.vector.BytesColumnVector;
import org.apache.hadoop.hive.ql.exec.vector.LongColumnVector;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
public class PersonVectorizer extends Vectorizer<Person> implements Serializable {
public PersonVectorizer(String schema) {
super(schema);
}
@Override
public void vectorize(Person element, VectorizedRowBatch batch) throws IOException {
BytesColumnVector nameColVector = (BytesColumnVector) batch.cols[0];
LongColumnVector ageColVector = (LongColumnVector) batch.cols[1];
int row = batch.size++;
nameColVector.setVal(row, element.getName().getBytes(StandardCharsets.UTF_8));
ageColVector.vector[row] = element.getAge();
}
}
要在应用程序中使用ORC批量编码器,用户需要添加以下依赖项:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-orc_2.12</artifactId>
<version>1.15.2</version>
</dependency>
然后可以像这样创建一个以ORC格式写入数据的FileSink:
import org.apache.flink.connector.file.sink.FileSink;
import org.apache.flink.orc.writer.OrcBulkWriterFactory;
String schema = "struct<_col0:string,_col1:int>";
DataStream<Person> input = ...;
final OrcBulkWriterFactory<Person> writerFactory = new OrcBulkWriterFactory<>(new PersonVectorizer(schema));
final FileSink<Person> sink = FileSink
.forBulkFormat(outputBasePath, writerFactory)
.build();
input.sinkTo(sink);
OrcBulkWriterFactory还可以接受Hadoop配置和属性,以便提供定制的Hadoop配置和ORC写入器属性。
String schema = ...;
Configuration conf = ...;
Properties writerProperties = new Properties();
writerProperties.setProperty("orc.compress", "LZ4");
// Other ORC supported properties can also be set similarly.
final OrcBulkWriterFactory<Person> writerFactory = new OrcBulkWriterFactory<>(
new PersonVectorizer(schema), writerProperties, conf);
ORC编写器属性的完整列表可以在这里找到。
想要向ORC文件添加用户元数据的用户可以在覆盖的vectorize(…)方法中调用addUserMetadata(…)。
public class PersonVectorizer extends Vectorizer<Person> implements Serializable {
@Override
public void vectorize(Person element, VectorizedRowBatch batch) throws IOException {
...
String metadataKey = ...;
ByteBuffer metadataValue = ...;
this.addUserMetadata(metadataKey, metadataValue);
}
}
Hadoop SequenceFile format
要在应用程序中使用SequenceFile批量编码器,需要添加以下依赖项:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-sequence-file</artifactId>
<version>1.15.2</version>
</dependency>
示例如下:
import org.apache.flink.connector.file.sink.FileSink;
import org.apache.flink.configuration.GlobalConfiguration;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.SequenceFile;
import org.apache.hadoop.io.Text;
DataStream<Tuple2<LongWritable, Text>> input = ...;
Configuration hadoopConf = HadoopUtils.getHadoopConfiguration(GlobalConfiguration.loadConfiguration());
final FileSink<Tuple2<LongWritable, Text>> sink = FileSink
.forBulkFormat(
outputBasePath,
new SequenceFileWriterFactory<>(hadoopConf, LongWritable.class, Text.class))
.build();
input.sinkTo(sink);
SequenceFileWriterFactory支持额外的构造函数参数来指定压缩设置。
Bucket Assignment
桶逻辑定义了如何将数据结构化到基本输出目录中的子目录中。
行格式和批量格式(请参阅文件格式)都使用DateTimeBucketAssigner作为默认的赋值器。默认情况下,DateTimeBucketAssigner基于系统默认时区创建小时桶,格式如下:yyyy-MM-dd——HH。日期格式(即桶大小)和时区都可以手动配置。
我们可以通过在格式生成器上调用. withbucketassigner(赋值器)来指定一个自定义的BucketAssigner。
Flink自带两个内置的BucketAssigners:
- DateTimeBucketAssigner : Default time based assigner
- BasePathBucketAssigner : Assigner that stores all part files in the base path (single global bucket)
Rolling Policy
RollingPolicy定义了何时关闭给定的正在进行的部件文件并将其移动到挂起状态,然后再移动到已完成状态。处于“finished”状态的部件文件是可以查看的,并且保证包含有效数据,在发生故障时不会被还原。在STREAMING模式中,滚动策略与检查点间隔(挂起的文件在下一个检查点完成)相结合,控制了下游读取器使用部分文件的速度,以及这些部分的大小和数量。在BATCH模式中,部分文件在作业结束时可见,但滚动策略可以控制它们的最大大小。
Flink自带两个内置的rollingpolicy:
- DefaultRollingPolicy
- OnCheckpointRollingPolicy
Part file lifecycle
为了在下游系统中使用FileSink的输出,我们需要了解产生的输出文件的命名和生命周期。
零件文件可以处于以下三种状态之一:
- In-progress:当前正在写入的部件文件正在进行中
- Pending:关闭(由于指定的滚动策略)等待提交的正在进行的文件
- Finished:在成功的检查点(STREAMING)或在输入(BATCH)挂起的文件的末尾转换为“Finished”
只有完成的文件才能被下游系统安全地读取,因为这些文件保证以后不会被修改。
对于每个活动的桶,每个编写器子任务在任何给定的时间都有一个正在进行的部分文件,但也可以有几个挂起和完成的文件。
用户可以通过以下方式指定OutputFileConfig:
OutputFileConfig config = OutputFileConfig
.builder()
.withPartPrefix("prefix")
.withPartSuffix(".ext")
.build();
FileSink<Tuple2<Integer, Integer>> sink = FileSink
.forRowFormat((new Path(outputPath), new SimpleStringEncoder<>("UTF-8"))
.withBucketAssigner(new KeyBucketAssigner())
.withRollingPolicy(OnCheckpointRollingPolicy.build())
.withOutputFileConfig(config)
.build();
Compaction
从1.15版本开始,FileSink支持对挂起的文件进行压缩,这允许应用程序有更小的检查点间隔,而不会生成大量的小文件,特别是在使用必须滚动接受检查点的批量编码格式时。
可以启用压缩
FileSink<Integer> fileSink=
FileSink.forRowFormat(new Path(path),new SimpleStringEncoder<Integer>())
.enableCompact(
FileCompactStrategy.Builder.newBuilder()
.setSizeThreshold(1024)
.enableCompactionOnCheckpoint(5)
.build(),
new RecordWiseFileCompactor<>(
new DecoderBasedReader.Factory<>(SimpleStringDecoder::new)))
.build();
一旦启用,就会在挂起文件和提交文件之间进行压缩。挂起的文件将首先提交到路径以…开始的临时文件中。然后,这些文件将由用户指定的压缩器按照策略进行压缩,并生成新的压缩后的挂起文件。然后,这些挂起的文件将被发送到提交程序,提交程序将提交到正式文件中。之后,源文件将被删除。
启用压缩时,需要指定FileCompactStrategy和FileCompactor。
FileCompactStrategy指定压缩文件的时间和类型。目前,有两个并行条件:目标文件大小和通过的检查点数量。一旦缓存文件的总大小达到了大小阈值,或者自上次压缩以来的检查点数量达到了指定的数量,将调度缓存文件进行压缩。
FileCompactor指定如何压缩给定的Path列表并写入结果文件。根据写文件的方式可以分为两种:
- OutputStreamBasedFileCompactor:用户可以将压缩后的结果写入输出流。当用户不想或不能从输入文件读取记录时,这很有用。一个例子是ConcatFileCompactor,它直接连接文件列表。
- RecordWiseFileCompactor:压缩器可以从输入文件逐一读取记录,并像FileWriter一样写入结果文件。一个例子是RecordWiseFileCompactor,它从源文件读取记录,然后用CompactingFileWriter写入它们。用户需要指定如何从源文件读取记录。
重要提示1:一旦启用了压缩,如果您想禁用压缩,则必须在构建FileSink时显式调用disableccompact。
重要提示2:当启用压缩时,写入的文件需要等待更长的时间才能显示。
重要的注意事项
通用
重要提示1:当使用Hadoop < 2.7时,请使用OnCheckpointRollingPolicy在每个检查点上滚动部分文件。原因是,如果部分文件“遍历”检查点间隔,那么,在从故障中恢复时,FileSink可能使用文件系统的truncate()方法来丢弃正在进行的文件中未提交的数据。此方法在2.7之前的Hadoop版本中不支持,Flink会抛出异常。
重要提示2:鉴于Flink接收器和udf通常不区分正常作业终止(例如有限输入流)和由于失败而终止,在作业正常终止时,最后正在进行的文件将不会转换到“已完成”状态。
重要提示3:Flink和FileSink从不覆盖提交的数据。鉴于此,当尝试从旧检查点/保存点(假设后续成功检查点提交了正在进行的文件)进行恢复时,FileSink将拒绝恢复并抛出异常,因为它无法定位正在进行的文件。
重要提示4:FileSink目前只支持HDFS、S3和Local三种文件系统。当在运行时使用不支持的文件系统时,Flink将抛出异常。
BATCH-specific
重要提示1:尽管Writer是使用用户指定的并行度执行的,但是Committer是使用等于1的并行度执行的。
重要提示2:在整个输入被处理后,暂挂文件被提交,即过渡到Finished状态。
重要提示3:当激活高可用性时,如果在提交者提交时发生JobManager故障,那么我们可能会有副本。这是固定的
未来的Flink版本(见FLIP-147的进展)。
S3-specific
重要提示1:对于S3, FileSink只支持基于hadoop的文件系统实现,不支持基于Presto的实现。如果您的作业使用FileSink写入S3,但您希望使用基于preto的文件ink进行检查点,建议显式使用“s3a://”(用于Hadoop)作为sink目标路径的方案,并使用“s3p://”作为检查点(用于Presto)。同时对接收器和检查点使用“s3://”可能会导致不可预测的行为,因为两个实现都“监听”该方案。
重要提示2:为了保证精确一次的语义同时高效,FileSink使用S3(从现在开始)的Multi-part Upload特性。该特性允许以独立的块(即“多部分”)上传文件,当主控板的所有部分都成功上传时,可以将其组合成原始文件。对于非活动的mpu, S3支持桶生命周期规则,用户可以使用该规则中止在初始化后指定天数内未完成的多部分上传。这意味着,如果您积极地设置该规则,并在某些部分文件未完全上传的情况下设置保存点,那么它们相关的mpu可能会在作业重启之前超时。这将导致您的作业无法从该保存点进行恢复,因为挂起的部分文件已经不存在了,并且Flink在试图获取它们时将失败,并出现异常。