「Flink实时数据分析系列」8.与外部系统的读写交互(上)

本文介绍了Apache Flink如何通过幂等性和事务性写实现流处理的一致性保证,详细讲解了源和接收端连接器的作用。Flink的Kafka连接器提供了至少一次和精确一次的输出保证,而文件系统连接器支持持续读写。此外,还探讨了Flink的Cassandra接收端连接器,提供了WAL模式以确保一致性。
摘要由CSDN通过智能技术生成

来源 | 「Stream Processing with Apache Flink」

作者 | Fabian Hueske and Vasiliki Kalavri

翻译 | 吴邪 大数据4年从业经验,目前就职于广州一家互联网公司,负责大数据基础平台自研、离线计算&实时计算研究

校对 | gongyouliu

编辑 | auroral-L

全文共11394字,预计阅读时间80分钟。

目录

一、应用的一致性保障

       1.幂等性写

       2.事务性写

二、内置连接器

       1.Apache Kafka 数据源连接器 

       2.Apache Kafka 接收端连接器 

       3.文件系统数据源连接器
              4.文件系统接收端连接器
              5.Apache Cassandra 接收端连接器

数据可以存储在许多不同形式的系统中,比如文件系统、对象存储、关系数据库系统、键值存储、搜索引擎、事件日志、消息队列等等。不同形式的系统为不同的访问模式设计的,适用于不同的特定场景。因此,今天的数据基础设施通常由多种多样的存储系统组成。在往架构中添加新组件之前,我们应该考虑一个问题:“新组件加入之后与原有组件的协作情况会有怎样的影响”

引入数据处理引擎之前要做好选型,以Apache Flink为例,因为它不包括存储层,而是依赖于外部存储系统来摄取和持久化数据。因此,对于像Flink这样的数据处理引擎来说,为了方便于外部系统进行数据读写交互,需要提供一个完备的连接器库以及实现自定义连接器的API,这两者是非常重要的。但是,对于一个希望在发生故障时能够实现一致性保证的流处理器来说,仅仅能够向外部数据存储中读取或写入数据是不够的。

在本章中,我们将讨论源和接收端连接器如何影响Flink流应用程序的一致性保证,并介绍Flink用于读写数据的最常见的连接器。你将了解如何实现自定义源和接收端连接器,以及如何实现向外部数据存储发送异步读写请求的方法。

一、应用的一致性保障 

在“检查点、保存点和状态恢复”章节中,你学到了Flink的检查点和定期接受应用程序状态的一致检查点的恢复机制。如果出现故障,应用程序的状态将从最新完成的检查点恢复并继续处理数据。然而,能够将应用的状态重置为一致点并不足以实现对应用的一致性管理保证。相反,应用程序的源和接收端连接器需要与Flink的检查点和恢复机制集成,并提供某些属性,以便能够提供有意义的保证。

为了给应用程序提供精确一次的状态一致性保证,应用程序的每个源连接器都需要能够将其读取位置设置为以前的检查点位置。在采取检查点时,source operator将保存其读取位置,并在故障恢复期间基于这些位置进行恢复。支持读取位置检查点的源连接器的示例是基于文件的源,这些源将读取offset存储在文件的字节流中,或者在Kafka源中,即分区主题。如果应用程序从源连接器获取数据,但无法存储和重置读取位置,则在发生故障时,应用程序可能会出现数据丢失,并且只能提供最多一次的一致性保证。

Flink的检查点和恢复机制以及可重新设置的源连接器这几个机制组合起来能够保证应用程序不会丢失任何数据。但是,应用程序可能会两次输出结果,因为在最后一个成功的检查点之后发出的所有结果(在恢复的情况下,应用程序退回到该检查点)将再次发出。因此,可重新设置的源和Flink的恢复机制不足以提供端到端的精确一次保证,即使应用程序状态是精确一次的。

一个旨在提供端到端精确一次保证的应用程序需要特殊的接收器连接器,有两种技术可以应用于不同的情况以实现精确一次的一致性保证:幂等写和事务性写。

1.幂等性写 

幂等运算可以执行多次,但是最终的结果是一样的。例如,重复地将相同的键值对插入到hashmap中是幂等操作,因为第一个插入操作将键值添加到map中,并且所有后续插入操作都不会更改map,因为它已经包含了键值对。另一方面,追加操作不是幂等操作,因为多次追加一个元素会导致结果多次变化。幂等写操作对于流应用程序来说很有趣,因为它们可以多次执行而不改变结果。因此,它们可以在一定程度上减轻由Flink的检查点机制引起的重放结果的影响。

