Spark Streaming整合Kafaka

Kafka版本选择

Kafka属于Spark Streaming中的高级Sources。
Kafka:Spark Streaming 2.4.3与Kafka broker版本0.8.2.1或更高版本兼容,跟0.8.2.1之前的版本是不兼容的。详情请看Kafka Integration Guide

Apache Kafka is publish-subscribe messaging rethought as a distributed, partitioned, replicated commit log service.
Apache Kafka 是一个高吞吐量分布式消息系统,由LinkedIn开源。Apache Kafka 是发布-订阅机制的消息传递系统,可以认为具有分布式、分区、复制的日志提交功能的服务。

Kafka项目在0.8和0.10版本之间引入了一个新的消费者API,因此有两个独立的相关的Spark Streaming packages可用。请根据你的brokers 和所需功能选择正确的package;请注意,0.8集成与 后面的0.9和0.10 的brokers兼容,但0.10集成与之前的brokers是不兼容的。
Note: Kafka 0.8 support is deprecated as of Spark 2.3.0.
请注意:在Spark 2.3.0版本,Kafka 0.8 被标记为过时。标记为过时的,在以后有可能会直接不用了,只是现在还保留。
如果生产上是0.8版本,请看spark-streaming-kafka-0-8
(0.8及以上版本到0.10以下版本)
如果生产上是0.10版本,请看spark-streaming-kafka-0-10
(0.10及以上版本)
在这里插入图片描述
从上面可以看出来,
spark-streaming-kafka-0-8支持0.8.2.1的Broker Version以及更高版本,而spark-streaming-kafka-0-10支持的是0.10.0的Broker Version以及更高版本。所以0.10.0版本之前的Broker Version只能使用spark-streaming-kafka-0-8。
Receiver DStream是个很古老的东西,在0.10.0以及没有了,只有一种模式Direct DStream。
Offset Commit API偏移量管理,在以前需要自己管理,但在0.10.0,它可以自动管理自己提交了。(这个在本文最后会用到)
总结:生产上,要根据Kafka的版本(就是broker)去选择相应的Spark Streaming Kafka的版本。

环境准备
zookeeper和kafka单节点部署

需要安装:
zookeeper-3.4.5-cdh5.7.0.tar.gz
kafka_2.11-0.10.1.1.tgz

下载网址:
wget http://archive.cloudera.com/cdh5/cdh/5/zookeeper-3.4.5-cdh5.7.0.tar.gz
wget http://archive.apache.org/dist/kafka/0.10.1.1/kafka_2.11-0.10.1.1.tgz

安装配置步骤网上找一下就好了,这里是单节点,具体不再详述,可参考:

https://yq.aliyun.com/articles/413884
https://www.cppentry.com/bencandy.php?fid=120&id=198730

另外需要注意的是:kafka里面的config/server.properties这个配置文件,有个地方需要配置一下,advertised.listeners,否则外面是无法访问进来的。

# Hostname and port the broker will advertise to producers and consumers. If not set,
# it uses the value for "listeners" if configured.  Otherwise, it will use the value
# returned from java.net.InetAddress.getCanonicalHostName().
#advertised.listeners=PLAINTEXT://your.host.name:9092

假如不配置advertised.listeners这个的话,运行IDEA streaming去访问kafka,会报警告:无法和Broker建立连接:

WARN NetworkClient: [Consumer clientId=consumer-1, groupId=use_a_separate_group_id_for_each_stream]  Connection to node 1 could not be established. Broker may not be available.
启动zookeeper和kafka
启动zookeeper
[hadoop@hadoop001 bin]$ pwd
/home/hadoop/app/zookeeper-3.4.5-cdh5.7.0/bin
[hadoop@hadoop001 bin]$ ./zkServer.sh start
Connecting to localhost:2181
.....此处省略1000字

启动kafka

[hadoop@hadoop001 kafka_2.11-0.10.1.1]$ bin/kafka-server-start.sh -daemon config/server.properties
[hadoop@hadoop001 kafka_2.11-0.10.1.1]$ jps
23522 QuorumPeerMain
25395 Kafka

创建topic

[hadoop@hadoop001 kafka_2.11-0.10.1.1]$ bin/kafka-topics.sh \
> --create \
> --zookeeper hadoop001:2181 \
> --partitions 1 \
> --replication-factor 1 \
> --topic liweitest
Created topic "liweitest".

