公司原来开发使用的是Kafka0.8版本的,虽然很好用,但是看了一下kafka官网的0.10版本更新了好多的特性,功能变得更强了。以后考虑换成0.10版本的,因此特意研究了一下两个版本的区别和使用方法。
一、spark-streaming-kafka-0-8_2.11-2.0.2.jar
1、pom.xml
1
package com.spark.main;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.function.Function;
import org.apache.spark.api.java.function.VoidFunction;
import org.apache.spark.streaming.Durations;
import org.apache.spark.streaming.api.java.JavaDStream;
import org.apache.spark.streaming.api.java.JavaPairInputDStream;
import org.apache.spark.streaming.api.java.JavaStreamingContext;
import org.apache.spark.streaming.kafka.KafkaUtils;
import kafka.serializer.StringDecoder;
import scala.Tuple2;
public class KafkaConsumer{
public static void main(String[] args) throws InterruptedException{
/**
JavaPairInputDStream
StringDecoder.class, StringDecoder.class, kafkaParams, topicsSet);
/**
lines.foreachRDD(new VoidFunction
public void call(JavaRDD rdd) throws Exception {
rdd.foreach(new VoidFunction() {
public void call(String s) throws Exception {
System.out.println(s);
}
});
}
});
// Start the computation
jssc.start();
jssc.awaitTermination();
}
}
二、spark-streaming-kafka-0-10_2.11-2.0.2.jar
1、pom.xml
1
package com.spark.main;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.function.Function;
import org.apache.spark.api.java.function.VoidFunction;
import org.apache.spark.streaming.Durations;
import org.apache.spark.streaming.api.java.JavaDStream;
import org.apache.spark.streaming.api.java.JavaInputDStream;
import org.apache.spark.streaming.api.java.JavaPairInputDStream;
import org.apache.spark.streaming.api.java.JavaStreamingContext;
import org.apache.spark.streaming.kafka010.ConsumerStrategies;
import org.apache.spark.streaming.kafka010.KafkaUtils;
import org.apache.spark.streaming.kafka010.LocationStrategies;
import kafka.serializer.StringDecoder;
import scala.Tuple2;
public class Kafka10Consumer{
public static void main(String[] args) throws InterruptedException{
/**
//通过KafkaUtils.createDirectStream(…)获得kafka数据,kafka相关参数由kafkaParams指定
JavaInputDStream
jssc,
LocationStrategies.PreferConsistent(),
ConsumerStrategies.Subscribe(topicsSet, kafkaParams)
);
/**
lines.foreachRDD(new VoidFunction
public void call(JavaRDD rdd) throws Exception {
rdd.foreach(new VoidFunction() {
public void call(String s) throws Exception {
System.out.println(s);
}
});
}
});
// Start the computation
jssc.start();
jssc.awaitTermination();
}
}
1、老的方法 -使用Receivers 和kafka的高级API
2、新的方法( Spark 1.3 开始引入)-不适用Receivers。这两个方式拥有不同的编程模型,性能特征和语义保证,为了获得更多细节,继续往下读。对于目前的版本的spark。这两个方式都是稳定的。
方法1 基于Receiver的 方式
这个方法使用了一个Receiver 接收数据。这个Receiver 是用kafka 高级的 consumer的api实现的。对于所有的receiver,通过Receiver 接收的kafka的数据会被存储到Spark的executors,然后 Spark Streaming 启动jobs处理数据。
然而 默认配置下,这个方式在失败的情况下回丢失数据(参考 receiver reliability.
)。为了保证零数据丢失,你必须在Spark Streaming (introduced in Spark 1.2)额外的开启Write Ahead Logs。这会同步的把接受的到kafka的数据写入到分布式系统(比如 HDFS) ahead logs 中,因此 所有的数据都可以在失败的时候进行恢复。 参考 Deploying section
以获取更多的关于 Write Ahead Logs.的信息。
下面, 我们讨论下如何在你的streaming的应用中 使用这个方法。
1、Linking: 对于Scala/Java 应用使用SBT/MAven的项目定义,连接到你的streaming的应用使用如下的artifact。
groupId = org.apache.spark
artifactId = spark-streaming-kafka-0-8_2.11
version = 2.2.0
2、编程: 如下所示
import org.apache.spark.streaming.kafka._
val kafkaStream = KafkaUtils.createStream(streamingContext,
[ZK quorum], [consumer group id], [per-topic number of Kafka partitions to consume])
你也可以通过使用 createStream 的参数指定key和value的类和以及它们响应的解码的类。参考See the API docsand the example.
需要记住的点
kafka中topic的分区和streaming中的RDD的分区是不一致的。因此增加在KafkaUtils.createStream()中增加topic的分区数只能增加使用消费这些topic的单台机器上的线程数。
为了接受并行的数据可以使用多个receiver 。不同的分组和topics可以创建多个kafka的DStreams.
如果你在一个可复制的文件系统内(比如hdfs)开启了Write Ahead Logs ,接收到的数据,接收到的数据已经在log中被复制了。因而,输入流的存储的等级是 StorageLevel.MEMORY_AND_DISK_SER。也就是说KafkaUtils.createStream(…, StorageLevel.MEMORY_AND_DISK_SER)).
3、 部署
和其他的Spark的应用一样,spark-submit 用来提交你的应用。然而 Scala/java 与python 在细节方面有稍微的不同。
对于Scala和Java的应用,如果你正在使用SBT或者maven管理项目,会把spark-streaming-kafka-0-8_2.11 和他的依赖打包到应用的JAR。需要确保spark-core_2.11 和spark-streaming_2.11 被标记为provided,因为这些在Spark的安装中就已经有了。然后使用spark-submit 提交你的应用(参见主编程指南中的部署部分)。
Python应用没有SBT和maven项目管理,spark-streaming-kafka-0-8_2.11 和它的依赖可以直接添加到Spark 要提交的包中(见应用提交指南)。
./bin/spark-submit --packages org.apache.spark:spark-streaming-kafka-0-8_2.11:2.2.0 …
或者,你也可以从maven的仓库下载 Maven 的 spark-streaming-kafka-0-8-assembly ,把它加到你要提交 spark-submit 通过–jars的形式。
方法2 :Direct Approcach(没有接收器)
在Spark1.3中引入了这种新的接收器较少的“直接”方法,以确保更强的端到端保证。该方法不使用接收者接收数据,而是周期性地查询Kafka在每个主题+分区中的最新偏移量,并据此定义每批(batch)处理的偏移范围。当处理数据的作业被启动时,Kafka的简单消费者API用于读取Kafka定义的偏移范围(类似于文件系统中的读取文件)。注意这个功能被引入是Spark1.3 在Scala和java API,Spark 1.4 才提供Python API。
这种方法比基于接收者的方法(即方法1)具有以下优点。
简化的并行性:无需创建多个输入Kfaka流并将它们合并。使用DirectStream, Spark Streaming 将创建和Kafka分区一样多的RDD分区进行消费,将所有kafka的数据并行读。所以Kafka和RDD的分区之间是一对一的映射,这样比较容易理解和调整。
效率:在第一种方法中实现零数据丢失要求将数据存储在Write Ahead Log,中,从而进一步复制数据。
这实际上是低效的,因为数据复制了两次,一次是Kafka,第二次是 Write Ahead Log。第二种方法消除了问题,因为没有接收器,因此不需要Write Ahead Log。只要Kafka有保存,消息就可以从Kafka中恢复。
Exactly-once 语义:第一种方法使用Kafka的高层API在ZooKeeper存储消费的偏移量。从传统上来说,这是从Kafka获取数据的方式。虽然这种方法(与write ahead logs相结合)可以确保零数据丢失(即至少一次语义),但有些记录在某些故障下可能会被消费 两次。这是因为Spark Streaming 接收的数据之间的不一致性以及Zookeeper中的偏移量。因而,在第二个方法中,我们使用简单的不使用Zookeeper 的Kafka的API。Spark Streaming用checkpoints跟踪偏移量 。这样消除了Spark Streaming 和Zookeeper/kafka的不一致性,所以无论是否失败,Spark Streaming 每个记录都只会被恰好接收一次。为了实现输出结果的exactly-once 的语义,你保存数据的操作必须要么是幂等的,要么是原子的(保存结果和偏移量)。(请参阅主编程指南中的输出操作的语义信息)。
注意,这种方法的一个缺点是它不更新ZK中的偏移量, 因此Zookeeper-based Kafka 监控器不会显示进度。然而,你可以通过这个方式在每个batch中访问偏移量和更新Zookeeper (见下文)。
接下来,我们将讨论如何在Streaming 的应用程序中使用此方法。
链接:这种方法只支持在scala/ java应用程序。你的SBT/Maven项目使用如下的artifact(见链接,在进一步信息的主要编程指南部分)。
groupId = org.apache.spark
artifactId = spark-streaming-kafka-0-8_2.11
version = 2.2.0
import org.apache.spark.streaming.kafka._
val directKafkaStream = KafkaUtils.createDirectStream[
[key class], [value class], [key decoder class], [value decoder class] ](
streamingContext, [map of Kafka parameters], [set of topics to consume])
你也可以传入messagehandler到createdirectstream访问messageandmetadata包含有关当前消息元数据,并将它转换为任意类型。请参见API文档和示例。
在Kafka的参数中,您必须指定metadata.broker.list或bootstrap.servers。默认情况下,它将从每个Kafka分区的最新偏移量开始消费。如果你设置的配置auto.offset.reset 参数smallest,它将从最小的偏移处开始消费。
你也可以使用的其他变化kafkautils.createdirectstream,从任意的偏移开始消费。此外,如果您希望访问每个batch中消耗的Kafka偏移量,您可以执行以下操作。
// Hold a reference to the current offset ranges, so it can be used downstream
var offsetRanges = Array.empty[OffsetRange]
directKafkaStream.transform { rdd =>
offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
rdd
}.map {
...
}.foreachRDD { rdd =>
for (o <- offsetRanges) {
println(s"${o.topic} ${o.partition} ${o.fromOffset} ${o.untilOffset}")
}
新版本的Kafka消费者API会将预先获取的消息写入缓存。因此,Spark在Executor端缓存消费者(而不是每次都重建)对于性能非常重要 ,并且系统会自动为分区分配在同一主机上的消费者进程(如果有的话)
一般来讲,你最好像上面的Demo一样使用LocationStrategies的PreferConsistent方法。
它会将分区数据尽可能均匀地分配给所有可用的Executor。
题外话:本地化策略看到这里就行了,下面讲的是一些特殊情况。
如果你的Executor和kafka broker在同一台机器上,可以用PreferBrokers,这将优先将分区调度到kafka分区leader所在的主机上。
题外话:废话,Executor是随机分布的,我怎么知道是不是在同一台服务器上?除非是单机版的are you明白?
分区之间的负荷有明显的倾斜,可以用PreferFixed。这个允许你指定一个明确的分区到主机的映射(没有指定的分区将会使用连续的地址)。
题外话:就是出现了数据倾斜了呗
Kafka的最大分区数
消费者缓存的数目默认最大值是64。如果你希望处理超过(64*excutor数目)kafka分区,配置参数:spark.streaming.kafka.consumer.cache.maxCapacity
是否禁止Kafka消费者缓存
如果你想禁止kafka消费者缓存,可以将spark.streaming.kafka.consumer.cache.enabled修改为false。禁止缓存缓存可能需要解决SPARK-19185描述的问题。一旦这个bug解决,这个属性将会在后期的spark版本中移除(Spark2.2.1的Bug)。
缓存的唯一性
Cache是根据topic的partition和groupid来唯一标识的,所以每次调用createDirectStream的时候要单独设置group.id。
消费者策略:
新的Kafka消费者API有许多不同的方式来指定主题。它们相当多的是在对象实例化后进行设置的。ConsumerStrategies提供了一种抽象,它允许Spark即使在checkpoint重启之后(也就是Spark重启)也能获得配置好的消费者。
ConsumerStrategies允许订阅确切指定的一组Topic。
ConsumerStrategies的Subscribe方法通过一个确定的集合来指定Topic
ConsumerStrategies的SubscribePattern方法允许你使用正则表达式来指定Topic,
注意,与0.8集成不同,在SparkStreaming运行期间使用Subscribe或SubscribePattern,应该响应添加分区。
最后,ConsumerStrategies.Assign()方法允许指定固定的分区集合。
所有三个策略(Subscribe,SubscribePattern,Assign)都有重载的构造函数,允许您指定特定分区的起始偏移量。
如果上述不满足您的特定需求,可以对ConsumerStrategy进行扩展、重写
到这里Direct方式参数和原理也讲完了,下面bibi了一堆其他的没用的,至少不常用,如果你还有耐心的话可以留着看其他更重要的部分,如果你还有心情的话想看还可以看看(博客写到这里我真是没有心情了,太难翻译了,休息一下再写吧)
创建一个RDD(Creating an RDD)
这部分官网罗里罗嗦一大堆,没看明白什么意思,看了好多博客,又跑了几次程序才明白是怎么回事。
意思就是原来是通过时间来取数据的,比如说一次取个5秒的数据什么的,这个方法是根据条数来取的,比如说一次取个100条,但是没有看明白的是:这不是SparkStreaming吗?怎么成RDD了?RDD怎么进行流式数据处理啊?然后下面又给出了一个例子
OffsetRange[] offsetRanges = {
// topic, partition, inclusive starting offset, exclusive ending offset
//参数依次是Topic名称,Kafka哪个分区,开始位置(偏移),结束位置
OffsetRange.create(“test”, 0, 0, 100),
OffsetRange.create(“test”, 1, 0, 100)
};
//注意,这里返回的是一个RDD
JavaRDD> rdd = KafkaUtils.createRDD(
sparkContext,
kafkaParams,
offsetRanges,
LocationStrategies.PreferConsistent()
);
获取偏移
在进行流式批处理的时候根据条数取数据还是有点用的,官网给出了一个例子
stream.foreachRDD(new VoidFunction>>() {
@Override
public void call(JavaRDD> rdd) {
//获取偏移
final OffsetRange[] offsetRanges = ((HasOffsetRanges) rdd.rdd()).offsetRanges();
rdd.foreachPartition(new VoidFunction>>() {
@Override
public void call(Iterator> consumerRecords) {
OffsetRange o = offsetRanges[TaskContext.get().partitionId()];
//获取Topic,Partition,起始偏移量,结束偏移量
System.out.println(
o.topic() + " " + o.partition() + " " + o.fromOffset() + " " + o.untilOffset());
}
});
}
});
存储偏移
不按官方的翻译了,这段的大致意思是:要想在任何情况下(注意是任何情况)都保证Kafka的数据都只被消费一次你还需要存储一下Kafka的偏移量,有三种方式来存储Kafka的偏移量。
第一种方式:Checkpoint(Spark的一个持久化机制)
这种方式很容易做到,但是有以下的缺点:
多次输出,结果必须满足幂等性(什么意思自己Google)
事务性不可选
如果代码变更不能从Checkpoint恢复,不过你可以同时运行新任务和旧任务,因为输出结果具有等幂性
二种方式:Kafka自身
Kafka提供的有api,可以将offset提交到指定的kafkatopic。默认情况下,新的消费者会周期性的自动提交offset到kafka。但是有些情况下,这也会有些问题,因为消息可能已经被消费者从kafka拉去出来,但是spark还没处理,这种情况下会导致一些错误。这也是为什么例子中stream将enable.auto.commit设置为了false。然而在已经提交spark输出结果之后,你可以手动提交偏移到kafka。相对于checkpoint,offset存储到kafka的好处是:kafka既是一个容错的存储系统,也是可以避免代码变更带来的麻烦。然而,Kafka不是事务性的,所以你的输出必须仍然是幂等的。
stream.foreachRDD(new VoidFunction>>() {
@Override
public void call(JavaRDD> rdd) {
OffsetRange[] offsetRanges = ((HasOffsetRanges) rdd.rdd()).offsetRanges();
// some time later, after outputs have completed
((CanCommitOffsets) stream.inputDStream()).commitAsync(offsetRanges);
}
});
第三种方式:自定义存储位置
对于支持事务的数据存储,即使在故障情况下,也可以在同一事务中保存偏移量作为结果,以保持两者同步。如果您仔细检查重复或跳过的偏移范围,则回滚事务可防止重复或丢失的邮件影响结果。这给出了恰好一次语义的等价物。也可以使用这种策略甚至对于聚合产生的输出,聚合通常很难使幂等。
// The details depend on your data store, but the general idea looks like this
// begin from the the offsets committed to the databaseMap fromOffsets = new HashMap<>();for (resultSet : selectOffsetsFromYourDatabase)
fromOffsets.put(new TopicPartition(resultSet.string("topic"), resultSet.int("partition")), resultSet.long("offset"));}
JavaInputDStream> stream = KafkaUtils.createDirectStream(
streamingContext,
LocationStrategies.PreferConsistent(),
ConsumerStrategies.Assign(fromOffsets.keySet(), kafkaParams, fromOffsets));
stream.foreachRDD(new VoidFunction>>() {
@Override
public void call(JavaRDD> rdd) {
OffsetRange[] offsetRanges = ((HasOffsetRanges) rdd.rdd()).offsetRanges();
Object 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
}});