值得注意的是,依赖幂等性的接收器来实现精确一次的应用程序必须保证重放时覆盖以前写的结果。例如,如果应用程序有一个接收器,它要将数据更新到键值存储中,则必须确保它能准确地计算用于更新的键值。此外,从sink系统读取数据的应用程序可能会在应用程序恢复期间观察到意外的结果。当重放开始时,先前发出的结果可能被先前的结果覆盖。因此,一个使用恢复应用程序的输出的应用程序,可能会看到时间上的跳跃,例如,读取比以前更小的计数。此外,在重放过程中,流应用程序的整体结果将处于不一致的状态,因为部分结果将被覆盖,而另一部分则没有。一旦重放完成,应用程序超过了先前失败的点,结果将再次保持一致。

2.事务性写 

实现端到端一致性的第二种方法是基于事务写,这里的构思是只将这些结果写入外部接收系统。这个方法确保端到端精确一次,因为在出现故障时,应用程序将重置到最后一个检查点,并且在该检查点之后没有向接收系统发送任何结果,只在完成检查点后才开始写数据,事务方法就不会受到幂等写重放不一致的影响,但是,它增加了延迟,因为只有在检查点完成后结果才是可见的。

Flink提供了两个功能模块来实现事务性的接收端连接器,一个通用的write-ahead-log (WAL)接收器和一个two-phase-commit(2PC)接收器。WAL sink将所有结果记录写入应用程序状态,并在接收到完成检查点的通知后将它们发送到sink系统。由于接收器将记录缓存在状态后端存储中,所以WAL接收器可以用于任何类型的接收器系统。然而,它并不能提供精确一次保证,相反会增加应用程序的状态大小,所以接收系统必须处理峰值的写入模式。

与WAL不同的是,2PC sink需要一个提供事务支持或公开构建块以模拟事务的接收器系统。对于每个检查点,接收器启动一个事务并将所有接收到的记录附加到事务中,将它们写入接收器系统而不提交它们。当它收到一个检查点完成的通知时,它提交事务并实现结果持久化。该机制依赖于sink从在完成检查点之前打开的故障中恢复后提交事务的能力。

2PC协议依赖Flink现有的检查点机制,检查点barriers是启动新事务的通知,所有关于operator其单个检查点成功的通知是提交投票,而通知检查点成功的JobManager消息是提交事务的指令。与WAL sink相比,2PC sink能够基于sink系统和sink的实现做到精确的一次输出。此外,一个2PC sink不断写记录到sink系统,不会出现WAL sink那种峰值的写入模式。

表8-1显示了在最佳情况下可以实现的不同类型的source和sink连接器的端到端一致性保证;根据sink的实现,实际的一致性可能会较差。

 

 

 

二、内置连接器 

Apache Flink提供连接器,用于与各种存储系统进行数据读写交互。消息队列和事件日志(如Apache Kafka、Kinesis或RabbitMQ)是读取数据流的常见源。在批处理为主的环境中,数据流也常常通过监控文件目录来摄取文件进行处理。

在sink端,数据流往往写入到消息队列,用于后续的事件流处理应用,写入文件系统归档或用于离线分析或批处理应用程序,或插入键值存储或关系数据库系统,如Cassandra,ElasticSearch或MySQL,使得数据可搜索和可查询,也可以用于仪表盘数据展示。

不好的一点是,除了用于RDBMS的JDBC外,大多数存储系统都没有标准接口,相反,每个系统都有自己的带有专用协议的连接器类库。因此,像Flink这样的处理系统需要维护几个专用连接器,以便能够从事件中读取事件并将事件写入最常用的消息队列、事件日志、文件系统、键值存储和数据库系统。

Flink为Apache Kafka、Kinesis、RabbitMQ、Apache Nifi、各种文件系统、Cassandra、ElasticSearch和JDBC提供连接器。此外,Apache Bahir项目还为ActiveMQ、Akka、Flume、Netty和Redis提供了额外的Flink连接器。

为了在应用程序中使用提供的连接器,需要将其依赖项添加到项目的构建文件中。我们在“引入外部和Flink依赖项”章节中解释了如何添加连接器依赖项。

在下一节中,我们将讨论Apache Kafka、基于文件的源和sink以及Apache Cassandra的连接器。这些是使用最广泛的连接器,你可以在Apache Flink或Apache Bahir的文档中找到关于其他连接器的更多信息。

1.Apache Kafka 数据源连接器 

Apache Kafka是一个分布式流平台。它的核心是一个分布式发布-订阅消息系统,广泛用于接收和分发事件流。在深入研究Flink的Kafka连接器之前,我们简要地解释一下Kafka的主要概念。