[hadoop@hadoop001 kafka_2.11-0.10.1.1]$ bin/kafka-topics.sh  --list --zookeeper hadoop001:2181
liweitest

启动生产者

[hadoop@hadoop001 kafka_2.11-0.10.1.1]$ bin/kafka-console-producer.sh --broker-list hadoop001:9092 --topic liweitest

启动消费者

[hadoop@hadoop001 kafka_2.11-0.10.1.1]$ bin/kafka-console-consumer.sh --zookeeper hadoop001:2181 --topic liweitest
Using the ConsoleConsumer with old consumer is deprecated and will be removed in a future major release. Consider using the new consumer by passing [bootstrap-server] instead of [zookeeper].

在生产者这边,生产一条数据:

[hadoop@hadoop001 kafka_2.11-0.10.1.1]$ bin/kafka-console-producer.sh --broker-list hadoop001:9092 --topic liweitest
this is a kafka test

消费者那边即可看到:

[hadoop@hadoop001 kafka_2.11-0.10.1.1]$ bin/kafka-console-consumer.sh --zookeeper hadoop001:2181 --topic liweitest
Using the ConsoleConsumer with old consumer is deprecated and will be removed in a future major release. Consider using the new consumer by passing [bootstrap-server] instead of [zookeeper].
this is a kafka test
Spark Streaming整合Kafaka

参考官网:
http://spark.apache.org/docs/latest/streaming-kafka-0-10-integration.html

在设计上,Spark Streaming集成Kafka对于 0.10版本的类似于0.8版本(现在只讲Direct Stream,其它不管,这里没有receiver)。
Spark StreamingKafka对于 0.10版本的集成提供了更简化的并行度,在Kafka分区和Spark分区之间是 1:1 的对应关系,能够去访问偏移量和元数据。在有receiver的Spark Streaming里,Spark的分区和Kafka的分区根本不是一回事,但是现在是Direct Stream,那么两个分区就是一样了,一个Spark里的partition去消费一个Kafka里的partition更好。(以前是receiver的方式,现在是Direct Stream的方式,具体可以看上面提到的0.8.2.1官网)。
但是,由于较新的集成使用新的 Kafka consumer API而不是之前的simple API,因此在使用上存在显著的差异。
此版本的集成被标记为实验性的,因此API可能会发生更改。

依赖

对于使用SBT/Maven项目定义的Scala/Java应用程序,用以下工件artifact连接你的streaming应用程序:

groupId = org.apache.spark
artifactId = spark-streaming-kafka-0-10_2.12
version = 2.4.3

上面是官网给的,下面是本次的,版本根据自己的情况去选择:

      <groupId>org.apache.spark</groupId>
      <artifactId>spark-streaming-kafka-0-10_2.11</artifactId>
      <version>2.4.0</version>

注意:不用手动添加org.apache.kafka artifacts 的依赖 (e.g. kafka-clients) 。spark-streaming-kafka-0-10 artifact 已经具有适当的可传递依赖项,并且不同的版本可能会在难以诊断的方式不兼容。意思就是说org.apache.kafka 相关的依赖已经被spark-streaming-kafka-0-10间接的给加进来了,所以不需要再添加。

创建Direct Stream
代码1.0

注意,导入的名称空间包括版本org.apache.spark.streaming.kafka010
参考官网代码,并修改一下:

package com.ruozedata.spark.com.ruozedata.spark.streaming

import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
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

object DirectKafkaApp {
  def main(args: Array[String]): Unit = {
  //因为是Direct Stream,没有receiver,所以可以是local[1]
    val conf = new SparkConf().setMaster("local[1]").setAppName("DirectKafkaApp")
    val ssc = new StreamingContext(conf, Seconds(10))

    val kafkaParams = Map[String, Object](
      "bootstrap.servers" -> "hadoop001:9092",
      "key.deserializer" -> classOf[StringDeserializer],
      "value.deserializer" -> classOf[StringDeserializer],
      "group.id" -> "use_a_separate_group_id_for_each_stream",
      "auto.offset.reset" -> "latest", //earlist
      "enable.auto.commit" -> (false: java.lang.Boolean)
    )

    //如果多个topic,用逗号分隔("topicA", "topicB")
    val topics = Array("liweitest")
    val stream = KafkaUtils.createDirectStream[String, String](
      ssc,
      PreferConsistent,
      Subscribe[String, String](topics, kafkaParams)
    )

    stream.map(record => (record.value)).flatMap(_.split(",")).map((_,1)).reduceByKey(_+_)
    .print()

    ssc.start()
    ssc.awaitTermination()
  }

}

