原文链接:http://spark.apache.org/docs/latest/streaming-kafka-0-10-integration.html
SparkStreaming集成Kafka0.10版本与集成0.8版本Kafka的
直接连接方式类似。提供了简单的并行性以及Kafka分区和Spark分区之间一对一的映射,以及元数据和偏移量的访问。然而,因为使用
新版本的消费者的API而不是简单方式的API造成了一些使用上的不同。当前版本的APi是试验性的,因此可能会被修改。
引入
对于scala或者java使用Maven或SBT的应用,在你的流处理应用中加入如下依赖(参考主文档中的
引入部分获得更多信息
>
groupId = org.apache.spark
>artifactId = spark-streaming-kafka-0-10_2.11
>version = 2.3.0
不要手动添加org.apache.kafka的依赖如(kafka-clients)。spark-streaming-kafka-0-10已经有适当的依赖树并且不同的版本之间容易出现不兼容的状况。
创建直接方式的流
注意导入的包名称以及版本是org.apache.spark.streaming.kafka010
Scala版本代码:
import
org.apache.kafka.clients.consumer.ConsumerRecord
import
org.apache.kafka.common.serialization.StringDeserializer
import
org.apache.spark.streaming.kafka010._
import
org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent
import
org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe
val
kafkaParams
=
Map
[
String
,
Object
](
"bootstrap.servers"
->
"localhost:9092,anotherhost:9092"
,
"key.deserializer"
->
classOf
[
StringDeserializer
],
"value.deserializer"
->
classOf
[
StringDeserializer
],
"group.id"
->
"use_a_separate_group_id_for_each_stream"
,
"auto.offset.reset"
->
"latest"
,
"enable.auto.commit"
->
(
false:
java.lang.Boolean
)
)
val
topics
=
Array
(
"topicA"
,
"topicB"
)
val
stream
=
KafkaUtils
.
createDirectStream
[
String
,
String
](
streamingContext
,
PreferConsistent
,
Subscribe
[
String
,
String
](
topics
,
kafkaParams
)
)
stream
.
map
(
record
=>
(
record
.
key
,
record
.
value
))
数据流中的每一项都是一个
ConsumerRecord类。
获取更多关于kafka参数的信息,参考
Kafka消费者配置文档
。如果Spark配置的批处理间隔大于Kafka默认的心跳超时时间(30秒),需要适当提高心跳间隔时间(heartbeat.interval.ms)以及session过期时间(session.timeout.ms)。如果批处理间隔大于五分钟,需要修改节点(broker)上配置的组最大过期时间(group.max.session.timeout.ms)。注意本例中,enable.auto.commiit参数是设置为false的,更加细节的讨论参看下方
偏移量维护
部分
本地策略(LocationStragies)
因为新的Kafka消费的API会将消息预先拉取到缓冲区中。因此,处于性能原因Spark会在executor上缓存消费者(而不是在每个批上(batch)创建消费者),并且优先在拥有更适合的消费者所在的主机上安排分区。
大多数情况下你应该使用如上所示的LocationStrategies.PreferConsistent。这将在可用的executors上均匀分布分区。如果你的executor与Kafka的代理节点在同一台物理机上,使用PreferBrokers,这更倾向于在该节点上安排KafkaLeader对应的分区。最后如果发生分区之间数据负载倾斜,使用PreferFixed。这允许你指定分区和主机之间的映射(任何未指定的分区将使用一致的位置)。
为消费者提供的最大缓存数为64,如果你希望处理超过(64*executor的数量)的kafka的分区,你可以spark.streaming.kafka.consumer.cache.maxCapacity配置类修改此设置。
如果不希望应用Kafka消费者的缓存策略,你可以将spark.streanig.kafka.consumer.cache.enabled设置为false。要解决SPARK-19185中描述的问题可能需要禁用缓存。一旦SPARK-19185bug被修复,该设置将在后续的版本中被删除。
缓存是将topic的分区和groupid作为key的,因此每次调用createDirectStream需要使用一个单独的group.id。
消费策略(ConsumerStrategies)
新版Kafka消费者的API提供了多种方式去指定消费的topic一些方式需要考虑post-object-instantiation设置。消费策略提供了一个抽象,来允许spark即使在重启检查点(checkpoint)后也能获得正确配置的消费者。
ConsumerStrategies.Subscribe,如上所示,允许你订阅一个固定的topic的集合。SubscribePattern允许你是用正则来指定你订阅的topic。注意不同于0.8版本,使用Subscribe或者SubscribePattern需要在流处理中对添加分区进行响应。Assign允许你指定固定分区的集合。以上三种方式都重载了构造函数允许你指定每个分区初始的偏移量。
如果上述的策略没有满足你的需求,而使你不得不指定你自己的消费者的策略,那么你可以使用ConsumerStrategy这个公共类自行拓展。
创建RDD(Creating an RDD)
如果你有一个更适合使用批处理的应用场景,可以定义一个范围的偏移量来创建一个RDD。
SCALA版本代码:
val
offsetRanges
=
Array
(
// topic, partition, inclusive starting offset, exclusive ending offset
OffsetRange
(
"test"
,
0
,
0
,
100
),
OffsetRange
(
"test"
,
1
,
0
,
100
)
)
val
rdd
=
KafkaUtils
.
createRDD
[
String
,
String
](
sparkContext
,
kafkaParams
,
offsetRanges
,
PreferConsistent
)
注意,不能使用PreferBrokers设置,因为在没有流数据的情况下不存在一个driver端的消费者为你拉取元数据。如果需要,使用ReferFixed设置来进行你自己的元数据查找。
获取偏移量(Obtaining Offsets)
SCALA版本:
stream
.
foreachRDD
{
rdd
=>
val
offsetRanges
=
rdd
.
asInstanceOf
[
HasOffsetRanges
].
offsetRanges
rdd
.
foreachPartition
{
iter
=>
val
o
:
OffsetRange
=
offsetRanges
(
TaskContext
.
get
.
partitionId
)
println
(
s"
${
o
.
topic
}
${
o
.
partition
}
${
o
.
fromOffset
}
${
o
.
untilOffset
}
"
)
}
}
注意HasOffsetRanges的类型转换只有在createDirectStream结果的第一个方法调用时才会成功,而不能在其之后的方法链中调用。需要认识到,RDD分区和Kafka分区之间的映射关系,在任何一个repartition或shuffle操作后(如reduceByKey()或Window())函数后都不再存在。
存储偏移量(Storing Offsets)
在失败的请款下,Kafka的交付语义取决于偏移量什么时候被保存。Spark的输出操作是
至少一次的。因此如果你想获得仅仅一次的语义,你要么在幂等的输出操作后存储或在一次与输出操作并行的原子操作中存储。你有三种选择来提高代码的可信赖度(复杂度)来存储偏移量。
checkPoints(检查点)
如果你打开了Spark的checkpointing选线,偏移量会被保存在checkpoint里面。这是很容易使用的,但是有一些缺点。首先你的输出操作必须是幂等的,否则就会得到一些重复的输出;事物并不是一个好的选择。除此之外,如果你的代码有了更改,就不能从checkpoint之中恢复。对于计划升级,可以在旧代码运行的同事部署新的代码来缓解这个问题(因为输出是幂等的,所以不会造成冲突)。但是对于意料之外的故障而需要更改代码的,除非你有其他的方式来获取开始的偏移量,否则就会丢失数据。
kafka本身(Kafka itself)
Kafka本身有一个提交偏移量到一个独特的Kafka topic的API,默认情况下,一个新的消费者会周期性的提交偏移量。这几乎不会是你想要的确切的提交方式,因为消息在成功拉取但是还没有在Spark中输出结果,就会导致未确定的语义。这就是为什么流处理例子中会将enable.auto.commit参数设置为false。然而,你可以在你确保输出操作已经完成后使用commitSync API向Kafka提交偏移量。与checkpoint方式相比,该种方式的好处是Kafka是一个持久化的存储,而不需要考虑代码的更新。然而,Kafka是非事物性的,所以你的输出操作仍需要具有幂等性。
Scala代码:
stream
.
foreachRDD
{
rdd
=>
val
offsetRanges
=
rdd
.
asInstanceOf
[
HasOffsetRanges
].
offsetRanges
// some time later, after outputs have completed
stream
.
asInstanceOf
[
CanCommitOffsets
].
commitAsync
(
offsetRanges
)
}
与HasOffsetRanges类似,转为CanCommitOffsets类型的操作只有在createDirectStream操作后可以调用,而不能在其他transformation操作后。commitAsync方法是线程安全的,如果你想获得有意义的语义那么必须在输出操作之后调用它。
你自己数据的存储(Your own data store)
对于支持事务的数据存储,可以在同一个事务中保存偏移量,这样即便在失败的情况下也可以保证两者的同步。如果你关心重复的或者跳过的偏移量额范围,回滚事务可以防止重复或丢失消息影响结果。这等价于仅仅一次的语义。也可能使用这种策略来对那些通常很难保证幂等性的聚合输出操作起作用。
// The details depend on your data store, but the general idea looks like this
// begin from the the offsets committed to the database
val
fromOffsets
=
selectOffsetsFromYourDatabase
.
map
{
resultSet
=>
new
TopicPartition
(
resultSet
.
string
(
"topic"
),
resultSet
.
int
(
"partition"
))
->
resultSet
.
long
(
"offset"
)
}.
toMap
val
stream
=
KafkaUtils
.
createDirectStream
[
String
,
String
](
streamingContext
,
PreferConsistent
,
Assign
[
String
,
String
](
fromOffsets
.
keys
.
toList
,
kafkaParams
,
fromOffsets
)
)
stream
.
foreachRDD
{
rdd
=>
val
offsetRanges
=
rdd
.
asInstanceOf
[
HasOffsetRanges
].
offsetRanges
val
results
=
yourCalculation
(
rdd
)
// begin your transaction
// update results
// update offsets where the end of existing offsets matches the beginning of this batch of offsets
// assert that offsets were updated correctly
// end your transaction
}
SSL.TLS
新版本的Kafka Consumer
支持SSL
。开启他只需要在将KafkaParams 传给createDirectStream或createRDD前赋合适的值。注意,这仅仅适用于spark和kafka 服务器之间的交流,你同样需要保证Spark节点内部之间的安全
(Spark安全)
通信。
val
kafkaParams
=
Map
[
String
,
Object
](
// the usual params, make sure to change the port in bootstrap.servers if 9092 is not TLS
"security.protocol"
->
"SSL"
,
"ssl.truststore.location"
->
"/some-directory/kafka.client.truststore.jks"
,
"ssl.truststore.password"
->
"test1234"
,
"ssl.keystore.location"
->
"/some-directory/kafka.client.keystore.jks"
,
"ssl.keystore.password"
->
"test1234"
,
"ssl.key.password"
->
"test1234"
)
程序部署(Deploying)
同Spark应用一样,spark-submit命令被用来部署你的程序。
对于JAVA或Scala应用来说,如果你使用SBT或MAVEN来做项目管理,需要将spark-streaming-kafka-010_2.11包以及它的依赖包添加到你的应用的JAR包中。确保spark-core+2.11包和spark-streaming_2.11包在你的依赖中位provided级别,因为他们在Spark的安装包中已经提供了。接下来使用spark-submit命令来部署你的应用(详情请看
部署环节)