Kafka将事件流组织为所谓的主题(Topic)。主题是一个事件日志,它保证事件按写入的顺序读取,为了扩展主题的写和读,可以将主题划分为分布在集群中的分区,顺序保证仅限于分区,kafka在从不同分区读取时不提供顺序保证。Kafka分区中的读取位置称为offset。

Flink为所有常见的Kafka版本提供源连接器。通过Kafka 0.11,客户端库的API得到了改进,并添加了新的特性。例如,Kafka 0.10增加了对记录时间戳的支持。自从1.0发布以来,API一直保持稳定。Flink提供了一个通用的Kafka连接器,适用于0.11以后的所有Kafka版本。Flink还为Kafka的0.8、0.9、0.10和0.11版本提供了特定于版本的连接器。对于本节的其余部分,我们将重点讨论通用连接器,而对于特定于版本的连接器,建议参考Flink的官方文档。

将 Flink Kafka连接器的依赖项添加到Maven 配置文件中,如下图所示:

<dependency>
   <groupId>org.apache.flink</groupId>
   <artifactId>flink-connector-kafka_2.12</artifactId>
   <version>1.7.1</version>
</dependency>

Flink Kafka连接器并行地接收事件流,每个并行源任务可以从一个或多个分区读取数据。任务跟踪每个分区的当前读取offset,并将其包含到检查点数据中。从失败中恢复时,将恢复offset,并且源实例将继续从检查点offset读取数据。Flink Kafka连接器不依赖Kafka自己的offset跟踪机制,该机制基于所谓的消费者组。图8-1显示了对源实例的分区分配。

创建一个Kafka源连接器,如示例8-1所示。

val properties = new Properties()
properties.setProperty("bootstrap.servers", "localhost:9092")
properties.setProperty("group.id", "test")
val stream: DataStream[String] = env.addSource(
new FlinkKafkaConsumer[String](
"topic",
new SimpleStringSchema(),
properties))

构造函数有三个参数。第一个参数定义要读取的主题。可以是单个主题、主题列表,也可以是匹配所有要读取的主题的正则表达式。当从多个主题读取时,Kafka连接器将所有主题的所有分区都视为相同的,并将它们的事件多路复用到单个流中。

第二个参数是DeserializationSchema 或 KeyedDeserializationSchema。Kafka消息存储为原始字节消息,需要反序列化为Java或Scala对象。在例8-1中使用的SimpleStringSchema是一个内置的DeserializationSchema,它只是将字节数组反序列化为字符串。此外,Flink还为Apache Avro和基于文本的JSON编码提供了实现。

DeserializationSchema 和KeyedDeserializationSchema是公共接口,因此你可以实现自定义的反序列化逻辑。

第三个参数是一个Properties对象,它配置用于连接和读取Kafka的Kafka客户端。一个最基本的属性配置包含两个属性,"bootstrap.servers" 和"group.id"。有关其他配置属性,请查阅Kafka官方文档。为了获取事件时间戳并生成水位线,可以通过调用FlinkKafkaConsumer.assignTimestampsAndWatermark()方法向Kafka 消费者提供一个AssignerWithPeriodicWatermark或AssignerWithPunctuatedWatermark。将分配器应用到每个分区,从而利用每个分区的顺序保证,并且源实例根据水位线传播协议合并分区水位线(请查阅“水位线传播和事件时间”)。

请注意,如果一个分区处于不活动状态,则源实例的水位线将不起作用。因此,一个不活动的分区会导致整个应用程序停顿,因为应用程序的水位线不可用。

从0.10.0版本开始,Kafka支持消息时间戳。当从Kafka版本0.10或更高版本读取消息时,如果应用程序以事件时间模式运行,消费者将自动提取消息时间戳作为事件时间戳。在这种情况下,仍然需要生成水位线,并且应该应用AssignerWithPeriodicWatermark 或 AssignerWithPunctuatedWatermark来分发之前分配的Kafka时间戳。

还有一些需要注意的配置选项,例如可以配置最初读取Topic分区的起始位置。有效的选项是:

  • 对于一个消费者组而言,kafka通过group.id知道最后一次的消费位置,这也是默认配置:

FlinkKafkaConsumer.setStartFromGroupOffsets()
  • 从每个分区的最开始位置消费:

FlinkKafkaConsumer.setStartFromEarliest()
  • 从每个分区的最新位置消费:

FlinkKafkaConsumer.setStartFromLatest()
  • 消费大于指定时间戳的所有记录(基于Kafka 0.10或更高版本):

FlinkKafkaConsumer.setStartFromTimestamp(long)
  • 使用map对象,指定每个分区的消费起始位置:

FlinkKafkaConsumer.setStartFromSpecificOffsets(Map)

注意,这种配置只影响第一次读位置。在进行恢复或从保存点开始时,应用程序将从存储在检查点或保存点中的offset开始读取。

可以将Flink Kafka消费者配置为自动发现与正则表达式匹配的新Topic或添加到Topic中的新分区。这些特性在默认情况下是禁用的,可以通过向Properties对象添加具有非负数的参数flink.partitiondiscovery.interval-millis来启用。

 

2.Apache Kafka 接收端连接器 

Flink为0.8以后的所有Kafka版本提供sink连接器。从Kafka 0.11,客户端的API得到了改进,并添加了新的特性,比如Kafka 0.10支持记录时间戳,Kafka 0.11支持事务性写。自发布1.0以来,API一直保持稳定。Flink提供了一个通用的Kafka连接器,适用于0.11以后的所有Kafka版本。Flink还提供了针对Kafka 0.8、0.9、0.10和0.11版本的特定于版本的连接器。对于本节的其余部分,我们将重点介绍通用连接器,并向你介绍Flink的文档,以获得特定于版本的连接器。Flink的通用Kafka连接器的依赖项被添加到Maven项目中,如下图所示:

<dependency>
   <groupId>org.apache.flink</groupId>
   <artifactId>flink-connector-kafka_2.12</artifactId>
   <version>1.7.1</version>
</dependency>

将Kafka sink添加到DataStream应用程序中,如例8-2所示

val stream: DataStream[String] = ...
val myProducer = new FlinkKafkaProducer[String](
               "localhost:9092", // broker list
               "topic", // target topic
               new SimpleStringSchema) // serialization schema
stream.addSink(myProducer)

例8-2中使用的构造函数接收三个参数。第一个参数是一个逗号分隔的Kafka broker地址字符串。第二个是写入数据的topic的名称,最后一个是SerializationSchema,它将sink的输入类型(例8-2中的字符串)转换为字节数组。SerializationSchema是我们在Kafka源连接器部分讨论的DeserializationSchema的对应版本。

FlinkKafkaProducer提供了更多具有不同参数组合的构造函数,如下:

  • 与Kafka源连接器类似,可以为Kafka客户端传递一个Properties对象来提供定制选项。在使用Properties时,必须将brokers列表作为“bootstrap.servers”属性。查看Kafka文档以获得完整的参数列表。

  • 你可以指定一个FlinkKafkaPartitioner来控制记录如何映射到Kafka分区。我们将在本节后面更深入地讨论这个特性。

  • 你还可以指定KeyedSerializationSchema,而不是使用SerializationSchema将记录转换为字节数组,KeyedSerializationSchema将记录序列化为两个字节数组—一个用于键,另一个用于Kafka消息的值。此外,KeyedSerializationSchema还提供了更多的功能,比如覆盖目标主题以写入多个主题。

kafka sink 至少一次的保证

Flink的Kafka sink提供的一致性保证取决于它的配置。Kafka sink在以下条件下提供至少一次的保证:

  • 启用了Flink的检查点,应用程序的所有sources都可以重新设置。

  • 如果写操作不成功,sink连接器将抛出异常,导致应用程序失败并恢复。这是默认的行为。通过将retries属性设置为大于0的值(默认值),可以将Kafka客户端配置为在写操作失败之前进行重试。你还可以通过在接收器对象上调用setLogFailuresOnly(true)来将接收器配置为只记录写故障。注意,这将使应用程序的输出保证无效。

  • sink连接器等待Kafka在完成其检查点之前确认输出记录。这是默认的行为。通过调用sink对象上的setFlushOnCheckpoint(false)禁用这种等待。但是,这也将禁用任何输出保证。

kafka sink 精确一次的保证

Kafka 0.11引入了对事务性写的支持。由于这个特性,Flink的Kafka接收器也能够提供精确的一次输出保证,只要接收器和Kafka配置正确。同样,Flink应用程序必须启用检查点并从可重置的源消费。此外,FlinkKafkaProducer提供了一个语义参数的构造函数,该参数负责调控接收器提供的一致性保证,如下:

  • Semantic.NONE,它不提供任何保证,记录可能丢失或多次写入。

  • Semantic.AT_LEAST_ONCE,它保证没有写操作丢失,但是可能会重复。这是默认设置。

  • Semantic.EXACTLY_ONCE,它构建在Kafka的事务上,将每个记录精确地写入一次。

