Spark Streaming + Kafka Integration Guide (Kafka broker version 0.8.2.1 or higher)
Note: Kafka 0.8 support is deprecated as of Spark 2.3.0.
这里我们解释如何配置Spark流来接收来自Kafka的数据。有两种方法可以做到这一点——使用接收器的旧方法和Kafka的高级API,以及不使用接收器的新方法(在Spark 1.3中引入)。它们具有不同的编程模型、性能特征和语义保证,因此请继续阅读以了解更多细节。在Spark的当前版本中,这两种方法都被认为是稳定的APIs。
Approach 1: Receiver-based Approach
这种方法使用接收器来接收数据。此接收器是使用Kafka高级消费者API实现的。与所有接收器一样,通过接收器从Kafka接收的数据存储在Spark executor中,然后由Spark Streaming启动的作业处理数据。
然而,在默认配置下,这种方法可能会在故障情况下丢失数据(请参阅 receiver reliability--接收器可靠性)。为了确保零数据丢失,您还必须在Spark流中启用写前日志(在Spark 1.2中引入)。这将同步地将所有接收到的Kafka数据保存到分布式文件系统(如HDFS),以便在发生故障时可以恢复所有数据。有关写前日志的详细信息,请参阅流编程指南中的 Deploying section部分。
接下来,我们将讨论如何在流应用程序中使用这种方法。
1. Linking: 对于使用SBT/Maven项目定义的Scala/Java应用程序,请将您的流应用程序链接到以下工件(有关更多信息,请参阅主编程指南中的 Linking section)。
groupId = org.apache.spark
artifactId = spark-streaming-kafka-0-8_2.12
version = 2.4.3
对于Python应用程序,在部署应用程序时必须添加上述库及其依赖项。见下面的部署小节。
2. Programming: 在流应用程序代码中,导入 KafkaUtils
并创建输入数据流,如下所示。
from pyspark.streaming.kafka import KafkaUtils
kafkaStream = KafkaUtils.createStream(streamingContext, \
[ZK quorum], [consumer group id], [per-topic number of Kafka partitions to consume])
默认情况下,Python API 将把Kafka数据解码为UTF8编码的字符串。您可以指定自定义解码函数,将Kafka记录中的字节数组解码为任意数据类型。请参阅 API docs。
要记住的要点:
。Kafka中的主题分区与Spark流中生成的RDDs分区不相关。因此,增加 KafkaUtils.createStream()
中指定主题的分区的数量只会增加在单个接收器中使用哪些主题的线程的数量。它不会增加Spark在处理数据时的并行性。有关这方面的更多信息,请参考主文档。
。可以使用不同的组和主题创建多个Kafka输入DStreams,以便使用多个接收器并行接收数据。
。如果使用复制的文件系统(如HDFS)启用了写前日志,则接收到的数据已经被复制到日志中。因此,输入流的存储级别是 StorageLevel.MEMORY_AND_DISK_SER(也就是说,使用KafkaUtils.createStream(..., StorageLevel.MEMORY_AND_DISK_SER)
)。
3. Deploying: 任何Spark应用程序一样, spark-submit
用于启动应用程序。然而,Scala/Java应用程序和Python应用程序的细节略有不同。
对于Scala和Java应用程序,如果您使用SBT或Maven进行项目管理,那么将 spark-streaming-kafka-0-8_2.12
及其依赖项打包到应用程序JAR中。确保 spark-core_2.12
和spark-streaming_2.12
被标记为提供的依赖项,就像在Spark安装中已经存在的依赖项一样。然后使用spark-submit启动应用程序(请参阅主编程指南中的 Deploying section 部分)。
对于缺少SBT/Maven项目管理的Python应用程序,可以使用--packages
直接将 spark-streaming-kafka-0-8_2.12
及其依赖项添加到 spark-submit
中(请参阅 Application Submission Guide)。也就是说,
./bin/spark-submit --packages org.apache.spark:spark-streaming-kafka-0-8_2.12:2.4.3 ...
或者,您也可以从 Maven repository --Maven存储库下载Maven工件 spark-streaming-kafka-0-8-assembly
的JAR,并将其添加到 spark-submit
中 --jars
。
Approach 2: Direct Approach (No Receivers)
Spark 1.3中引入了这种新的无接收器“直接”方法,以确保更好的端到端保证。这种方法不使用接收器来接收数据,而是定期查询Kafka在每个主题+分区( topic+partition)中的最新偏移量,并相应地定义在每个批处理中要处理的偏移量范围。启动处理数据的作业时,Kafka的简单消费者API用于从Kafka读取已定义的偏移量范围(类似于从文件系统读取文件)。注意,这个特性是在Spark 1.3中为Scala和Java API引入的,在Spark 1.4中为Python API引入的。
与基于接收器的方法(即方法1)相比,此方法具有以下优点:
. Simplified Parallelism (简化的并行性):不需要创建多个输入Kafka流并将它们合并。使用directStream, Spark流将创建与要消费的Kafka分区一样多的RDD分区,这些分区将并行地从Kafka读取数据。因此Kafka和RDD分区之间存在一对一的映射,这更容易理解和优化。
. Efficiency(效率):在第一种方法中实现零数据丢失要求数据存储在写前日志中,而写前日志又进一步复制了数据。这实际上是低效的,因为数据被有效地复制了两次——一次由Kafka复制,第二次由Write-Ahead日志复制。第二种方法消除了这个问题,因为没有接收器,因此不需要提前写日志。只要有足够的Kafka保留,就可以从Kafka中恢复消息。
. Exactly-once semantics:第一种方法使用Kafka的高级API在Zookeeper中存储消费的偏移量。这是使用Kafka数据的传统方式。虽然这种方法(与写前日志相结合)可以确保零数据丢失(即至少一次语义),但是在某些失败情况下,有些记录可能被消费两次的几率很小。这是因为Spark流可靠接收的数据与Zookeeper跟踪的偏移量不一致。因此,在第二种方法中,我们使用不使用Zookeeper的简单Kafka API。偏移量由Spark流在其检查点内跟踪。这消除了Spark流和Zookeeper/Kafka之间的不一致性,因此,尽管失败了,Spark流仍然有效地接收每条记录一次。为了实现结果输出的精确地一次性语义,将数据保存到外部数据存储的输出操作必须是幂等的,或者是保存结果和偏移量的原子事务(有关进一步信息,请参阅主编程指南中的输出操作语义-- Semantics of output operations)。
注意,这种方法的一个缺点是它不更新Zookeeper中的偏移量,因此基于Zookeeper的Kafka监视工具不会显示进度。但是,您可以在每个批处理中访问此方法处理的偏移量,并亲自更新Zookeeper(参见下面)。
接下来,我们将讨论如何在流应用程序中使用这种方法。
1. Linking: 这种方法只在Scala/Java应用程序中受支持。将您的SBT/Maven项目与以下构件链接起来(有关更多信息,请参阅主编程指南中的链接部分)。
groupId = org.apache.spark
artifactId = spark-streaming-kafka-0-8_2.12
version = 2.4.3
2. Programming: 在流应用程序代码中,导入KafkaUtils并创建一个输入DStream,如下所示。
from pyspark.streaming.kafka import KafkaUtils
directKafkaStream = KafkaUtils.createDirectStream(ssc, [topic], {"metadata.broker.list": brokers})
您还可以将 messageHandler
传递给 createDirectStream
,以访问包含关于当前消息的元数据的 KafkaMessageAndMetadata
,并将其转换为所需的任何类型。默认情况下,Python API将Kafka数据解码为UTF8编码的字符串。可以指定自定义解码函数,将Kafka记录中的字节数组解码为任意数据类型。参见 API docs。
在Kafka参数中,必须指定 metadata.broker.list
或 bootstrap.servers
。默认情况下,它将开始使用每个Kafka分区的最新偏移量。如果将Kafka参数中的配置 auto.offset.reset
设置为 smallest
,那么它将从最小的偏移开始消耗。
也可以使用 KafkaUtils.createDirectStream
的其他变体从任意偏移开始使用。此外,如果要访问每个批次中消耗的Kafka偏移量,可以执行以下操作。
offsetRanges = []
def storeOffsetRanges(rdd):
global offsetRanges
offsetRanges = rdd.offsetRanges()
return rdd
def printOffsetRanges(rdd):
for o in offsetRanges:
print "%s %s %s %s" % (o.topic, o.partition, o.fromOffset, o.untilOffset)
directKafkaStream \
.transform(storeOffsetRanges) \
.foreachRDD(printOffsetRanges)
如果您希望基于ZooKeeper的Kafka监控工具显示流应用程序的进度,可以使用此工具自己更新ZooKeeper。
注意,HasOffsetRanges的类型转换只有在directKafkaStream上调用的第一个方法中完成时才会成功,而不是在随后的方法链中。为了访问偏移量,可以使用transform()代替foreachRDD()作为第一个方法调用,然后进一步调用Spark的其它方法。但是,请注意RDD分区和Kafka分区之间的一对一映射不会在任何洗牌或重分区(例如reduceByKey()或window())方法之后保留。
另一件需要注意的是,由于这种方法不使用接收器,因此与接收器相关的标准(即,spark.streaming.receiver.*
形式的配置)将不适用于这种方法(但将适用于其他输入DStreams)创建的输入数据流。相反,使用 spark.streaming.kafka.*
配置。一个重要的是spark.streaming.kafka.maxRatePerPartition
,它是此直接API读取每个kafka分区的最大速率(每秒消息数)。
3. Deploying:这与第一种方法相同。