目录标题
一、概述
此文内容主要来自于官方文档,并且使用spark streaming 消费kafka的数据进行实时计算,经过自己测试实验进行一个总结。
spark 版本:2.4.0
kafka 版本:0.10
scala版本:2.11
Kafka 0.10的Spark Streaming集成在设计上类似于0.8 Direct Stream方法。它提供简单的并行性,Kafka分区和Spark分区之间的1:1对应关系以及对偏移量和元数据的访问。但是,由于较新的集成使用了新的Kafka consumer API而不是简单的API,因此用法上存在显著差异。集成的此版本标记为实验性的,因此API可能会发生更改。
二、Spark Streaming 整合kafka步骤
1、引入依赖
对于使用SBT / Maven项目定义的Scala / Java应用程序,将流式应用程序与以下artifact链接
groupId = org.apache.spark
artifactId = spark-streaming-kafka-0-10_2.11
version = 2.4.0
不要手动添加对
org.apache.kafka
的依赖(例如kafka-clients)。该spark-streaming-kafka-0-10
已经具有适当的传递依赖关系,并且不同的版本可能不兼容。
这里使用maven来管理spark streaming的依赖,我们可以去maven仓库中搜索spark-streaming-kafka-0-10
,找到相应的依赖,我这里使用的是cdh集群安装的spark,所以选择Cloudera相关的依赖,如下图所示:
查看spark-streaming-kafka-0-10
jar包的位置在cloudera-repos仓库中
cloudera-repos仓库地址:
https://repository.cloudera.com/artifactory/cloudera-repos/
所以在pom.xml文件中需要添加<repository>
的地址才能下载对应的依赖,此次spark streaming 与kafka的整合还需要Spark-streaming 和 Spark-core的依赖,完整的pom.xml文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>spark-streaming-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<repositories>
<repository>
<id>scala-tools.org</id>
<name>Scala-Tools Maven2 Repository</name>
<url>http://scala-tools.org/repo-releases</url>
</repository>
<repository>
<id>cloudera</id>
<name>cloudera repository</name>
<url>https://repository.cloudera.com/artifactory/cloudera-repos/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>scala-tools.org</id>
<name>Scala-Tools Maven2 Repository</name>
<url>http://scala-tools.org/repo-releases</url>
</pluginRepository>
</pluginRepositories>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.spark/spark-streaming-kafka-0-10 -->
<!-- spark streaming 与kafka整合的依赖包 -->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-kafka-0-10_2.11</artifactId>
<version>2.4.0-cdh6.3.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.spark/spark-core -->
<!-- spark core, 创建SparkConf需要的依赖-->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.11</artifactId>
<version>2.4.0-cdh6.3.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.spark/spark-streaming -->
<!-- spark streaming ,创建StreamingContext需要的依赖-->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.11</artifactId>
<version>2.4.0-cdh6.3.1</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>src/main/scala</sourceDirectory>
<plugins>
<!--编译scala文件的插件-->
<plugin>
<groupId>org.scala-tools</groupId>
<artifactId>maven-scala-plugin</artifactId>
<version>2.15.2</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
<configuration>
<scalaVersion>2.11</scalaVersion>
</configuration>
</plugin>
<!--打jar包的插件-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<archive>
<manifest>
<mainClass>com.southcn.nfplus.recall.VideoRecallStreaming</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<reporting>
<plugins>
<plugin>
<groupId>org.scala-tools</groupId>
<artifactId>maven-scala-plugin</artifactId>
<configuration>
<scalaVersion>2.11</scalaVersion>
</configuration>
</plugin>
</plugins>
</reporting>
</project>
2、创建 Direct Stream
请注意,导入的名称空间包括版本org.apache.spark.streaming.kafka010
,scala代码如下:
package spark.streaming.demo
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.kafka010.{CanCommitOffsets, ConsumerStrategies, HasOffsetRanges, KafkaUtils, LocationStrategies}
import org.slf4j.{Logger, LoggerFactory}
object KafkaUtil {
// kafka的broker地址
val brokers: String = "cdh1:9092,cdh2:9092,cdh3:9092"
// 每一个stream使用一个独立的group.id
val groupId: String = "kafka_demo"
// 要消费的topic,接受一个数组,可以传入多个topic
val topics: Array[String] = Array("topicA", "topicB")
// 用于记录日志
val log: Logger = LoggerFactory.getLogger(this.getClass)
val kafkaParams: Map[String, Object] = Map[String, Object](
"bootstrap.servers" -> brokers,
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> groupId,
"auto.offset.reset" -> "latest",
"enable.auto.commit" -> (false: java.lang.Boolean)
)
def getKafkaStream(ssc: StreamingContext, topics: Array[String]): InputDStream[ConsumerRecord[String, String]] ={
// 使用KafkaUtils.createDirectStream创建数据流
val stream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream[String, String](
ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe[String, String](topics, kafkaParams)
)
stream
}
def main(args: Array[String]): Unit = {
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName(this.getClass.getName)
val ssm: StreamingContext = new StreamingContext(sparkConf, Seconds(10))
val inputDStream: InputDStream[ConsumerRecord[String, String]] = getKafkaStream(ssm, topics)
inputDStream.foreachRDD { rdd =>
// 获取偏移量
val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
rdd.foreachPartition {iter =>
iter.foreach { consumerRecord =>
val key: String = consumerRecord.key()
val value: String = consumerRecord.value()
println(s"key: ${key}, value: ${value}")
}
}
// 一段时间,计算完成之后, 异步提交偏移量
inputDStream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
}
ssm.start()
ssm.awaitTermination()
}
}
流中的每个项目都是一个ConsumerRecord
有关可能的kafkaParams的信息,请参阅Kafka Consumer config docs。如果您的Spark批处理持续时间大于默认的Kafka心跳会话超时(30秒),请适当增加heartbeat.interval.ms
和session.timeout.ms
。对于大于5分钟的批处理,这将需要更改代理上的group.max.session.timeout.ms
。请注意,示例将enable.auto.commit
设置为false,有关讨论,请参见下面的存储偏移。
1、LocationStrategies 位置策略说明
新的Kafka consumer API将消息预取到缓冲区中。因此,出于性能原因,Spark集成时将缓存的consumer保留在执行程序上(而不是为每个批次重新创建它们),并且更喜欢调度partitions到在具有适当的consumer的主机上,这一点很重要。
在大多数情况下,您应该使用LocationStrategies.PreferConsistent
如上所示。这将在可用执行程序之间平均分配分区。如果您的executors
与Kafka broker
位于同一主机上,请使用PreferBrokers
,它将首选在Kafka leader上为该分区调度分区。最后,如果分区之间的负载有明显的偏差,请使用PreferFixed
。这使您可以指定分区到主机的显式映射(任何未指定的分区将使用一致的位置)。
consumers的缓存的默认最大大小为64。如果您希望处理超过(64 *执行程序数)个Kafka分区,则可以通过更改此设置spark.streaming.kafka.consumer.cache.maxCapacity
。
如果您想为Kafka consumers禁用缓存,可以设置spark.streaming.kafka.consumer.cache.enabled
为false。要解决SPARK-19185中描述的问题,可能需要禁用缓存。解决SPARK-19185后,可以在更高版本的Spark中删除此属性。
缓存由topicpartition和group.id设置密钥,因此对的每次调用都应单独 使用一个。group.idcreateDirectStream
2、ConsumerStrategies 消费者策略说明
新的Kafka consumers API具有多种不同的方式来指定主题,其中一些方式需要大量的post-object-instantiation
设置。 ConsumerStrategies提供了一个抽象,即使从checkpoint重新启动后,Spark仍可以获取正确配置的consumers 。
ConsumerStrategies.Subscribe,如上所示,允许您订阅固定的主题集合。SubscribePattern允许您使用正则表达式指定感兴趣的主题。请注意,与0.8集成不同,在运行流期间使用Subscribe或SubscribePattern应该响应添加分区。最后,Assign允许您指定固定的分区集合。这三种策略都具有重载的构造函数,这些构造函数使您可以为特定分区指定起始偏移量。
如果您有上述选项无法满足的特定消费者设置需求,则ConsumerStrategy可以扩展公共类。
3、存储偏移量
发生故障时的Kafka交付语义取决于存储偏移量的方式和时间。Spark输出操作至少一次。因此,如果您希望等效于一次语义,则必须在等幂输出之后存储偏移量,或者在输出中将偏移量存储在原子事务中。通过这种集成,您可以按照增加可靠性(和代码复杂度)的顺序,使用3个选项来存储偏移量。
- checkpoint
如果启用Spark checkpointing,则偏移量将存储在检查点中。这很容易实现,但是有缺点。您的输出操作必须是幂等的,因为您将获得重复的输出。此外,如果您的应用程序代码已更改,则无法从检查点恢复。对于计划的升级,您可以通过与旧代码同时运行新代码来减轻这种情况(因为输出无论如何都需要等幂,因此它们不应冲突)。但是对于需要代码更改的计划外故障,除非有另一种方法来识别已知的起始偏移量,否则您将丢失数据。 - kafka本身
Kafka具有偏移提交API,该API在特殊的Kafka主题中存储偏移量。默认情况下,新使用者将定期自动提交偏移量。这几乎肯定不是您想要的,因为由使用者成功轮询的消息可能尚未导致Spark输出操作,从而导致语义未定义。这就是为什么上面的流示例将“ enable.auto.commit”设置为false的原因。但是,您可以使用commitAsyncAPI在知道存储了输出之后将偏移量提交给Kafka 。与检查点相比,它的好处是,无论您对应用程序代码进行的更改如何,Kafka都是持久存储。但是,Kafka不是事务性的,因此您的输出必须仍然是幂等的。 - 自己的数据存储,比如mysql
对于支持事务的数据存储,即使在失败情况下,将偏移与结果保存在同一事务中也可以使两者保持同步。如果您在检测重复或跳过的偏移量范围时很谨慎,则回滚事务可防止重复或丢失的消息影响结果。这相当于一次语义。即使是由于聚合而产生的输出(通常很难使等幂),也可以使用此策略。
参考文档
官方文档:http://spark.apache.org/docs/2.4.0/streaming-kafka-0-10-integration.html