当使用Kafka接收器以精确一次模式运行Flink应用程序时,需要考虑一些事情,这有助于大致了解Kafka如何处理事务。简而言之,Kafka的事务将所有消息添加到分区日志中,并将打开的事务标记为未提交。一旦事务被提交,标记就被更改为已提交。从topic读取数据的消费者可以配置一个隔离级别(通过isolation.level属性)。声明它是否可以读取未提交的消息(默认read_uncommitted)。如果消费者被配置为read_committed,那么一旦它遇到未提交的消息,它就停止从一个分区消费,并在提交消息时继续使用。因此,打开的事务可能会阻止消费者读取分区消息并带来显著的延迟。Kafka通过在超时后拒绝和关闭事务来防止这种情况,使用transaction.timeout.ms的属性进行配置超时时间。

在Flink的Kafka sink上下文中,由于恢复周期过长而超时的事务会导致数据丢失,所以正确配置事务超时属性是非常重要的。默认情况下,Flink Kafka sink设置transaction.timeout.ms为一小时。这意味着你可能需要调整kafka本身的transaction.max.timeout.ms属性,默认设置为15分钟。此外,提交消息的可见性取决于Flink应用程序的检查点间隔。请查阅Flink文档,了解在启用精确一次一致性时的其他相关案例。

检查kafka集群的配置

Kafka集群的默认配置仍然会导致数据丢失,即使在确认写操作之后也是如此。你应该仔细修改Kafka设置的配置,特别注意以下参数:

ack
log.flush.interval.messages
log.flush.interval.ms
log.flush。*

我们建议你参考Kafka文档,以获得关于它的配置参数的详细信息和适用配置的指导原则。

自定义分区和写入消息时间戳

当将消息写入Kafka topic时,Flink Kafka sink任务可以选择写入topic的哪个分区。FlinkKafkaPartitioner可以在Flink Kafka sink的一些构造函数中定义。如果没有指定,默认的分区器将每个sink任务映射到一个Kafka分区,由同一个sink任务发出的所有记录都被写到同一个分区,如果任务多于分区,单个分区可能包含多个sink任务的记录。如果分区的数量大于子任务的数量,则默认配置将导致空分区,事件时间模式下的Flink应用程序,消费此时的topic是,可能会出现问题。

通过提供一个自定义FlinkKafkaPartitioner,你可以控制如何将记录路由到topic分区。例如,可以根据记录的key属性创建分区器,或者创建循环分区器以实现均匀分布。还可以根据消息key将分区委托给Kafka。这需要一个KeyedSerializationSchema来提取消息key,并使用null配置FlinkKafkaPartitioner参数来禁用默认分区器。

最后,可以将Flink的Kafka sink配置为写入消息时间戳,这是Kafka 0.10所支持的。通过在sink对象上调用setWriteTimestampToKafka(true),可以将记录的事件时间戳写入Kafka。

3.文件系统数据源连接器 

Filesystems通常用于以成本低效益高的方式存储海量数据。在大数据体系架构中,它们通常充当批处理应用程序的数据源和数据接收器。结合Apache Parquet或Apache ORC这些高级文件格式,文件系统可以有效地为分析查询引擎,如Apache Hive、Apache Impala或Presto等提供服务。因此,文件系统通常用于“连接”流和批处理应用程序。

Apache Flink提供了一个可重置的源连接器,以摄入文件中的数据作为流。文件系统源是flinkstreaming- java模块的一部分,因此,你不需要添加任何其他依赖项来使用此功能。Flink支持不同类型的文件系统,比如本地文件系统(包括NFS或SAN共享、Hadoop HDFS、Amazon S3和OpenStack Swift FS)。请查阅“文件系统配置”以了解如何在Flink中配置文件系统。示例8-3显示了如何通过按行读取文本文件来读取流。

val lineReader = new TextInputFormat(null)
val lineStream: DataStream[String] = env.readFile[String](
lineReader, // The FileInputFormat
"hdfs:///path/to/my/data", // The path to read
FileProcessingMode.PROCESS_CONTINUOUSLY, // The processing mode
30000L) // The monitoring interval in ms

StreamExecutionEnvironment.readFile()方法的参数为:

  • FileInputFormat负责读取文件内容。我们将在本节的后面讨论这个接口的细节。例8-3中的TextInputFormat的null参数定义了单独设置的路径。

  • 应该读取的路径。如果路径引用一个文件,则读取单个文件,如果它引用一个目录,FileInputFormat将扫描该目录以读取文件。

  • 读取路径的模式。主要有两种,分别是PROCESS_ONCE或 PROCESS_CONTINUOUSLY。在PROCESS_ONCE模式中,当作业启动并读取所有匹配的文件时,读取路径将被扫描一次。在 PROCESS_CONTINUOUSLY中,将定期扫描路径(初始扫描后),连续读取新文件和修改过的文件。

  • 周期性扫描路径的时间间隔,单位为毫秒。在PROCESS_ONCE模式中忽略该参数。