让程序跑起来后,去kafka生产者上,生产一条数据:

[hadoop@hadoop001 kafka_2.11-0.10.1.1]$ bin/kafka-console-producer.sh --broker-list hadoop001:9092 --topic liweitest
word,love,shanghai,beijing,word,word,love

然后在IDEA中就可以看到输出:

-------------------------------------------
Time: 1564498450000 ms
-------------------------------------------

-------------------------------------------
Time: 1564498460000 ms
-------------------------------------------
(love,2)
(word,3)
(beijing,1)
(shanghai,1)
代码2.0

上面虽然跑通了,但是很多东西都是写死的,生产上肯定不可能这样的,很多东西需要用参数传进去。

package com.ruozedata.spark.com.ruozedata.spark.streaming

import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
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

object DirectKafkaApp {
  def main(args: Array[String]): Unit = {
    //需要传进来三个参数,判断传进来的参数是不是三个
    if(args.length !=3){
      System.err.print("Usage: DirectKafkaApp <brokers> <topic> <groupid>")
      System.exit(1)
    }

    //把参数赋值给一个数组
    val Array(brokers,topic,groupid) =args

    val conf = new SparkConf().setMaster("local[1]").setAppName("DirectKafkaApp")
    val ssc = new StreamingContext(conf, Seconds(10))

    val kafkaParams = Map[String, Object](
      "bootstrap.servers" -> brokers,
      "key.deserializer" -> classOf[StringDeserializer],
      "value.deserializer" -> classOf[StringDeserializer],
      "group.id" -> groupid,
      "auto.offset.reset" -> "latest", //earlist
      "enable.auto.commit" -> (false: java.lang.Boolean)
    )

    //Spark是支持多个topic的
    //如果多个topic,用逗号分隔("topicA", "topicB")
    val topics = topic.split(",")
    val stream = KafkaUtils.createDirectStream[String, String](
      ssc,
      PreferConsistent,
      Subscribe[String, String](topics, kafkaParams)
    )

    stream.map(record => (record.value)).flatMap(_.split(",")).map((_,1)).reduceByKey(_+_)
    .print()

    ssc.start()
    ssc.awaitTermination()
  }
}

IDEA参数传递,如下:
在这里插入图片描述
参数之间空格分割
在这里插入图片描述
然后运行程序,生产者产生一条数据,输出和上面运行的是一样的。

代码打成jar包(瘦包)上传服务器运行

然后把代码打包上传至服务器,然后使用。
在这里插入图片描述
在这里插入图片描述
打包成功。这里打包打的是瘦包,只包含代码,不包含依赖,建议打瘦包,不要打胖包,生产上绝对不允许打胖包的。

上传至服务器:

[hadoop@hadoop001 lib]$ ls
spark-train-1.0.jar
[hadoop@hadoop001 lib]$ pwd
/home/hadoop/lib

然后用spark-submit提交:

spark-submit \
--master local[2] \
--name DirectKafkaApp \
--class com.ruozedata.spark.com.ruozedata.spark.streaming.DirectKafkaApp \
/home/hadoop/lib/spark-train-1.0.jar \
hadoop001:9092 liweitest use_a_separate_group_id_for_each_stream

然后报错,找不到相关kafka的依赖:

Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/kafka/common/serialization/StringDeserializer
......
Caused by: java.lang.ClassNotFoundException: org.apache.kafka.common.serialization.StringDeserializer......

因为代码里导入的kafka(代码:import org.apache.kafka…)相关包都是依赖这个:

    <dependency>
      <groupId>org.apache.spark</groupId>
      <artifactId>spark-streaming-kafka-0-10_2.11</artifactId>
      <version>${spark.version}</version>
    </dependency>

