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 |
+-----------------------+-------------------------+------------+--------+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章