FileInputFormat是一种专门用于从文件系统中读取文件的InputFormat。FileInputFormat分两个步骤读取文件。第一步,它会扫描文件系统路径,并为所有匹配的文件创建所谓的文件切割分片。文件切割分片定义文件上的范围,通常用起始offset和长度定义。在将一个大文件分成多个分段之后,可以将这些分段分配给多个reader任务来并行地读取文件。根据文件的编码,可能只需要生成一个分割来读取整个文件。FileInputFormat的第二步是接收输入分割,读取分割定义的文件范围,并返回所有相应的记录。

DataStream应用程序中使用的FileInputFormat还应该实现CheckpointableInputFormat接口,该接口定义了检查点的方法,并在文件分割中重置InputFormat的当前读取位置。如果FileInputFormat没有实现CheckpointableInputFormat接口,则文件系统源连接器仅在启用检查点时至少提供一次保证,因为输入格式将从上次执行完整检查点时处理的分割开始读取。

在1.7版本中,Flink提供了一些扩展FileInputFormat和实现CheckpointableInputFormat的类。TextInputFormat按行读取文本文件(按换行字符分隔),CsvInputFormat的子类按逗号分隔值读取文件,AvroInputFormat按avro编码记录读取文件。

在PROCESS_CONTINUOUSLY模式下,文件系统源连接器根据修改时间戳识别新文件。这意味着如果一个文件被修改,它将被完全重新处理,因为修改时间戳发生了变化,包括由于追加内容而引起的修改,因此持续读取文件的一种常见技术是将它们写入临时目录,并在完成后自动将它们移到被监控的目录中。当一个文件被完全读取并且完成了一个检查点时,就可以从目录中删除它。如果读取最终一致的列表操作,如S3的文件存储,也有影响。由于文件可能不会按照其修改时间戳的顺序出现,因此文件系统源连接器可能会忽略它们。

值得注意的是,在PROCESS_ONCE模式中,在扫描文件系统路径并创建所有的分片之后,不会采取任何检查点。

如果你想在基于事件时间应用程序中使用文件系统源连接器,生成水位线具体一定的挑战性,因为输入拆分是在一个进程中生成的,并且按照文件的修改时间戳分发给所有并行reader任务。为了生成合适的水印,你需要对包含在任务稍后处理的拆分中的记录的最小时间戳进行推理。

 

4.文件系统接收端连接器 

将流写入文件是一个常见的需求,比如说,为离线分析处理准备低延迟的数据。因为大多数应用程序只有在文件完成写入,以及流应用程序长时间运行后才能读取文件,所以streaming sink 连接器通常会将其输出分块到多个文件中。此外,通常会将记录组织到所谓的bucket中,这样消费应用程序可以更好地控制读取哪些数据。

与文件系统源连接器一样,Flink的StreamingFileSink连接器也包含在flink-streaming-java模块中。因此,不需要向构建文件添加依赖项。

StreamingFileSink为应用程序提供端到端的精确一次保证,因为应用程序配置了精确一次检查点,并且在出现故障时重置所有源。我们将在本节后面更详细地讨论恢复机制。示例8-4展示了如何使用最基本的配置创建StreamingFileSink并将其附加到流中。

val input: DataStream[String] = …
val sink: StreamingFileSink[String] = StreamingFileSink.forRowFormat(
   new Path("/base/path"),
   new SimpleStringEncoder[String]("UTF-8")
).build()


input.addSink(sink)

当StreamingFileSink接收到记录时,将记录分配给一个bucket。bucket是基本路径的子目录,在示例8-4中使用StreamingFileSink构建器配置了“/base/path”。

bucket是由BucketAssigner选择的,这是一个公共接口,并为每个记录返回一个BucketId,该BucketId决定了记录将被写入的目录。可以使用withBucketAssigner()方法在构建器上配置BucketAssigner()。如果没有显式指定BucketAssigner,则使用DateTimeBucketAssigner,根据记录写入时的处理时间将记录分配到每小时的bucket。

每个bucket目录包含多个分片文件,这些文件由StreamingFileSink的多个并行实例并发生成。此外,每个并行实例将其输出分割成多个分片文件。分片文件的路径格式如下:

[base-path]/[bucket-path]/part-[task-idx]-[id]