因为打包的时候,spark-streaming-kafka-0-10_2.11这些包都没有打进来,所以找不到kafka相关的包。
解决方法是借助于 --pagages

//把第三方jar包,比如maven相关的jar包,传到driver和executor端,多个jar包用逗号分隔
//格式为:groupId:artifactId:version
//--packages org.apache.spark:spark-streaming-kafka-0-10_2.11:2.4.0
  --packages                  Comma-separated list of maven coordinates of jars to include
                              on the driver and executor classpaths. Will search the local
                              maven repo, then maven central and any additional remote
                              repositories given by --repositories. The format for the
                              coordinates should be groupId:artifactId:version.

调整好再来:

spark-submit \
--master local[2] \
--name DirectKafkaApp \
--class com.ruozedata.spark.com.ruozedata.spark.streaming.DirectKafkaApp \
--packages org.apache.spark:spark-streaming-kafka-0-10_2.11:2.4.0 \
/home/hadoop/lib/spark-train-1.0.jar \
hadoop001:9092 liweitest use_a_separate_group_id_for_each_stream

生产上可以写个shell脚本,就不用每次都写了。

//第一次运行,要去maven仓库下载依赖
[hadoop@hadoop001 lib]$ spark-submit \
> --master local[2] \
> --name DirectKafkaApp \
> --class com.ruozedata.spark.com.ruozedata.spark.streaming.DirectKafkaApp \
> --packages org.apache.spark:spark-streaming-kafka-0-10_2.11:2.4.0 \
> /home/hadoop/lib/spark-train-1.0.jar \
> hadoop001:9092 liweitest use_a_separate_group_id_for_each_stream
Ivy Default Cache set to: /home/hadoop/.ivy2/cache
The jars for the packages stored in: /home/hadoop/.ivy2/jars
:: loading settings :: url = jar:file:/home/hadoop/app/spark-2.4.2-bin-2.6.0-cdh5.7.0/jars/ivy-2.4.0.jar!/org/apache/ivy/core/settings/ivysettings.xml
org.apache.spark#spark-streaming-kafka-0-10_2.11 added as a dependency
:: resolving dependencies :: org.apache.spark#spark-submit-parent-4a185e21-b722-4a42-9669-6b11ff55575c;1.0
        confs: [default]
        found org.apache.spark#spark-streaming-kafka-0-10_2.11;2.4.0 in central
                found org.apache.kafka#kafka-clients;2.0.0 in central
        found org.lz4#lz4-java;1.4.0 in central
        found org.xerial.snappy#snappy-java;1.1.7.1 in central
        found org.slf4j#slf4j-api;1.7.16 in central
        found org.spark-project.spark#unused;1.0.0 in central
downloading https://repo1.maven.org/maven2/org/apache/spark/spark-streaming-kafka-0-10_2.11/2.4.0/spark-streaming-kafka-0-10_2.11-2.4.0.jar ...
        [SUCCESSFUL ] org.apache.spark#spark-streaming-kafka-0-10_2.11;2.4.0!spark-streaming-kafka-0-10_2.11.jar (8845ms)
downloading https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/2.0.0/kafka-clients-2.0.0.jar ...
............此处省略1000字
        org.xerial.snappy#snappy-java;1.1.7.1 from central in [default]
        ---------------------------------------------------------------------
        |                  |            modules            ||   artifacts   |
        |       conf       | number| search|dwnlded|evicted|| number|dwnlded|
        ---------------------------------------------------------------------
        |      default     |   6   |   6   |   6   |   0   ||   6   |   6   |
        ---------------------------------------------------------------------
:: retrieving :: org.apache.spark#spark-submit-parent-4a185e21-b722-4a42-9669-6b11ff55575c
        confs: [default]
        6 artifacts copied, 0 already retrieved (4436kB/10ms)
...........

然后去kafka生产者上面生产一条数据,就可以在spark-submit里看到了:

...........
INFO TaskSchedulerImpl: Removed TaskSet 31.0, whose tasks have all completed, from pool 
INFO DAGScheduler: ResultStage 31 (print at DirectKafkaApp.scala:44) finished in 0.046 s
INFO DAGScheduler: Job 15 finished: print at DirectKafkaApp.scala:44, took 0.150553 s
-------------------------------------------
Time: 1564502720000 ms
-------------------------------------------
(love,4)
(word,6)
(beijing,2)
(shanghai,2)

另外可以去WebUI上面看:http://hadoop001:4040/jobs/
去环境里可以看到,下面这些都是自己添加的,最后一个是我们自己上传的,另外几个是通过–packages加的。
在这里插入图片描述
另外,–packages直接会根据g、a、v三个参数把所有依赖下载下来,但是如果是–jars的话,需要你去手动一个一个的指定具体的jar包。jar包很少的时候可以使用–jars,但是如果jar包比较多,而且比较乱,不要使用–jars。
但是–packages这个需要条件:需要联网,不然无法去仓库下载依赖;要有私服。

打成小胖包(只包含部依赖)

除了上面说的–packages和–jars这两种方式,还可以打成一个小胖包。
比如:上面这些代码的依赖假如、有很多,如spark-core、spark-sql、spark-streaming、scala、hadoop、spark-streaming-kafka等等。生产环境上一般来说spark-core、spark-sql、spark-streaming、scala、hadoop这些jar包都是已经存在的了(比如Spark安装的时候就已经自带存在了),因为这些都是最基本的,而spark-streaming-kafka这个依赖很少用,生产上就没有,那么现在只需要设置一下其它的包不打进去(设置成provided),而只把spark-streaming-kafka这个依赖打进去即可。

①第一步
pom.xml文件里,如果某个依赖不要,就在后面加个 provided

//不需要hadoop的依赖,就在后面加个<scope>provided</scope>
    <!--添加Hadoop依赖-->
    <dependency>
      <groupId>org.apache.hadoop</groupId>
      <artifactId>hadoop-client</artifactId>
      <version>${hadoop.version}</version>
      <scope>provided</scope>
    </dependency>

其它类似,这里只需要两个,所以这两个不加provided

    <dependency>
      <groupId>com.jolbox</groupId>
      <artifactId>bonecp</artifactId>
      <version>0.8.0.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.apache.spark</groupId>
      <artifactId>spark-streaming-kafka-0-10_2.11</artifactId>
      <version>${spark.version}</version>
    </dependency>

②第二步
除了上面之外,还需要一个插件的支持,因为在编译的时候要用到

//放到<build>里面
<pluginManagement>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-assembly-plugin</artifactId>
            <configuration>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
            </configuration>
        </plugin>
    </plugins>
</pluginManagement>

③第三步
在这里插入图片描述
在这里插入图片描述
④第四步 打包
可以看到你设置的就出现了,然后点击运行
在这里插入图片描述
在这里插入图片描述
然后去相关文件夹下面可以看到,第一个是之前打的瘦包,不包含依赖,只有代码,总共是199kb。后面一个是刚刚打的jar包,包含我们需要的依赖,当然去除了hadoop、spark、scala等的依赖(如果完全打胖包会非常大),这里大概是12M大小。可以用解压软件打开jar包,看一下,里面已经加上了我们需要的依赖,不需要的依赖上没有的。
在这里插入图片描述

⑤上传jar包,运行
上传后

[hadoop@hadoop001 lib]$ ls
spark-train-1.0.jar  spark-train-1.0-jar-with-dependencies.jar

启动:(不需要–packages了)

spark-submit \
--master local[2] \
--name DirectKafkaApp \
--class com.ruozedata.spark.com.ruozedata.spark.streaming.DirectKafkaApp \
/home/hadoop/lib/spark-train-1.0-jar-with-dependencies.jar \
hadoop001:9092 liweitest use_a_separate_group_id_for_each_stream

在Kafka生产者上面生产一条数据

[hadoop@hadoop001 kafka_2.11-0.10.1.1]$ bin/kafka-console-producer.sh --broker-list hadoop001:9092 --topic liweitest
word,love,china,word,china,word

在spark-submit这边就可以看到输出结果了

-------------------------------------------
Time: 1564589170000 ms
-------------------------------------------
(love,1)
(word,3)
(china,2)

然后去WebUI界面的Environment看一下:http://hadoop001:4040/jobs/
在这里插入图片描述
在这里插入图片描述
至此,完成。这种方式不需要服务器去联网。

一些配置(了解)