例如,给定一个“/johndoe/demo”的基本路径和一个part前缀“part”,这个路径“/johndoe/demo/2018-07-22-17/part-4-8”指向由第五个(下标从0开始)sink任务写入bucket“2018-07-22-17”的8个文件,即:2018年7月22日下午5点。

提交文件的id可能不是连续的

非连续文件id(提交文件名称中的最后一个数字)不表示数据丢失。StreamingFileSink只是增加文件id。当丢弃挂起的文件时,它不会重用它们的id。

RollingPolicy确定任务何时创建新分片文件。可以使用构建器上的withRollingPolicy()方法来配置RollingPolicy。默认情况下,StreamingFileSink使用DefaultRollingPolicy,该策略配置为当分片文件超过128 MB或超过60秒时滚动生成新分片文件。还可以配置非活动时间间隔,在此之后将滚动生成分片文件。

StreamingFileSink支持两种将记录写入分片文件的模式:row编码和buik编码。在row编码模式中,每个记录都单独编码并附加到一个分片文件中,在bulk编码中,记录被分批收集和写入的。Apache Parquet以列格式组织和压缩记录,是一种需要bulk编码的文件格式。

例8-4通过提供一个将单个记录写入分片文件的Encoder,使用row编码创建StreamingFileSink。在例8-4中,我们使用了SimpleStringEncoder,它调用了记录的toString()方法,并将记录的字符串写入文件。Encoder是一个简单的接口和单一方法,非常容易实现。

例8-5所示,创建了一个bulk编码的StreamingFileSink。

val input: DataStream[String] = …
val sink: StreamingFileSink[String] = StreamingFileSink.forBulkFormat(
   new Path("/base/path"),
   ParquetAvroWriters.forSpecificRecord(classOf[AvroPojo])
).build()


input.addSink(sink)

bulk编码模式下的StreamingFileSink需要BulkWriter.Factory。在例8-5中,我们对Avro文件使用了Parquet writer。请注意,Parquet writer包含在flink-parquet模块中,该模块需要作为依赖项添加。像往常一样,BulkWriter.Factory是一个可以实现自定义文件格式的接口,如Apache Orc。

bulk编码模式下的StreamingFileSink不能选择RollingPolicy。bulk编码格式只能与OnCheckpointRollingPolicy相结合,OnCheckpointRollingPolicy在每个检查点上滚动生成分片文件。

StreamingFileSink提供了精确的一次输出保证。StreamingFileSink通过一个提交协议来实现这一点,该协议将文件移动到不同的stages,包括处理中、挂起状态和完成状态,基于Flink的检查点机制。当sink写入文件时,文件处于处理中状态。当RollingPolicy决定滚动文件时,将关闭该文件并通过重命名将其移动到挂起状态。当下一个检查点完成时,挂起的文件将移动到完成状态(再次通过重命名)。

挂起的文件可能永远不会被提交

在某些情况下,永远不会提交挂起文件。StreamingFileSink确保这不会导致数据丢失。但是,这些文件不会自动清除。

在手动删除一个挂起文件之前,你需要检查它是在延迟还是即将提交。找到具有相同任务索引和更大ID的提交文件后,可以安全地删除挂起文件。

在失败的情况下,sink任务需要将当前正在处理的文件重置为最近一次成功检查点处的offset。这是通过关闭当前正在处理的文件并丢弃文件末尾的无效部分来实现的,例如,通过使用文件系统的truncate操作。

STREAMINGFILESINK需要启用检查点

如果应用程序没有启用检查点,那么StreamingFileSink将永远不会将文件从挂起状态移动到完成状态。

5.Apache Cassandra 接收端连接器 

Apache Cassandra是一个流行的、可伸缩的和高可用的列存储数据库系统。Cassandra 将数据集建模为由多个类型的列组成的行的表,一个或多个列必须定义为(复合)主键。每行都有的主键作为唯一标识。在其他API中,Cassandra 查询语言(CQL)是Cassandra 的特性,是一种类似于SQL的语言,用于读写数据、创建、修改和删除数据库对象,如键空间和表。

Flink提供了一个接收端连接器来将数据流写入Cassandra。Cassandra 的数据模型是基于主键的,所有对Cassandra 的写入都是基于upsert语义。结合checkpointing、resetable source和确定性应用程序逻辑,可以保证最终输出的一致性,因为结果在恢复过程中被重置为以前的版本,这意味着消费者可能会读取比以前更旧的结果。此外,多个键的值可能不同步。

为了防止恢复过程中的时间不一致,并为具有不确定性应用程序逻辑的应用程序提供精确一次的输出保证,Flink的Cassandra 连接器可以配置为WAL。我们将在本节后面更详细地讨论WAL模式。以下代码显示了使用Cassandra 接收器连接器而需要添加到应用程序的构建文件中的依赖关系:

<dependency>
  <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-cassandra_2.12</artifactId>
  <version>1.7.1</version>
</dependency> 

为了说明Cassandra 接收端连接器的使用,我们使用了Cassandra 表的简单示例,它包含关于传感器读数的数据,由传感器Id和温度两列组成。

CREATE KEYSPACE IF NOT EXISTS example
WITH replication = {'class': 'SimpleStrategy', 'replication_factor':
'1'};
CREATE TABLE IF NOT EXISTS example.sensors (
sensorId VARCHAR,
temperature FLOAT,
PRIMARY KEY(sensorId)
);

Flink提供了不同的接收器实现将不同数据类型的数据流写入Cassandra 。Flink的Java Tuple和Row类型以及scala的内置Tuple和case类的处理方式与用户定义的POJO类型不同。我们分别讨论这两种情况。示例8-7展示了如何创建一个接收器,它将Tuple、case类或Row的数据流写入Cassandra 表中。在这个例子中,将DataStream[(String,Float))写入“传感器”表。

val readings: DataStream[(String, Float)] = ???
val sinkBuilder: CassandraSinkBuilder[(String, Float)] =
CassandraSink.addSink(readings)
sinkBuilder
.setHost("localhost")
.setQuery(
"INSERT INTO example.sensors(sensorId, temperature) VALUES (?, ?);")
.build()

Cassandra sink是调用Cassandra Sink.add Sink()方法获得的构建器创建和配置的,添加应该发出的数据流对象, 该方法为数据流的数据类型返回正确的构造器。在示例8-7中,它返回用于处理Scala Tuple的Cassandra接收器的构造器。

用于Tuple、case类和Row的Cassandra接收器构建器需要遵循CQL  INSERT查询的规范。使用CassandraSinkBuilder.setQuery()方法配置查询。在执行期间,sink将准备好的语句进行注册,并将Tuple、case类或Row的字段转换为已准备好的语句的参数。字段根据其位置映射到对应的参数中。

由于POJO字段没有顺序,因此需要对它们进行不同的处理。示例8-8演示了如何为类型传感器读取的POJO配置Cassandra接收器。

val readings: DataStream[SensorReading] = ??? CassandraSink.addSink(readings) 
.setHost("localhost") 
.build()

如示例8-8所示,我们不指定INSERT查询。相反,POJO交给Cassandra的对象映射器,它自动将POJO字段映射到Cassandra表的字段。为了使其生效,POJO类及其字段需要进行注释,并为所有字段提供setter和getter方法,如示例8-9所示。默认构造函数是Flink在讨论支持的数据类型时在“支持的数据类型”中提到的。

@Table(keyspace = "example", name = "sensors")
 class SensorReadings( 
 @Column(name = "sensorId") var id: String, 
 @Column(name = "temperature") var temp: Float) { 
 def this() = { this("", 0.0) }
 def setId(id: String): Unit = this.id = id 
 def getId: String = id 
 def setTemp(temp: Float): Unit = this.temp = temp 
 def getTemp: Float = temp 
 }
 

除了图8-7和8-8中的配置选项外,Cassandra 接收器器还提供了一些配置接收器连接器的方法:

  • setClusterBuilder(ClusterBuilder):集群构造器构建一个Cassandra集群,用于管理与Cassandra的连接。它可以配置一个或多个主机名和端口;定义负载均衡、重试和重新连接策略;并提供访问凭据。

  • setHost(String,[Int]):此方法是配置单个地址的主机名和端口的简单集群构造器的快捷方式。如果没有配置端口,则使用Cassandra的默认端口9042。

  • setQuery(String):这指定CQL INSERT查询将Tuple、case类或Row写入Cassandra。

  • setMapperOptions(MapperOptions):这为Cassandra的对象映射器提供了选项,例如一致性、生命周期和空字段处理的配置。如果接收器发出Tuple、case类或Row,则忽略选项。

  • enableWriteAheadLog([CheckpointCommitter]):这使得WAL能够在非确定性应用程序逻辑的情况下提供精确一次的输出保证。检查点比较器用于在外部数据存储中存储有关已完成的检查点的信息。如果未配置CheckpointCommitter,则信息将写入特定的Cassandra 表。

带有WAL的Cassandra接收器连接器是基于Flink的GenericWriteAheadSink operator实现的。这个operator是如何工作的,包括CheckpointCommitter的作用,以及它提供了哪些一致性保证,在“事务Sink连接器”中有更详细的描述”。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

数据与智能

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值