kafka的参数,请参考kafka官网。如果,你的spark批次时间超过了kafka的心跳时间(30s),需要增加 heartbeat.interval.ms和session.timeout.ms。如果批处理时间是5min,那么就需要调整 group.max.session.timeout.ms。注意,例子中是将enable.auto.commit设置为了false。

本地策略

新版本Kafka的消费者API会预先获取消息(数据)到buffer缓冲区。因此,为了提升性能,在Executor端缓存消费者(这些数据)(而不是每个批次重新创建)是非常有必要的,并且优先在拥有适合的消费者所在的主机上调度安排分区。

可以使用LocationStrategies.PreferConsistent(在上面代码中有这个)这个参数会将分区尽量均匀地分配到可用的executors上去。如果你的executors和Kafka brokers在相同机器上,请使用PreferBrokers,它将优先将分区调度到kafka分区leader所在的主机上。这是不太可能的,因为是生产上基本上Kafka brokers和executors不会在相同的机器上的,一般Kafka都是单独的。所以呢用LocationStrategies.PreferConsistent这个就可以了。

关于offset

上面的代码中,在代码程序(Spark Streaming)启动之前,如果在Kafka生产者生产了一些数据,那么代码程序启动了之后,这些前面生产的数据都丢失了,Streaming程序是消费不了前面的数据的,只能消费streaming启动之后生产出来的数据。
这个跟这个设置有关,因为这里设置了"latest"只消费最新的:

"auto.offset.reset" -> "latest"
 "enable.auto.commit" -> (false: java.lang.Boolean) //这个要设置成false,原因后面会讲到

但是如果把这个设置修改成"earliest",从最早的地方开始消费:

"auto.offset.reset" -> "earliest"

那么它会从最开始把历史的数据都消费,如:

-------------------------------------------
Time: 1564671300000 ms
-------------------------------------------
(beijin,2)
(quit,1)
(love,62)
(,48)
(word,79)
(beijing,49)
(china,12)
(this is a test,1)
(this is a kafka test,7)
(shanghai,51)
-------------------------------------------
Time: 1564671310000 ms
-------------------------------------------

那应该怎么办呢?
把下面代码放到上面代码后面运行一下,可以打印一些东西:

//可以点进去看offsetRanges源码
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}")
  }
}

运行一下,可以看到输出:

-------------------------------------------
Time: 1564672020000 ms
-------------------------------------------
(beijin,2)
(quit,1)
(love,62)
(,48)
(word,79)
(beijing,49)
(china,12)
(this is a test,1)
(this is a kafka test,7)
(shanghai,51)

liweitest 0 0 114
-------------------------------------------
Time: 1564672030000 ms
-------------------------------------------

liweitest 0 114 114
-------------------------------------------
Time: 1564672080000 ms
-------------------------------------------
(this is a new test,1)

liweitest 0 114 115
-------------------------------------------
Time: 1564672090000 ms
-------------------------------------------

liweitest 0 115 115
-------------------------------------------
Time: 1564672100000 ms
-------------------------------------------
(love,1)
(word,1)

liweitest 0 115 116

第一次输出:liweitest 0 0 114
liweitest(topic主题) 0(分区数(本次就1个分区)) 0(从哪里开始消费) 114(到哪里)
后面我又增加了生产了几条新的记录,所以后面又有了:
liweitest 0 114 115
liweitest 0 115 116

那么如果你希望从某个地方开始消费,你肯定要提前把这个地方先存下来,到用的时候再把它拿出来就行了。

先来看一下streaming里的语义这个概念:
streaming系统的语义通常是根据系统可以处理每条记录的次数来捕获的。
①At most once:每条记录要么被处理一次要么不被处理。(这种方式生产上不能用,可能会丢数据)
②At least once:每条记录被处理一次或多次。这种方式可以确保不会丢失任何数据,但是可能有重复的数据。
③Exactly once: 每条记录被处理一次且仅有一次。这种方式,数据不会丢失,数据也不会被处理多次,没有重复。这种是三种方式中最强的一种保障。

官网上有说道:Spark output operations are at-least-once. 就是说Spark的输出操作是 at-least-once。这种方式可以确保不会丢失任何数据,但是可能有重复的数据。
所以说,如果你想保证Exactly once: 每条记录被处理一次且仅有一次,那么你必须在幂等的输出之后,把offset给存储下来。

那么怎么存下来呢?(重点)

Kafka has an offset commit API that stores offsets in a special Kafka topic.
Kafka有一个offset commit API,用于在特殊的Kafka topic中存储offset。
Offset Commit API偏移量管理,在以前需要自己管理,但在0.10.0,它可以自动管理自己提交了。
默认情况下,新消费者将周期性的自动提交offset。 但是,消费者虽然消费了这些消息,也成功了,不过在Spark中却没有进行输出或者输出失败了,消费者并不知道Spark是否把数据输出了,意思就是说虽然消费了这些数据,但是却输出却没有存下来,那么有什么用呢。那么这个结果就是没有定义的语义。这就是上面的代码中将“enable.auto.commit”设置为false的原因。
来看下图:
在这里插入图片描述
一般流程是:Kafka数据到Spark,Spark首先处理业务逻辑,然后进行commit offset,然后用foreachRDD把数据输出到DB。但是如果先commit offset,再处理业务逻辑就会有问题,提交以后,你不知道业务到底有没有成功处理完成。

如果你知道消费数据后的输出已被存储之后,就是说你的Spark结果处理完之后,再来把offset提交到Kafka上面去,(使用commitAsync API来进行提交)。
代码如下:

   //可以点击看源码
  // some time later, after outputs have completed
  stream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)

源码:

//Array[OffsetRange]是数组,因为它可能它有多个partition
  @Experimental
  def commitAsync(offsetRanges: Array[OffsetRange]): Unit
  ......

final class OffsetRange private(
    val topic: String,
    val partition: Int,
    val fromOffset: Long,
    val untilOffset: Long) extends Serializable {
    ......

代码详细一点:

    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}")
      }
      // some time later, after outputs have completed
      stream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
    }

这样的话就OK了。

具体分析:
①上面代码输出的结果是liweitest 0 115 116
就是topic是liweitest ,partition是0,消费从第115条到116条。
②把代码程序关掉。
③重新启动,那么就是:liweitest 0 0 116
就是topic是liweitest ,partition是0,消费从第0条到116条。
④过一会,会是liweitest 0 116 116
就是topic是liweitest ,partition是0,消费从第116条到116条。
④Kafka那边生产一条记录,Spark这边会输出liweitest 0 116 117
就是topic是liweitest ,partition是0,消费从第116条到117条。
⑤然后停掉Spark程序
⑥Kafka那边再生产2条记录(现在就有119条了)
⑦再重新启动,那么就是:liweitest 0 117 119
就是topic是liweitest ,partition是0,消费从第117条到119条。
就是说Spark停掉之后,Kafka又生产的数据不会丢失,它还会从上次记住的那个offset开始进行消费。

上面一部分讲的是offset存到哪里去,优先推荐Kafka。
其实offset可以存到很多地方,你可以自己去定义,比如ZK/HBase/MySQL/Redis…等等
都可以。生产上,如果Kafka是0.8到1.0之间的版本,那么Kafka是没有Offset Commit API的,那么只能自己去实现。
实现的逻辑?步骤?
(思路掌握,面试会被问到offset管理。offset偏移量管理是Spark Streaming的核心所在。具体代码可以不用,现在都可以用上面Kafka那种方式)
1)batch job批处理时间到了的时候,这个作业应该要去查询到已经消费过的offset的最大值(存到哪里就去哪里去取);
2)获取数据;
3)处理数据;
4)commit offset 提交offset (这个就是存到哪里去)
来看官网给的具体代码:

// The details depend on your data store, but the general idea looks like this

// begin from the the offsets committed to the database
//从你的数据库中拿到offset,
//拿出来,有三个东西:topic、partition、offset
//有了这三个东西,就可以确定offset存的最大的值了
//拿到之后转成map
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

  //下面是更新offset,把offset写回去,写回去之后,下次再拿的时候才能知道从从哪里开始消费
  // 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
}

比如你要把offset存到MySQL里,你肯定要在MySQL建个表来存:

MySQL
+-----------------------+-------------------------+------------+--------+
| topic                 | groupid                 | partitions | offset |
+-----------------------+-------------------------+------------+--------+
| offset_topic          | offset_test_group       |          0 |   118 |
+-----------------------+-------------------------+------------+--------+
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值